From b32f28e71252fd90c70602671de257acbfcfad62 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Mon, 7 Sep 2026 14:00:28 +0800 Subject: [PATCH 01/29] Initial Commit of speed dial improvement --- resources/web/dialog/SpeedDial/speeddial.js | 981 ++++++++++++------ .../web/dialog/SpeedDial/speeddial.test.js | 82 +- resources/web/dialog/SpeedDial/style.css | 31 + src/slic3r/GUI/ActionRegistry.cpp | 390 ++++++- src/slic3r/GUI/ActionRegistry.hpp | 45 +- src/slic3r/GUI/GLCanvas3D.cpp | 6 - src/slic3r/GUI/GLCanvas3D.hpp | 1 - src/slic3r/GUI/KBShortcutsDialog.cpp | 4 +- src/slic3r/GUI/MainFrame.cpp | 19 +- src/slic3r/GUI/Notebook.cpp | 15 +- src/slic3r/GUI/Plater.cpp | 4 - src/slic3r/GUI/SpeedDialDialog.cpp | 149 ++- src/slic3r/GUI/SpeedDialDialog.hpp | 4 +- src/slic3r/GUI/Widgets/Button.cpp | 230 ++-- src/slic3r/GUI/Widgets/Button.hpp | 46 +- 15 files changed, 1441 insertions(+), 566 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 65f64b4399..b25df97723 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -1,14 +1,22 @@ // 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) ---- -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 RECENTS = []; // [{id,title,source,group,input,shortcut}] - last-N launched var query = ""; var sel = { zone: "list", i: 0 }; // zone: 'list' | 'fav' var lastResizeHeight = 0; 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 // this script - it is shared with the Plugins dialog. Speed dial search is always case-insensitive. @@ -17,251 +25,352 @@ var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, count // ---- pure helpers (no DOM; unit-tested) ------------------------------------- function filterActions(actions, query) { - var q = (query || "").trim(); - var list = actions || []; - matchIndex = {}; - if (!q) - return list.slice(0); + var q = (query || "").trim(); + var list = actions || []; + matchIndex = {}; + if (!q) + return list.slice(0); - var out = []; - for (var i = 0; i < list.length; i++) { - var a = list[i]; - var titleMatch = FuzzyRanges(a.title, q, false); - var sourceMatch = FuzzyRanges(a.source, q, false); - if (!titleMatch && !sourceMatch) - continue; - matchIndex[a.id] = { title: titleMatch, source: sourceMatch, useTitle: !!titleMatch }; - out.push(a); - } - return out; + var out = []; + for (var i = 0; i < list.length; i++) { + var a = list[i]; + var titleMatch = FuzzyRanges(a.title, q, false); + var sourceMatch = FuzzyRanges(a.source || "", q, false); + if (!titleMatch && !sourceMatch) + continue; + matchIndex[a.id] = { title: titleMatch, source: sourceMatch, useTitle: !!titleMatch }; + out.push(a); + } + return out; } function visibleFavourites(favourites, actions) { - // why: a fav whose id has no live action (plugin unloaded/disabled) renders a dead - // monogram tile whose click run()s to a silent no-op; drop it from the quick-bar. - var seen = {}; - (actions || []).forEach(function (a) { seen[a.id] = true; }); - return (favourites || []).filter(function (id, i, arr) { - return seen[id] && arr.indexOf(id) === i; - }); + // why: a fav whose id has no live action (plugin unloaded/disabled) renders a dead + // monogram tile whose click run()s to a silent no-op; drop it from the quick-bar. + var seen = {}; + (actions || []).forEach(function (a) { seen[a.id] = true; }); + return (favourites || []).filter(function (id, i, arr) { + return seen[id] && arr.indexOf(id) === i; + }); } 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 +// " " (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) { - 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 -// 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) { - if (sel.zone === "fav") - return favIds[sel.i]; - // why: search-first keeps the list blank until the user types; an empty query still - // filters to ALL actions, so without this gate Enter fires an action never shown. - if (!shouldRenderActionList(query)) - return null; - var a = actions[sel.i]; - return a && a.id; + if (sel.zone === "fav") + return favIds[sel.i]; + if (!actions || !actions.length) + return null; + var a = actions[sel.i]; + return a && a.id; } function foldLabel(s) { return String(s || "").toLowerCase().replace(/[^a-z0-9]+/g, ""); } // Title-case a source for display: "GCODE OPTIMIZER"/"iRoNiNg pRo" -> "Gcode Optimizer"/"Ironing Pro". function prettySource(source) { - return String(source || "").toLowerCase().replace(/\b\w/g, function (c) { return c.toUpperCase(); }); + return String(source || "").toLowerCase().replace(/\b\w/g, function (c) { return c.toUpperCase(); }); } // 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. function actionLabel(action, actions) { - var label = action.title + " from " + prettySource(action.source); - if (actions && actions.length) { - var mine = foldLabel(action.title) + "|" + foldLabel(action.source); - var clash = actions.some(function (o) { - return o.id !== action.id && foldLabel(o.title) + "|" + foldLabel(o.source) === mine; - }); - if (clash) - label += " (" + action.id + ")"; - } - return label; + var label = action.title + " from " + prettySource(action.source || action.group || ""); + if (actions && actions.length) { + var mine = foldLabel(action.title) + "|" + foldLabel(action.source || action.group || ""); + var clash = actions.some(function (o) { + return o.id !== action.id && foldLabel(o.title) + "|" + foldLabel(o.source || o.group || "") === mine; + }); + if (clash) + label += " (" + action.id + ")"; + } + return label; } // 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. -// why: ordinal is assigned by id, not by ACTIONS order - ACTIONS is frecency-sorted and +// 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 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. -function tileCode(action, actions) { - var list = actions || []; - var ti = (action.title || " ").charAt(0).toUpperCase(); - var sameTitle = list.filter(function (o) { return (o.title || " ").charAt(0).toUpperCase() === ti; }); - if (sameTitle.length <= 1) - return ti; - var pi = (action.source || " ").charAt(0).toUpperCase(); - var sameSource = sameTitle.filter(function (o) { return (o.source || " ").charAt(0).toUpperCase() === pi; }); - if (sameSource.length <= 1) +function monogramFor(item, list, titleOf, sourceOf, idOf) { + var items = list || []; + var title = titleOf(item) || " "; + var ti = title.charAt(0).toUpperCase(); + var sameTitle = items.filter(function (o) { return (titleOf(o) || " ").charAt(0).toUpperCase() === ti; }); + if (sameTitle.length <= 1) + return ti; + var source = sourceOf(item) || " "; + var pi = source.charAt(0).toUpperCase(); + var sameSource = sameTitle.filter(function (o) { return (sourceOf(o) || " ").charAt(0).toUpperCase() === pi; }); + if (sameSource.length <= 1) + return pi + ti; + 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++) + if (sameSource[i] === item || idOf(sameSource[i]) === idOf(item)) + return pi + ti + (i + 1); return pi + ti; - sameSource.sort(function (a, b) { return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; }); - for (var i = 0; i < sameSource.length; i++) - if (sameSource[i].id === action.id) - return pi + ti + (i + 1); - 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() { - if (clearEl) - clearEl.hidden = !query; + if (clearEl) + clearEl.hidden = !query; } function stateFromPayload(payload) { - return { - actions: payload.actions || [], - favourites: payload.favourites || [], - query: "", - sel: { zone: "list", i: 0 }, - lastResizeHeight: 0 - }; + return { + actions: payload.actions || [], + favourites: payload.favourites || [], + recent: payload.recent || [], + query: "", + sel: { zone: "list", i: 0 }, + lastResizeHeight: 0, + phase: "commands", + settingsResults: [], + tabOptions: [] + }; } function resetScrollPositions(list, doc) { - if (list) - list.scrollTop = 0; - if (doc && doc.scrollingElement) - doc.scrollingElement.scrollTop = 0; - if (doc && doc.documentElement) - doc.documentElement.scrollTop = 0; - if (doc && doc.body) - doc.body.scrollTop = 0; + if (list) + list.scrollTop = 0; + if (doc && doc.scrollingElement) + doc.scrollingElement.scrollTop = 0; + if (doc && doc.documentElement) + doc.documentElement.scrollTop = 0; + if (doc && doc.body) + doc.body.scrollTop = 0; } // nextSel: pure arrow-nav transition. Down fav->list0; Down list->clamp; Up list@0->fav0; // Up list->i-1; Left/Right clamp within fav. Returns a fresh {zone,i}. function nextSel(sel, key, listLen, favLen) { - var zone = sel.zone, i = sel.i; - if (key === "ArrowDown") { - if (zone === "fav") return { zone: "list", i: 0 }; - return { zone: "list", i: Math.min(i + 1, Math.max(0, listLen - 1)) }; - } - if (key === "ArrowUp") { - if (zone === "list") { - if (i <= 0) return favLen ? { zone: "fav", i: 0 } : { zone: "list", i: 0 }; - return { zone: "list", i: i - 1 }; + var zone = sel.zone, i = sel.i; + if (key === "ArrowDown") { + if (zone === "fav") return { zone: "list", i: 0 }; + return { zone: "list", i: Math.min(i + 1, Math.max(0, listLen - 1)) }; } + if (key === "ArrowUp") { + if (zone === "list") { + if (i <= 0) return favLen ? { zone: "fav", i: 0 } : { zone: "list", i: 0 }; + return { zone: "list", i: i - 1 }; + } + return { zone: zone, i: i }; + } + if (key === "ArrowLeft" && zone === "fav") return { zone: "fav", i: Math.max(0, i - 1) }; + if (key === "ArrowRight" && zone === "fav") return { zone: "fav", i: Math.min(favLen - 1, i + 1) }; return { zone: zone, i: i }; - } - if (key === "ArrowLeft" && zone === "fav") return { zone: "fav", i: Math.max(0, i - 1) }; - if (key === "ArrowRight" && zone === "fav") return { zone: "fav", i: Math.min(favLen - 1, i + 1) }; - return { zone: zone, i: i }; } // ---- bridge ------------------------------------------------------------------ function SendMessage(msg) { - if (typeof SendWXMessage !== "function") - return; - if (typeof msg === "string") msg = { command: msg }; - if (msg.sequence_id === undefined) msg.sequence_id = Date.now(); - SendWXMessage(JSON.stringify(msg)); + if (typeof SendWXMessage !== "function") + return; + if (typeof msg === "string") msg = { command: msg }; + if (msg.sequence_id === undefined) msg.sequence_id = Date.now(); + 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) { - if (!payload) return; - if (typeof payload === "string") { try { payload = JSON.parse(payload); } catch (e) { return; } } - if (payload.command === "list_actions") { - var next = stateFromPayload(payload); - ACTIONS = next.actions; - FAVS = next.favourites; - query = next.query; - sel = next.sel; - lastResizeHeight = next.lastResizeHeight; - if (qEl) { - qEl.value = ""; - qEl.placeholder = "Search " + ACTIONS.length + " actions"; - syncClearButton(); + if (!payload) return; + if (typeof payload === "string") { try { payload = JSON.parse(payload); } catch (e) { return; } } + if (payload.command === "list_actions") { + var next = stateFromPayload(payload); + ACTIONS = next.actions; + FAVS = next.favourites; + RECENTS = next.recent; + query = next.query; + sel = next.sel; + lastResizeHeight = next.lastResizeHeight; + phase = next.phase; + settingsResults = next.settingsResults; + tabOptions = next.tabOptions; + if (qEl) { + qEl.value = ""; + qEl.placeholder = "Search " + ACTIONS.length + " actions"; + syncClearButton(); + } + render({ resize: true, resetScroll: true }); + 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 }); } - render({ resize: true, resetScroll: true }); - focusInput(); - } }; // ---- DOM helpers ------------------------------------------------------------- function $(id) { return document.getElementById(id); } function byId(id) { - for (var i = 0; i < ACTIONS.length; i++) if (ACTIONS[i].id === id) return ACTIONS[i]; - return null; + for (var i = 0; i < ACTIONS.length; i++) if (ACTIONS[i].id === id) return ACTIONS[i]; + 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); } +// 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) { - var h = 0; - for (var i = 0; i < id.length; i++) - h = (h * 31 + id.charCodeAt(i)) >>> 0; - return h % 360; + var h = 0; + for (var i = 0; i < id.length; i++) + h = (h * 31 + id.charCodeAt(i)) >>> 0; + return h % 360; } // Build a
with the search-match ranges wrapped in . Used for both the // title and the source eyebrow. Pure (only touches the document factory), so the node-vm test never // calls it and load-time stays DOM-free. function markedText(className, text, match) { - var node = document.createElement("div"); - node.className = className; node.title = text; - if (!match || !match.length) { - node.textContent = text; + var node = document.createElement("div"); + node.className = className; node.title = text; + if (!match || !match.length) { + node.textContent = text; + return node; + } + var last = 0; + for (var i = 0; i < match.length; i++) { + var range = match[i]; + if (range[0] > last) + node.appendChild(document.createTextNode(text.slice(last, range[0]))); + var m = document.createElement("mark"); + m.textContent = text.slice(range[0], range[1]); + node.appendChild(m); + last = range[1]; + } + if (last < text.length) + node.appendChild(document.createTextNode(text.slice(last))); return node; - } - var last = 0; - for (var i = 0; i < match.length; i++) { - var range = match[i]; - if (range[0] > last) - node.appendChild(document.createTextNode(text.slice(last, range[0]))); - var m = document.createElement("mark"); - m.textContent = text.slice(range[0], range[1]); - node.appendChild(m); - last = range[1]; - } - if (last < text.length) - node.appendChild(document.createTextNode(text.slice(last))); - return node; } function starSvg(on) { - return '' + - ''; + return '' + + ''; } // ---- render ------------------------------------------------------------------ function renderFav() { - favEl.innerHTML = ""; - var favs = currentVisibleFavs(); - favEl.hidden = favs.length === 0; - if (!favs.length && sel.zone === "fav") - sel = { zone: "list", i: 0 }; - else if (sel.zone === "fav") - sel.i = Math.max(0, Math.min(sel.i, favs.length - 1)); - updateFavEyebrow(favs); - favs.forEach(function (id, i) { - var a = byId(id); - var tile = document.createElement("button"); - tile.className = "fav-tile" + (sel.zone === "fav" && sel.i === i ? " sel" : ""); - tile.style.setProperty("--h", hue(id)); - tile.textContent = tileCode(a, ACTIONS); - tile.title = a.title; - tile.setAttribute("aria-label", actionLabel(a, ACTIONS)); - tile.onclick = function () { sel = { zone: "fav", i: i }; run(a); }; - tile.oncontextmenu = function (ev) { - ev.preventDefault(); - // why: selecting shows the eyebrow, which grows the launcher - resize so the popup - // isn't clipped (mirrors arrow-nav). requestResize no-ops when height is unchanged. - sel = { zone: "fav", i: i }; render({ resize: true }); - showFavMenu(ev.clientX, ev.clientY, id); - }; - favEl.appendChild(tile); - }); + // 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 = ""; + var favs = currentVisibleFavs(); + favEl.hidden = favs.length === 0; + if (!favs.length && sel.zone === "fav") + sel = { zone: "list", i: 0 }; + else if (sel.zone === "fav") + sel.i = Math.max(0, Math.min(sel.i, favs.length - 1)); + updateFavEyebrow(favs); + favs.forEach(function (id, i) { + var a = byId(id); + var tile = document.createElement("button"); + tile.className = "fav-tile" + (sel.zone === "fav" && sel.i === i ? " sel" : ""); + tile.style.setProperty("--h", hue(id)); + tile.textContent = tileCode(a, ACTIONS); + tile.title = a.title; + tile.setAttribute("aria-label", actionLabel(a, ACTIONS)); + 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 = ''; + unpin.onclick = function (ev) { ev.stopPropagation(); toggleFav(id); }; + tile.appendChild(unpin); + tile.oncontextmenu = function (ev) { + ev.preventDefault(); + // why: selecting shows the eyebrow, which grows the launcher - resize so the popup + // isn't clipped (mirrors arrow-nav). requestResize no-ops when height is unchanged. + sel = { zone: "fav", i: i }; render({ resize: true }); + showFavMenu(ev.clientX, ev.clientY, id); + }; + favEl.appendChild(tile); + }); } // ---- favourite context menu (right-click a tile) ----------------------------- @@ -270,83 +379,87 @@ var favMenuEl = null; function hideFavMenu() { if (favMenuEl) favMenuEl.hidden = true; } function addFavMenuItem(label, enabled, fn) { - var item = document.createElement("button"); - item.className = "ctx-item"; - item.textContent = label; - item.disabled = !enabled; - item.onclick = function () { hideFavMenu(); fn(); }; - favMenuEl.appendChild(item); + var item = document.createElement("button"); + item.className = "ctx-item"; + item.textContent = label; + item.disabled = !enabled; + item.onclick = function () { hideFavMenu(); fn(); }; + favMenuEl.appendChild(item); } // One reused menu node (Move left/right + Unpin), positioned at the cursor and clamped // to the viewport. Native browser context menus can't add items, so we roll our own tiny one. function showFavMenu(x, y, id) { - if (!favMenuEl) { - favMenuEl = document.createElement("div"); - favMenuEl.className = "ctx-menu"; - document.body.appendChild(favMenuEl); - } - favMenuEl.innerHTML = ""; - var favs = currentVisibleFavs(); - var vi = favs.indexOf(id); - addFavMenuItem("Move left", vi > 0, function () { moveFav(id, -1); }); - addFavMenuItem("Move right", vi >= 0 && vi < favs.length - 1, function () { moveFav(id, 1); }); - addFavMenuItem("Unpin", true, function () { toggleFav(id); }); - favMenuEl.hidden = false; - favMenuEl.style.left = Math.max(0, Math.min(x, window.innerWidth - favMenuEl.offsetWidth - 4)) + "px"; - favMenuEl.style.top = Math.max(0, Math.min(y, window.innerHeight - favMenuEl.offsetHeight - 4)) + "px"; + if (!favMenuEl) { + favMenuEl = document.createElement("div"); + favMenuEl.className = "ctx-menu"; + document.body.appendChild(favMenuEl); + } + favMenuEl.innerHTML = ""; + var favs = currentVisibleFavs(); + var vi = favs.indexOf(id); + addFavMenuItem("Move left", vi > 0, function () { moveFav(id, -1); }); + addFavMenuItem("Move right", vi >= 0 && vi < favs.length - 1, function () { moveFav(id, 1); }); + addFavMenuItem("Unpin", true, function () { toggleFav(id); }); + favMenuEl.hidden = false; + favMenuEl.style.left = Math.max(0, Math.min(x, window.innerWidth - favMenuEl.offsetWidth - 4)) + "px"; + favMenuEl.style.top = Math.max(0, Math.min(y, window.innerHeight - favMenuEl.offsetHeight - 4)) + "px"; } // Swap a favourite with its visible neighbour (dir -1/+1) and persist the new order. Swapping // by id inside FAVS (not the visible slice) keeps any hidden pins (no live action) in place. function moveFav(id, dir) { - var favs = currentVisibleFavs(); - var vi = favs.indexOf(id); - var ni = vi + dir; - if (vi === -1 || ni < 0 || ni >= favs.length) return; - var a = FAVS.indexOf(id), b = FAVS.indexOf(favs[ni]); - if (a === -1 || b === -1) return; - FAVS[a] = favs[ni]; FAVS[b] = id; - SendMessage({ command: "reorder_favourites", ids: FAVS.slice() }); - sel = { zone: "fav", i: ni }; - render({ resize: true }); + var favs = currentVisibleFavs(); + var vi = favs.indexOf(id); + var ni = vi + dir; + if (vi === -1 || ni < 0 || ni >= favs.length) return; + var a = FAVS.indexOf(id), b = FAVS.indexOf(favs[ni]); + if (a === -1 || b === -1) return; + FAVS[a] = favs[ni]; FAVS[b] = id; + SendMessage({ command: "reorder_favourites", ids: FAVS.slice() }); + sel = { zone: "fav", i: ni }; + render({ resize: true }); } // Name of the selected favourite, shown above the bar; hidden unless a fav is selected. function updateFavEyebrow(favs) { - if (!eyeEl) return; - var a = sel.zone === "fav" && favs.length ? byId(favs[sel.i]) : null; - eyeEl.textContent = a ? a.title : ""; - eyeEl.hidden = !a; + if (!eyeEl) return; + var a = sel.zone === "fav" && favs.length ? byId(favs[sel.i]) : null; + eyeEl.textContent = a ? a.title : ""; + eyeEl.hidden = !a; } -function renderList() { - var q = (query || "").trim(); - var arr = filterActions(ACTIONS, query); - if (sel.zone === "list") - sel.i = Math.max(0, Math.min(sel.i, arr.length - 1)); - listEl.innerHTML = ""; - // why: search-first - the list stays blank until the user types; only a - // non-empty query with zero hits earns the "No actions match" message. - if (!shouldRenderActionList(query)) { - listEl.className = "dial-list empty"; - if (countEl) countEl.hidden = true; - return; - } - listEl.className = "dial-list" + (!arr.length ? " empty" : ""); - if (!arr.length) { - if (countEl) countEl.hidden = true; - 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) { +// One settings row; has no star/tile because settings aren't pinnable. +function renderSettingRow(s, i) { + var row = document.createElement("div"); + row.className = "row" + (sel.zone === "list" && sel.i === i ? " sel" : ""); + row.setAttribute("aria-label", s.label); + + var tile = document.createElement("div"); + tile.className = "tile"; + tile.style.setProperty("--h", hue(s.opt_key)); + tile.textContent = settingCode(s, settingsResults); + + 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; +} + +// A command/action row (used for both recents and filtered results). +function renderActionRow(a, i) { var on = FAVS.indexOf(a.id) !== -1; var row = document.createElement("div"); row.className = "row" + (sel.zone === "list" && sel.i === i ? " sel" : ""); @@ -360,20 +473,20 @@ function renderList() { var left = document.createElement("div"); left.className = "row-left"; 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"); line.className = "row-line"; var name = markedText("row-name", a.title, mi ? mi.title : null); line.appendChild(name); if (a.shortcut) { - var sc = document.createElement("div"); - sc.className = "row-sc"; - a.shortcut.split("+").forEach(function (k) { - var key = document.createElement("kbd"); - key.textContent = k; - sc.appendChild(key); - }); - line.appendChild(sc); + var sc = document.createElement("div"); + sc.className = "row-sc"; + a.shortcut.split("+").forEach(function (k) { + var key = document.createElement("kbd"); + key.textContent = k; + sc.appendChild(key); + }); + line.appendChild(sc); } left.appendChild(sourceEl); left.appendChild(line); @@ -389,110 +502,350 @@ function renderList() { star.ondblclick = function (ev) { ev.stopPropagation(); }; row.appendChild(star); - row.onclick = function () { sel = { zone: "list", i: i }; render(); }; - row.ondblclick = function () { sel = { zone: "list", i: i }; run(a); }; - listEl.appendChild(row); - }); + row.onclick = function () { sel = { zone: "list", i: i }; render({ resize: true }); }; + row.ondblclick = function () { sel = { zone: "list", i: i }; activateEntry(a); }; + 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) { - renderFav(); - renderList(); - scrollSelectedIntoView(); - if (opts && opts.resetScroll) - resetScrollPositions(listEl, document); - if (opts && opts.resize) - requestResize(); + renderFav(); + renderList(); + scrollSelectedIntoView(); + if (opts && opts.resetScroll) + resetScrollPositions(listEl, document); + if (opts && opts.resize) + requestResize(); } // Keep the selected item in view as arrows move it: the list scrolls vertically, the fav bar // horizontally (arrow nav "pushes" the scrollable fav row to follow the selection). function scrollSelectedIntoView() { - var el = null; - if (sel.zone === "list" && listEl) - el = listEl.querySelector(".row.sel"); - else if (sel.zone === "fav" && favEl) - el = favEl.querySelector(".fav-tile.sel"); - if (el && el.scrollIntoView) - el.scrollIntoView({ block: "nearest", inline: "nearest" }); + var el = null; + if (sel.zone === "list" && listEl) + el = listEl.querySelector(".row.sel"); + else if (sel.zone === "fav" && favEl) + el = favEl.querySelector(".fav-tile.sel"); + if (el && el.scrollIntoView) + el.scrollIntoView({ block: "nearest", inline: "nearest" }); } function requestResize() { - if (!document.body) - return; - setTimeout(function () { - var launcher = document.querySelector(".launcher"); - if (!launcher) - return; - var height = Math.ceil(launcher.getBoundingClientRect().height); - if (!height || height === lastResizeHeight) - return; - lastResizeHeight = height; - SendMessage({ command: "resize", height: height }); - }, 0); + if (!document.body) + return; + setTimeout(function () { + var launcher = document.querySelector(".launcher"); + if (!launcher) + return; + var height = Math.ceil(launcher.getBoundingClientRect().height); + if (!height || height === lastResizeHeight) + return; + lastResizeHeight = height; + SendMessage({ command: "resize", height: height }); + }, 0); } // ---- actions ----------------------------------------------------------------- function toggleFav(id) { - var k = FAVS.indexOf(id); - var newState = k === -1; - if (newState) FAVS.push(id); else FAVS.splice(k, 1); - SendMessage({ command: "toggle_favourite", id: id, fav: newState }); - render({ resize: true }); + var k = FAVS.indexOf(id); + var newState = k === -1; + if (newState) FAVS.push(id); else FAVS.splice(k, 1); + SendMessage({ command: "toggle_favourite", id: id, fav: newState }); + 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) { - if (!a) return; - SendMessage({ command: "run_action", id: a.id, title: a.title }); + if (!a) return; + 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() { - var id = selectedActionId(sel, filterActions(ACTIONS, query), currentVisibleFavs(), query); - if (id) run(byId(id)); + if (phase === "percent") { + 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 -------------------------------------------------------------------- function OnInit() { - qEl = $("q"); listEl = $("list"); favEl = $("favBar"); clearEl = $("clear"); eyeEl = $("favEyebrow"); countEl = $("count"); - syncClearButton(); - - $("clear").onclick = function () { - query = ""; qEl.value = ""; sel = { zone: "list", i: 0 }; render({ resize: true, resetScroll: true }); qEl.focus(); + qEl = $("q"); listEl = $("list"); favEl = $("favBar"); clearEl = $("clear"); eyeEl = $("favEyebrow"); countEl = $("count"); syncClearButton(); - }; - qEl.addEventListener("input", function () { - query = qEl.value; sel = { zone: "list", i: 0 }; syncClearButton(); render({ resize: true, resetScroll: true }); - }); - // why: dismiss the fav context menu on any click/scroll away from it (capture scroll to catch nested scrollers). - document.addEventListener("click", hideFavMenu); - document.addEventListener("scroll", hideFavMenu, true); + $("clear").onclick = function () { + query = ""; qEl.value = ""; sel = { zone: "list", i: 0 }; + if (phase === "settings") SendMessage({ command: "search_settings", q: "" }); + render({ resize: true, resetScroll: true }); qEl.focus(); + syncClearButton(); + }; + qEl.addEventListener("input", function () { + query = qEl.value; sel = { zone: "list", i: 0 }; syncClearButton(); + if (phase === "settings") SendMessage({ command: "search_settings", q: query }); + render({ resize: true, resetScroll: true }); + }); - document.addEventListener("keydown", function (e) { - if (favMenuEl && !favMenuEl.hidden && e.key === "Escape") { e.preventDefault(); hideFavMenu(); return; } - var arr = filterActions(ACTIONS, query); - var favs = currentVisibleFavs(); - // 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. - var lr = e.key === "ArrowLeft" || e.key === "ArrowRight"; - if (e.key === "ArrowDown" || e.key === "ArrowUp" || (lr && sel.zone === "fav")) { - e.preventDefault(); - sel = nextSel(sel, e.key, arr.length, favs.length); - // 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. - render({ resize: true }); - } else if (e.key === "Enter") { - e.preventDefault(); - runSelected(); - } else if (e.key === "Escape") { - e.preventDefault(); - if (query) { query = ""; qEl.value = ""; sel = { zone: "list", i: 0 }; syncClearButton(); render({ resize: true, resetScroll: true }); } - else SendMessage({ command: "close_page" }); - } - }); + // why: dismiss the fav context menu on any click/scroll away from it (capture scroll to catch nested scrollers). + document.addEventListener("click", hideFavMenu); + document.addEventListener("scroll", hideFavMenu, true); - SendMessage({ command: "request_actions" }); + document.addEventListener("keydown", function (e) { + if (favMenuEl && !favMenuEl.hidden && e.key === "Escape") { e.preventDefault(); hideFavMenu(); return; } + var list = currentList(); + // 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, + // let Left/Right fall through so they move the caret in the focused search field. + var lr = e.key === "ArrowLeft" || e.key === "ArrowRight"; + 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(); + sel = nextSel(sel, e.key, list.length, favs.length); + // 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. + render({ resize: true }); + } else if (e.key === "Enter") { + e.preventDefault(); + if (phase === "percent") runJumpToLayer(query.trim()); + else runSelected(); + } else if (e.key === "Escape") { + e.preventDefault(); + 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" }); + } + }); + + SendMessage({ command: "request_actions" }); } diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 05c1e69c23..2ce625072b 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -22,20 +22,92 @@ assert.equal( "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, "whitespace-only 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 recent/empty list"); 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( - ctx.selectedActionId({ zone: "list", i: 0 }, duplicateActions, [], ""), + ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), [], ""), 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( - ctx.selectedActionId({ zone: "list", i: 0 }, duplicateActions, [], "rep"), + ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), [], "rep"), "0123456789abcdef", "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( ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"], ""), "fedcba9876543210", diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index 72de412f66..cdbeca2c69 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -55,6 +55,28 @@ body { border: 1px solid var(--speed-tile-border, #d8d8d8); } .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 { position: fixed; z-index: 10; @@ -127,6 +149,15 @@ body { color: var(--muted, var(--orca-muted, #6b7280)); } .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 { flex: 0 1 auto; min-height: 0; diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index f3df867ab7..3b9e568a96 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -1,15 +1,29 @@ #include "ActionRegistry.hpp" +#include "GCodeViewer.hpp" +#include "GLCanvas3D.hpp" #include "GUI.hpp" #include "GUI_App.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 +#include +#include #include #include +#include +#include +#include + #include #include #include @@ -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) -{ - 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) -{ - wxGetApp().app_config->set(kConfigSection, key, j.dump()); -} +void write_section(const char* key, const nlohmann::json& j) { wxGetApp().app_config->set(kConfigSection, key, j.dump()); } std::vector read_string_array(const char* key) { @@ -53,7 +62,7 @@ double frecency_score(int count, long long last, long long now) if (count <= 0) return 0.0; constexpr double HALF_LIFE_DAYS = 30.0; - double age = std::max(0.0, double(now - last) / 86400.0); + double age = std::max(0.0, double(now - last) / 86400.0); return count * std::pow(2.0, -age / HALF_LIFE_DAYS); } @@ -79,26 +88,25 @@ struct PluginScriptAction : AppAction // The id an action for (plugin_key, capability) would have - lets refresh_capability // remove a gone capability without materialising the action. 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) : AppAction(kIdPrefix, - capability_in.empty() ? plugin_key_in : capability_in, // title - plugin_key_in, // source_key - std::move(source_name)), - plugin_key(std::move(plugin_key_in)), capability(std::move(capability_in)) + capability_in.empty() ? plugin_key_in : capability_in, // title + plugin_key_in, // source_key + std::move(source_name)) + , 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; const ExecutionResult result = PluginManager::instance().run_script_capability(plugin_key, capability, error); if (!error.empty()) return {AppActionRunResult::Level::Error, from_u8(error)}; - const bool skipped = result.status == PluginResult::Skipped; + const bool skipped = result.status == PluginResult::Skipped; const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished."); return {skipped ? AppActionRunResult::Level::Info : AppActionRunResult::Level::Success, result.message.empty() ? fallback : from_u8(result.message)}; @@ -107,8 +115,7 @@ struct PluginScriptAction : AppAction // Builds an action for a capability, or nullptr if it is not a currently-loaded, // enabled script capability. -std::unique_ptr make_action(const std::string& plugin_key, const std::string& capability, - const std::string& source_name) +std::unique_ptr make_action(const std::string& plugin_key, const std::string& capability, const std::string& source_name) { PluginManager& manager = PluginManager::instance(); if (!manager.is_plugin_loaded(plugin_key)) @@ -119,8 +126,167 @@ std::unique_ptr make_action(const std::string& plugin_key, const std: return std::make_unique(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 make_command(std::string key, std::string title, std::string group, std::string input = "") +{ return std::make_unique(std::move(key), std::move(title), std::move(group), std::move(input)); } + +// The built-in palette commands, registered once at init(). +std::vector> native_commands() +{ + std::vector> 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 settings_inputs() +{ + std::vector 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 +ActionRegistry::~ActionRegistry() = default; + void ActionRegistry::init() { assert(wxThread::IsMain()); @@ -151,18 +317,12 @@ void ActionRegistry::init() // Subscribe before enumerating so a concurrent load cannot land between the initial // snapshot and callback registration. Duplicate notifications are safe: upsert is by // id and the m_actions scan in refresh_source is idempotent. - manager.subscribe_on_load_callback( - [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_load_callback([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_capability_load_callback( - [on_capability](const PluginCapabilityId& capability) { - on_capability(capability, ActionChange::Added); - }); + [on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Added); }); manager.subscribe_on_capability_unload_callback( - [on_capability](const PluginCapabilityId& capability) { - on_capability(capability, ActionChange::Removed); - }); + [on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Removed); }); // enumerate current script capabilities std::unordered_map source_names; @@ -173,12 +333,18 @@ void ActionRegistry::init() for (const auto& capability : manager.get_plugin_capabilities("", PluginCapabilityType::Script)) { if (!capability) continue; - const std::string& key = capability->audit_plugin_key(); - auto it = source_names.find(key); + const std::string& key = capability->audit_plugin_key(); + auto it = source_names.find(key); const std::string& source_name = it == source_names.end() ? key : it->second; if (auto action = make_action(key, capability->name(), source_name)) 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) @@ -198,7 +364,7 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange if (change == ActionChange::Removed) return; - PluginManager& manager = PluginManager::instance(); + PluginManager& manager = PluginManager::instance(); const std::string source_name = find_loaded_source_name(manager, plugin_key); for (const auto& capability : manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::Script)) { if (!capability) @@ -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, - ActionChange change) +void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change) { assert(wxThread::IsMain()); @@ -233,7 +398,7 @@ void ActionRegistry::upsert(std::unique_ptr action) return; seed_state(*action); - std::string id = action->id(); + std::string id = action->id(); std::shared_ptr stored = std::move(action); m_actions.insert_or_assign(std::move(id), std::move(stored)); } @@ -246,11 +411,11 @@ void ActionRegistry::remove(const std::string& id) void ActionRegistry::seed_state(AppAction& a) const { - auto favs = read_string_array("favourite_actions"); + auto favs = read_string_array("favourite_actions"); a.favourite = std::find(favs.begin(), favs.end(), a.id()) != favs.end(); nlohmann::json stats = read_section("stats", nlohmann::json::object()); - auto it = stats.find(a.id()); + auto it = stats.find(a.id()); if (it != stats.end() && it->is_object()) { a.count = it->value("count", 0); a.last = it->value("last", 0LL); @@ -269,14 +434,11 @@ const AppAction* ActionRegistry::by_id(const std::string& id) const return it == m_actions.end() ? nullptr : it->second.get(); } -AppAction* ActionRegistry::find(const std::string& id) -{ - return const_cast(by_id(id)); -} +AppAction* ActionRegistry::find(const std::string& id) { return const_cast(by_id(id)); } // ---- dispatch + write-through ---------------------------------------------- -AppActionRunResult ActionRegistry::run(const std::string& id) +AppActionRunResult ActionRegistry::run(const std::string& id, const std::string& param) { assert(wxThread::IsMain()); auto it = m_actions.find(id); @@ -286,13 +448,13 @@ AppActionRunResult ActionRegistry::run(const std::string& id) // nested event loop; a queued source refresh can erase the entry while the // keep-alive preserves the action until run returns. std::shared_ptr keep = it->second; - AppActionRunResult o = keep->run(); + AppActionRunResult o = keep->run(param); if (o.level == AppActionRunResult::Level::Busy) return o; // Bump stats (write-through). Re-read to avoid clobbering a concurrent field. nlohmann::json stats = read_section("stats", nlohmann::json::object()); - if (!stats.is_object()) // corrupt (valid-JSON, non-object) value degrades to empty + if (!stats.is_object()) // corrupt (valid-JSON, non-object) value degrades to empty stats = nlohmann::json::object(); nlohmann::json& e = stats[id]; if (!e.is_object()) @@ -300,7 +462,10 @@ AppActionRunResult ActionRegistry::run(const std::string& id) e["count"] = e.value("count", 0) + 1; e["last"] = (long long) std::time(nullptr); 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; } @@ -325,8 +490,7 @@ void ActionRegistry::reorder_favourites(const std::vector& ids) std::vector next; // keep the requested order, but only ids that are actually favourites (guard a bad payload) for (const auto& id : ids) - if (std::find(cur.begin(), cur.end(), id) != cur.end() && - std::find(next.begin(), next.end(), id) == next.end()) + if (std::find(cur.begin(), cur.end(), id) != cur.end() && std::find(next.begin(), next.end(), id) == next.end()) next.push_back(id); // why: don't drop favourites the page omitted (e.g. pins with no live action hidden from the bar) for (const auto& id : cur) @@ -374,17 +538,139 @@ nlohmann::json ActionRegistry::snapshot() const return a->id() < b->id(); }); + auto action_to_json = [](const AppAction* a) { + return nlohmann::json({{"id", a->id()}, + {"title", a->title()}, + {"source", a->source_name()}, + {"group", a->group}, + {"input", a->input}, + {"shortcut", ""}}); + }; + nlohmann::json actions = nlohmann::json::array(); for (const AppAction* a : sorted) - actions.push_back({{"id", a->id()}, - {"title", a->title()}, - {"source", a->source_name()}, - {"shortcut", ""}}); + actions.push_back(action_to_json(a)); + // 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 // reorder the favourites bar). The page (js) filters out ids with no live action itself. 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 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(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 + / 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 diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index c421297217..7b718b4c09 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -18,6 +18,9 @@ namespace Slic3r { namespace GUI { // How a source's action set changed. Drives the registry's refresh handlers. 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 // actions translate their runner-specific result into this generic shape. struct AppActionRunResult @@ -56,8 +59,18 @@ struct AppAction int count = 0; 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 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: // The definition is constructor-set and immutable. Refreshes replace an action @@ -92,6 +105,8 @@ private: class ActionRegistry { public: + ~ActionRegistry(); + // 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 // updates together. @@ -108,7 +123,7 @@ public: const AppAction* by_id(const std::string& id) const; // 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 reorder_favourites(const std::vector& ids); // persist a new bar order @@ -116,9 +131,33 @@ public: bool should_ask(const std::string& id) const; 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; + // "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..) 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: void seed_state(AppAction& a) const; // favourite/stats from config AppAction* find(const std::string& id); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 7db7e01e9c..f218a3f564 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -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_ALL, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent); -wxDEFINE_EVENT(EVT_GLCANVAS_OPEN_SPEED_DIAL, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event); wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent); @@ -3502,11 +3501,6 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) 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': { diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 84dbd5d652..4899a94e51 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -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_ALL, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent); -wxDECLARE_EVENT(EVT_GLCANVAS_OPEN_SPEED_DIAL, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event); // data: +1 => increase, -1 => decrease wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent); diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index ed513c425f..82e499ac55 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -197,6 +197,8 @@ void KBShortcutsDialog::fill_shortcuts() // Switch table page { ctrl + L("Tab"), L("Switch table page")}, + // Open speed dial + { ctrl + "K", L("Open speed dial") }, //DEL #ifdef __APPLE__ {"fn+⌫", L("Delete Selected")}, @@ -267,8 +269,6 @@ void KBShortcutsDialog::fill_shortcuts() { "O", L("Zoom out") }, { "V", L("Toggle printable for object/part") }, { 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 }); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 675bc8da27..2b4c8754c8 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -701,6 +701,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ } return;} #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.ShiftDown() && evt.GetKeyCode() == 'G') { m_plater->apply_background_progress(); @@ -3346,6 +3348,11 @@ void MainFrame::init_menubar_as_editor() wxGetApp().open_preferences(); }, "", 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); #endif // Help menu @@ -3370,7 +3377,13 @@ void MainFrame::init_menubar_as_editor() auto top_menu = m_topbar->GetTopMenu(); top_menu->AppendSeparator(); - append_menu_item( + 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( top_menu, wxID_ANY, _L("Preset Bundle") + "\t", "", [this](wxCommandEvent &) { // Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit @@ -3508,6 +3521,10 @@ void MainFrame::init_menubar_as_editor() #else // On Mac, the Apple menu ignores non-standard custom items, so add Preset Bundle to the File menu 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( fileMenu, wxID_ANY, _L("Preset Bundle"), "", [this](wxCommandEvent&) { diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index 67c1831e4d..5d10e230e9 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -10,6 +10,7 @@ #include "Widgets/Label.hpp" #include +#include #include 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 */) { - 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); if (bmp_name.empty() && bmp.IsOk()) 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); //BBS set size for button 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); Button* btn = m_pageButtons[n]; 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 diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 90206a438c..0537edbc6f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -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_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& evt) { 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(); }); diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index 09140b9868..8a0aca5445 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -9,20 +9,26 @@ #include "Plater.hpp" #include "Widgets/WebViewHostDialog.hpp" +#include + #include #include #include #include +#ifdef __linux__ +#include +#endif + namespace Slic3r { namespace GUI { namespace { // ADJUST WIDTH HERE (DIP px). Fixed dialog width; was 360, now 1.5x. Height is not set here - // the dialog auto-resizes to the page content (see resize_to_content + the list max-height in style.css). -constexpr int kPopupWidth = 540; -constexpr int kPopupMinHeight = 60; // just above the bare search-bar height, so the dialog hugs content +constexpr int kPopupWidth = 540; +constexpr int kPopupMinHeight = 60; // just above the bare search-bar height, so the dialog hugs content constexpr int kPopupMaxHeight = 282; int json_int_or(const nlohmann::json& j, const char* key, int fallback) @@ -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(); } +// 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) - : WebViewHostDialog(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, - wxBORDER_NONE | wxFRAME_NO_TASKBAR) + : WebViewHostDialog(parent, + wxID_ANY, + wxEmptyString, + wxDefaultPosition, + wxDefaultSize, + wxBORDER_NONE | wxFRAME_NO_TASKBAR | wxFRAME_FLOAT_ON_PARENT) { SetBackgroundColour(bg_color()); 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(); event.Skip(); }); - if (!create_webview("web/dialog/SpeedDial/index.html", wxEmptyString, - wxSize(kPopupWidth, kPopupMaxHeight), wxSize(kPopupWidth, kPopupMinHeight))) { + if (!create_webview("web/dialog/SpeedDial/index.html", wxEmptyString, wxSize(kPopupWidth, kPopupMaxHeight), + wxSize(kPopupWidth, kPopupMinHeight))) { auto* sizer = new wxBoxSizer(wxVERTICAL); - sizer->Add(new wxStaticText(this, wxID_ANY, wxS("wxWebView unavailable")), - wxSizerFlags().Border(wxALL, 20)); + sizer->Add(new wxStaticText(this, wxID_ANY, wxS("wxWebView unavailable")), wxSizerFlags().Border(wxALL, 20)); SetSizer(sizer); SetClientSize(FromDIP(wxSize(kPopupWidth, kPopupMinHeight))); } @@ -61,8 +93,7 @@ void SpeedDialWebDialog::request_show() { if (IsShown()) { Raise(); - if (browser()) - browser()->SetFocus(); + focus_webview(browser(), m_page_ready); return; } @@ -70,8 +101,9 @@ void SpeedDialWebDialog::request_show() Raise(); if (m_page_ready) send_actions(); - if (browser()) - browser()->SetFocus(); + // Grab focus now and again on wxEVT_ACTIVATE; grabbing directly on the WebKit widget is + // what makes typing reach the search field immediately on open. + focus_webview(browser(), m_page_ready); } 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") { m_page_ready = true; send_actions(); - } - else if (command == "toggle_favourite") + } else if (command == "toggle_favourite") wxGetApp().action_registry().set_favourite(payload.value("id", ""), payload.value("fav", false)); else if (command == "reorder_favourites") { std::vector ids; @@ -105,13 +136,61 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload) if (id.is_string()) ids.push_back(id.get()); wxGetApp().action_registry().reorder_favourites(ids); - } - else if (command == "run_action") - run_action(payload.value("id", ""), payload.value("title", "")); - else if (command == "resize") + } 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 == "search_settings") + search_settings(payload.value("q", "")); + 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)); } +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) { if (height <= 0) @@ -127,14 +206,15 @@ void SpeedDialWebDialog::resize_to_content(int height) 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(); - const AppAction* a = reg.by_id(id); + const AppAction* a = reg.by_id(id); if (!a) 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(); if (IsModal()) EndModal(wxID_CANCEL); @@ -143,8 +223,7 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti if (ask) { const wxString label = title.empty() ? from_u8(atitle) : from_u8(title); - RichMessageDialog dlg(wxGetApp().mainframe, wxString::Format(_L("Run \"%s\"?"), label), - _L("Run plugin"), wxOK | wxCANCEL); + RichMessageDialog dlg(wxGetApp().mainframe, wxString::Format(_L("Run \"%s\"?"), label), _L("Run plugin"), wxOK | wxCANCEL); dlg.ShowCheckBox(_L("Don't ask again for this action")); if (dlg.ShowModal() != wxID_OK) return; @@ -152,18 +231,21 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti wxGetApp().action_registry().suppress_ask(id); } - wxGetApp().CallAfter([id] { + wxGetApp().CallAfter([id, param] { if (wxGetApp().is_closing()) return; - AppActionRunResult result = wxGetApp().action_registry().run(id); + AppActionRunResult result = wxGetApp().action_registry().run(id, param); if (result.level == AppActionRunResult::Level::Busy) return; if (!result.message.IsEmpty() && wxGetApp().plater()) - wxGetApp().plater()->get_notification_manager()->push_notification( - NotificationType::CustomNotification, - result.level == AppActionRunResult::Level::Error ? NotificationManager::NotificationLevel::ErrorNotificationLevel : - NotificationManager::NotificationLevel::RegularNotificationLevel, - into_u8(result.message)); + wxGetApp() + .plater() + ->get_notification_manager() + ->push_notification(NotificationType::CustomNotification, + result.level == AppActionRunResult::Level::Error ? + NotificationManager::NotificationLevel::ErrorNotificationLevel : + NotificationManager::NotificationLevel::RegularNotificationLevel, + into_u8(result.message)); }); } @@ -172,7 +254,8 @@ void SpeedDialWebDialog::send_actions() nlohmann::json snap = wxGetApp().action_registry().snapshot(); call_web_handler({{"command", "list_actions"}, {"actions", std::move(snap["actions"])}, - {"favourites", std::move(snap["favourites"])}}); + {"favourites", std::move(snap["favourites"])}, + {"recent", std::move(snap["recent"])}}); } -}} +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/SpeedDialDialog.hpp b/src/slic3r/GUI/SpeedDialDialog.hpp index 52ee05e502..74e806dd85 100644 --- a/src/slic3r/GUI/SpeedDialDialog.hpp +++ b/src/slic3r/GUI/SpeedDialDialog.hpp @@ -20,8 +20,10 @@ private: void on_script_message(const nlohmann::json& payload) override; void handle_web_command(const nlohmann::json& payload); 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 search_settings(const std::string& query); + void search_tabs(); bool m_page_ready{false}; // Guards the CallAfter in on_script_message across dialog destruction, same as diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index 5a5bd89403..e25956e3d5 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -25,36 +25,29 @@ END_EVENT_TABLE() * calling Refresh()/Update(). */ -Button::Button() - : paddingSize(10, 8) +Button::Button() : paddingSize(10, 8) { - background_color = StateColor( - std::make_pair(0xF0F0F1, (int) StateColor::Disabled), - std::make_pair(0x52c7b8, (int) StateColor::Hovered | StateColor::Checked), - std::make_pair(0x009688, (int) StateColor::Checked), - std::make_pair(*wxLIGHT_GREY, (int) StateColor::Hovered), - std::make_pair(*wxWHITE, (int) StateColor::Normal)); - text_color = StateColor( - std::make_pair(*wxLIGHT_GREY, (int) StateColor::Disabled), - std::make_pair(*wxBLACK, (int) StateColor::Normal)); + background_color = StateColor(std::make_pair(0xF0F0F1, (int) StateColor::Disabled), + std::make_pair(0x52c7b8, (int) StateColor::Hovered | StateColor::Checked), + std::make_pair(0x009688, (int) StateColor::Checked), + std::make_pair(*wxLIGHT_GREY, (int) StateColor::Hovered), + std::make_pair(*wxWHITE, (int) StateColor::Normal)); + text_color = StateColor(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() -{ - Create(parent, text, icon, style, iconSize, btn_id); -} +Button::Button(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id) : Button() +{ Create(parent, text, icon, style, iconSize, btn_id); } bool Button::Create(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id) { StaticBox::Create(parent, btn_id, wxDefaultPosition, wxDefaultSize, style); state_handler.attach(std::vector{&text_color}); state_handler.update_binds(); - //BBS set default font + // BBS set default font SetFont(Label::Body_14); wxWindow::SetLabel(text); if (!icon.IsEmpty()) { - //BBS set button icon default size to 20 + // BBS set button icon default size to 20 this->active_icon = ScalableBitmap(this, icon.ToStdString(), iconSize > 0 ? iconSize : 20); } messureSize(); @@ -82,14 +75,12 @@ void Button::SetIcon(const wxString& icon) { auto tmpBitmap = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_cnt()); if (!icon.IsEmpty()) { - //BBS set button icon default size to 20 + // BBS set button icon default size to 20 if (!tmpBitmap.bmp().IsSameAs(this->active_icon.bmp())) { this->active_icon = tmpBitmap; Refresh(); } - } - else - { + } else { this->active_icon = ScalableBitmap(); Refresh(); } @@ -97,7 +88,7 @@ void Button::SetIcon(const wxString& icon) void Button::SetIcon(const wxBitmap& icon) { - this->active_icon = ScalableBitmap(); + this->active_icon = ScalableBitmap(); this->active_icon.bmp() = icon; messureSize(); Refresh(); @@ -121,6 +112,13 @@ void Button::SetPaddingSize(const wxSize& size) messureSize(); } +void Button::SetIconSpacing(int spacing) +{ + m_icon_spacing = spacing; + messureSize(); + Refresh(); +} + void Button::SetTextColor(StateColor const& color) { text_color = color; @@ -128,7 +126,7 @@ void Button::SetTextColor(StateColor const& color) Refresh(); } -void Button::SetTextColorNormal(wxColor const &color) +void Button::SetTextColorNormal(wxColor const& color) { text_color.setColorForStates(color, 0); Refresh(); @@ -145,22 +143,22 @@ bool Button::Enable(bool enable) return result; } -void Button::SetCanFocus(bool canFocus) { +void Button::SetCanFocus(bool canFocus) +{ StaticBox::SetCanFocus(canFocus); this->canFocus = canFocus; } void Button::SetValue(bool state) { - if (GetValue() == state) return; + if (GetValue() == state) + return; state_handler.set_state(state ? StateHandler::Checked : 0, StateHandler::Checked); } bool Button::GetValue() const { return state_handler.states() & StateHandler::Checked; } -void Button::SetCenter(bool isCenter) -{ - this->isCenter = isCenter; } +void Button::SetCenter(bool isCenter) { this->isCenter = isCenter; } void Button::SetVertical(bool vertical) { @@ -177,38 +175,33 @@ wxString btn_disabled[10] = {"#DFDFDF", "#DFDFDF", "#DFDFDF", "#DFDFDF", "#DFDFD void Button::SetStyle(const ButtonStyle style, const ButtonType type) { - if (type == ButtonType::Compact) { - this->SetPaddingSize(FromDIP(wxSize(8,3))); + if (type == ButtonType::Compact) { + this->SetPaddingSize(FromDIP(wxSize(8, 3))); this->SetCornerRadius(this->FromDIP(8)); this->SetFont(Label::Body_10); - } - else if (type == ButtonType::Window) { - this->SetSize(FromDIP(wxSize(58,24))); - this->SetMinSize(FromDIP(wxSize(58,24))); + } else if (type == ButtonType::Window) { + this->SetSize(FromDIP(wxSize(58, 24))); + this->SetMinSize(FromDIP(wxSize(58, 24))); this->SetCornerRadius(this->FromDIP(12)); this->SetFont(Label::Body_12); - } - else if (type == ButtonType::Choice) { - this->SetMinSize(FromDIP(wxSize(100,32))); - this->SetPaddingSize(FromDIP(wxSize(12,8))); + } else if (type == ButtonType::Choice) { + this->SetMinSize(FromDIP(wxSize(100, 32))); + this->SetPaddingSize(FromDIP(wxSize(12, 8))); this->SetCornerRadius(this->FromDIP(4)); this->SetFont(Label::Body_14); - } - else if (type == ButtonType::Parameter) { - this->SetMinSize(FromDIP(wxSize(120,26))); - this->SetSize(FromDIP(wxSize(120,26))); + } else if (type == ButtonType::Parameter) { + this->SetMinSize(FromDIP(wxSize(120, 26))); + this->SetSize(FromDIP(wxSize(120, 26))); this->SetCornerRadius(this->FromDIP(4)); this->SetFont(Label::Body_14); - } - else if (type == ButtonType::Icon) { - this->SetPaddingSize(FromDIP(wxSize(5,5))); - this->SetMinSize(FromDIP(wxSize(26,26))); - this->SetSize(FromDIP(wxSize(26,26))); + } else if (type == ButtonType::Icon) { + this->SetPaddingSize(FromDIP(wxSize(5, 5))); + this->SetMinSize(FromDIP(wxSize(26, 26))); + this->SetSize(FromDIP(wxSize(26, 26))); this->SetCornerRadius(this->FromDIP(4)); - } - else if (type == ButtonType::Expanded) { - this->SetMinSize(FromDIP(wxSize(-1,32))); - this->SetPaddingSize(FromDIP(wxSize(12,8))); + } else if (type == ButtonType::Expanded) { + this->SetMinSize(FromDIP(wxSize(-1, 32))); + this->SetPaddingSize(FromDIP(wxSize(12, 8))); this->SetCornerRadius(this->FromDIP(4)); this->SetFont(Label::Body_14); } @@ -217,39 +210,33 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type) bool is_dark = StateColor::darkModeColorFor("#FFFFFF") != wxColour("#FFFFFF"); - auto clr_arr = style == ButtonStyle::Regular ? btn_regular : - style == ButtonStyle::Confirm ? btn_confirm : - style == ButtonStyle::Alert ? btn_alert : + auto clr_arr = style == ButtonStyle::Regular ? btn_regular : + style == ButtonStyle::Confirm ? btn_confirm : + style == ButtonStyle::Alert ? btn_alert : style == ButtonStyle::Disabled ? btn_disabled : - btn_regular ; + btn_regular; - auto bg_color = StateColor( - std::pair(wxColour(clr_arr[0]), (int)StateColor::Disabled), - std::pair(wxColour(clr_arr[1]), (int)StateColor::Pressed), - std::pair(wxColour(clr_arr[2]), (int)StateColor::Hovered), - std::pair(wxColour(clr_arr[3]), (int)StateColor::Normal), - std::pair(wxColour(clr_arr[4]), (int)StateColor::Enabled) - ); + auto bg_color = StateColor(std::pair(wxColour(clr_arr[0]), (int) StateColor::Disabled), + std::pair(wxColour(clr_arr[1]), (int) StateColor::Pressed), + std::pair(wxColour(clr_arr[2]), (int) StateColor::Hovered), + std::pair(wxColour(clr_arr[3]), (int) StateColor::Normal), + std::pair(wxColour(clr_arr[4]), (int) StateColor::Enabled)); bg_color.setTakeFocusedAsHovered(false); this->SetBackgroundColor(bg_color); wxColour focus_clr = clr_arr[is_dark ? 8 : 9]; - auto border_color = StateColor( - std::pair(wxColour(clr_arr[0]), (int)StateColor::Disabled), - std::pair(wxColour(clr_arr[2]), (int)(StateColor::Hovered | ~StateColor::Focused)), - std::pair(wxColour(focus_clr ), (int)StateColor::Focused), - std::pair(wxColour(clr_arr[3]), (int)StateColor::Normal) - ); + auto border_color = StateColor(std::pair(wxColour(clr_arr[0]), (int) StateColor::Disabled), + std::pair(wxColour(clr_arr[2]), (int) (StateColor::Hovered | ~StateColor::Focused)), + std::pair(wxColour(focus_clr), (int) StateColor::Focused), + std::pair(wxColour(clr_arr[3]), (int) StateColor::Normal)); border_color.setTakeFocusedAsHovered(false); this->SetBorderColor(border_color); - this->SetTextColor(StateColor( - std::pair(wxColour(clr_arr[5]), (int)StateColor::Disabled), - std::pair(wxColour(clr_arr[7]), (int)StateColor::Hovered), - std::pair(wxColour(clr_arr[6]), (int)StateColor::Normal) - )); + this->SetTextColor(StateColor(std::pair(wxColour(clr_arr[5]), (int) StateColor::Disabled), + std::pair(wxColour(clr_arr[7]), (int) StateColor::Hovered), + std::pair(wxColour(clr_arr[6]), (int) StateColor::Normal))); m_has_style = true; - m_style = style; - m_type = type; + m_style = style; + m_type = type; } void Button::Rescale() @@ -260,7 +247,7 @@ void Button::Rescale() messureSize(); - if(m_has_style) + if (m_has_style) SetStyle(m_style, m_type); Refresh(); @@ -281,7 +268,7 @@ void Button::paintEvent(wxPaintEvent& evt) void Button::render(wxDC& dc) { StaticBox::render(dc); - int states = state_handler.states(); + int states = state_handler.states(); wxSize size = GetSize(); dc.SetBrush(*wxTRANSPARENT_BRUSH); // calc content size @@ -289,22 +276,22 @@ void Button::render(wxDC& dc) wxSize textSize = this->textSize.GetSize(); const ScalableBitmap& icon = active_icon; - wxSize padding = this->paddingSize; - int spacing = 5; + wxSize padding = this->paddingSize; + int spacing = m_icon_spacing; // Wrap text auto text = GetLabel(); if (vertical && textSize.x + padding.x * 2 > size.x) { Label::split_lines(dc, size.x - padding.x * 2, text, text, 2); textSize = dc.GetMultiLineTextExtent(text); if (padding.x * 2 + textSize.x > size.x) { - text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, size.x - padding.x * 2); + text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, size.x - padding.x * 2); textSize = dc.GetMultiLineTextExtent(text); } } auto szContent = textSize; if (icon.bmp().IsOk()) { if (szContent.y > 0) { - //BBS norrow size between text and icon + // BBS norrow size between text and icon if (vertical) szContent.y += spacing; else @@ -313,10 +300,12 @@ void Button::render(wxDC& dc) szIcon = icon.GetBmpSize(); if (vertical) { szContent.y += szIcon.y; - if (szIcon.x > szContent.x) szContent.x = szIcon.x; + if (szIcon.x > szContent.x) + szContent.x = szIcon.x; } else { 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) { int d = std::min(padding.x, (szContent.x - size.x) / 2); @@ -325,10 +314,11 @@ void Button::render(wxDC& dc) } } // move to center - wxRect rcContent = { {0, 0}, size }; + wxRect rcContent = {{0, 0}, size}; if (isCenter) { 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); } // start draw @@ -339,7 +329,7 @@ void Button::render(wxDC& dc) else pt.y += (rcContent.height - szIcon.y) / 2; dc.DrawBitmap(icon.bmp(), pt); - //BBS norrow size between text and icon + // BBS norrow size between text and icon if (vertical) { pt.y += szIcon.y + spacing; pt.x = rcContent.x; @@ -373,19 +363,21 @@ void Button::messureSize() wxSize szContent = textSize.GetSize(); if (this->active_icon.bmp().IsOk()) { if (szContent.y > 0) { - //BBS norrow size between text and icon + // BBS narrow size between text and icon if (vertical) - szContent.y += 5; + szContent.y += m_icon_spacing; else - szContent.x += 5; + szContent.x += m_icon_spacing; } wxSize szIcon = this->active_icon.GetBmpSize(); if (vertical) { szContent.y += szIcon.y; - if (szIcon.x > szContent.x) szContent.x = szIcon.x; + if (szIcon.x > szContent.x) + szContent.x = szIcon.x; } else { 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; @@ -429,13 +421,13 @@ void Button::mouseReleased(wxMouseEvent& event) } } -void Button::mouseCaptureLost(wxMouseCaptureLostEvent &event) +void Button::mouseCaptureLost(wxMouseCaptureLostEvent& event) { wxMouseEvent evt; mouseReleased(evt); } -void Button::keyDownUp(wxKeyEvent &event) +void Button::keyDownUp(wxKeyEvent& event) { if (event.GetKeyCode() == WXK_SPACE || event.GetKeyCode() == WXK_RETURN) { wxMouseEvent evt(event.GetEventType() == wxEVT_KEY_UP ? wxEVT_LEFT_UP : wxEVT_LEFT_DOWN); @@ -444,8 +436,8 @@ void Button::keyDownUp(wxKeyEvent &event) return; } if (event.GetEventType() == wxEVT_KEY_DOWN && - (event.GetKeyCode() == WXK_TAB || event.GetKeyCode() == WXK_LEFT || event.GetKeyCode() == WXK_RIGHT - || event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_DOWN)) + (event.GetKeyCode() == WXK_TAB || event.GetKeyCode() == WXK_LEFT || event.GetKeyCode() == WXK_RIGHT || + event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_DOWN)) HandleAsNavigationKey(event); else event.Skip(); @@ -462,7 +454,9 @@ void Button::sendButtonEvent() 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) { wxKeyEvent event(CreateKeyEvent(wxEVT_KEY_DOWN, wParam, lParam)); switch (wParam) { @@ -483,8 +477,7 @@ void Button::EnableTooltipEvenDisabled() { #if defined(_MSC_VER) || defined(_WIN32) auto parent = this->GetParent(); - if (parent) - { + if (parent) { parent->Bind(wxEVT_MOTION, &Button::OnParentMotion, this); parent->Bind(wxEVT_LEAVE_WINDOW, &Button::OnParentLeave, this); }; @@ -494,22 +487,21 @@ void Button::EnableTooltipEvenDisabled() void Button::OnParentMotion(wxMouseEvent& event) { 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(); - wxString tip = this->GetToolTipText(); - if (!tip.IsEmpty() && !this->IsEnabled() && screen_rect.Contains(pos)) - { - if (!tipWindow) - { + wxString tip = this->GetToolTipText(); + if (!tip.IsEmpty() && !this->IsEnabled() && screen_rect.Contains(pos)) { + if (!tipWindow) { tipWindow = wxTipWindow::New(this, tip); - if (!tipWindow) return event.Skip(); + if (!tipWindow) + return event.Skip(); tipWindow->Enable(false); } - if (tipWindow->GetLabel() != tip) - { + if (tipWindow->GetLabel() != tip) { tipWindow->SetLabel(tip); } @@ -517,11 +509,8 @@ void Button::OnParentMotion(wxMouseEvent& event) // using wxGetMousePosition() which returns (0,0) on Wayland. tipWindow->Position(this->ClientToScreen(wxPoint(0, 0)), this->GetSize()); tipWindow->Popup(); - } - else - { - if (tipWindow) - { + } else { + if (tipWindow) { tipWindow->Dismiss(); tipWindow->Destroy(); tipWindow = nullptr; @@ -534,15 +523,14 @@ void Button::OnParentMotion(wxMouseEvent& event) void Button::OnParentLeave(wxMouseEvent& event) { auto parent = this->GetParent(); - if (!parent) return event.Skip(); + if (!parent) + return event.Skip(); - if (tipWindow) - { - wxPoint pos = parent->ClientToScreen(event.GetPosition()); + if (tipWindow) { + wxPoint pos = parent->ClientToScreen(event.GetPosition()); wxRect screen_rect = this->GetScreenRect(); - wxString tip = this->GetToolTipText(); - if (!screen_rect.Contains(pos)) - { + wxString tip = this->GetToolTipText(); + if (!screen_rect.Contains(pos)) { tipWindow->Dismiss(); tipWindow->Destroy(); tipWindow = nullptr; diff --git a/src/slic3r/GUI/Widgets/Button.hpp b/src/slic3r/GUI/Widgets/Button.hpp index 2991edd425..a1a05e9831 100644 --- a/src/slic3r/GUI/Widgets/Button.hpp +++ b/src/slic3r/GUI/Widgets/Button.hpp @@ -8,24 +8,25 @@ class ButtonProps { public: - static int ChoiceButtonGap(){return 10;}; - static int WindowButtonGap(){return 10;}; + static int ChoiceButtonGap() { return 10; }; + static int WindowButtonGap() { return 10; }; }; -enum class ButtonStyle{ +enum class ButtonStyle { Regular, Confirm, Alert, Disabled, }; -enum class ButtonType{ - Compact , // Font10 FullyRounded For spaces with less areas - Window , // Font12 FullyRounded For regular buttons in windows and not related with parameter boxes - Choice , // Font14 Semi-Rounded For dialog/window choice buttons +enum class ButtonType { + Compact, // Font10 FullyRounded For spaces with less areas + Window, // Font12 FullyRounded For regular buttons in windows and not related with parameter boxes + Choice, // Font14 Semi-Rounded For dialog/window choice buttons 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 - Expanded , // Font14 Semi-Rounded For full length buttons. ex. buttons in static box + 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 }; class Button : public StaticBox @@ -34,17 +35,18 @@ class Button : public StaticBox wxRect textSize; wxSize minSize; // set by outer wxSize paddingSize; + int m_icon_spacing = 5; ScalableBitmap active_icon; - StateColor text_color; + StateColor text_color; bool pressedDown = false; bool m_selected = true; - bool canFocus = true; + bool canFocus = true; bool isCenter = true; bool vertical = false; - static const int buttonWidth = 200; + static const int buttonWidth = 200; static const int buttonHeight = 50; public: @@ -66,21 +68,23 @@ public: void SetPaddingSize(const wxSize& size); + void SetIconSpacing(int spacing); + void SetStyle(const ButtonStyle style /*= ButtonStyle::Regular*/, const ButtonType type /*= ButtonType::None*/); - void SetTextColor(StateColor const &color); + void SetTextColor(StateColor const& color); - void SetTextColorNormal(wxColor const &color); + void SetTextColorNormal(wxColor const& color); void SetSelected(bool selected = true) { m_selected = selected; } // Only meant to be used by inspector, not public API ButtonStyle GetStyle() const { return m_style; } - ButtonType GetType() const { return m_type; } - bool IsSelected() const { return m_selected; } + ButtonType GetType() const { return m_type; } + bool IsSelected() const { return m_selected; } bool Enable(bool enable = true) override; - void EnableTooltipEvenDisabled();// The tip will be shown even if the button is disabled + void EnableTooltipEvenDisabled(); // The tip will be shown even if the button is disabled void SetCanFocus(bool canFocus) override; @@ -104,7 +108,7 @@ protected: private: bool m_has_style = false; ButtonStyle m_style; - ButtonType m_type; + ButtonType m_type; void paintEvent(wxPaintEvent& evt); @@ -115,10 +119,10 @@ private: // some useful events void mouseDown(wxMouseEvent& event); void mouseReleased(wxMouseEvent& event); - void mouseCaptureLost(wxMouseCaptureLostEvent &event); - void keyDownUp(wxKeyEvent &event); + void mouseCaptureLost(wxMouseCaptureLostEvent& event); + void keyDownUp(wxKeyEvent& event); - // + // void sendButtonEvent(); // parent motion From 2dce0ad24f85d1d91709b0e0a85620154cc3523c Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 8 Sep 2026 16:46:56 +0800 Subject: [PATCH 02/29] Back to spacebar for speed dial shortcut. Integrated inline process settings editing. Optimized saerch results --- resources/web/dialog/SpeedDial/speeddial.js | 941 ++++++++++++++---- .../web/dialog/SpeedDial/speeddial.test.js | 162 ++- resources/web/dialog/SpeedDial/style.css | 182 ++++ resources/web/js/fuzzy-search.js | 39 + src/slic3r/GUI/ActionRegistry.cpp | 687 +++++++++++-- src/slic3r/GUI/ActionRegistry.hpp | 66 +- src/slic3r/GUI/KBShortcutsDialog.cpp | 3 +- src/slic3r/GUI/MainFrame.cpp | 21 +- src/slic3r/GUI/ParamsPanel.cpp | 4 +- src/slic3r/GUI/Search.hpp | 4 + src/slic3r/GUI/SpeedDialDialog.cpp | 61 +- src/slic3r/GUI/SpeedDialDialog.hpp | 1 - 12 files changed, 1833 insertions(+), 338 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index b25df97723..65d2e24430 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -10,40 +10,118 @@ var sel = { zone: "list", i: 0 }; // zone: 'list' | 'fav' var lastResizeHeight = 0; 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). +// ---- windowed list render ---------------------------------------------------- +// The command list is rendered in windows (append-on-scroll) so a huge settings pool doesn't build +// the whole DOM per keystroke. Rows are exactly ROW_H tall (matches .row min-height 44px; see --row-h, +// which is documented to stay in sync). `renderEnd` is the exclusive count of rows currently in the DOM; +// a bottom spacer fills the rest of the list so the scrollbar reflects the full match count and +// "scroll past the last rendered row" reveals the next window. +var K_ROWS = 50; +var ROW_H = 44; +var renderEnd = 0; +var builtKey = ""; // phase|query|listLen - when it changes, rows are rebuilt from the first window +var spacerEl = null; // the trailing height spacer, always the last child of listEl + +// search-cache: the normalized (folded+lowercased) needle for the current query pass. +var searchNeedle = ""; + +// Palette phase: 'commands' (one unified search over actions/commands/settings, recents on empty +// query), '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 // this script - it is shared with the Plugins dialog. Speed dial search is always case-insensitive. // element handles, assigned in OnInit (kept null so load-time touches no DOM) -var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, countEl = null; +var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, countEl = null, headEl = null; + +// ---- inline setting editor state --------------------------------------------- +// The "setting" phase (opened by activating a setting action) replaces the list with an editor card +// for one option. phase transitions: commands -> setting -> (apply / open-in-sidebar) -> closed, or +// Esc back to commands. settingDesc is the C++ descriptor; settingRows are the per-index control +// descriptors (1 row for scalars, one per index for vectors); settingFieldEls hold the live controls. +var settingId = ""; // the setting action id being edited +var settingDesc = null; // {id,opt_key,type,title,breadcrumb,category,unit,tooltip,editable,control,cardinality,value|values,index_labels,enum_options,min,max,is_int} +var settingRows = []; // [{index,kind,value,label,enum_options,min,max,unit,is_int}] +var settingFieldEls = []; // [Element...] parallel to settingRows +var settingPreviewIcon = null; // beside the editor title, updated live on dropdown pick +var openDropDownEl = null; // the custom dropdown toggle button whose option list is expanded // ---- pure helpers (no DOM; unit-tested) ------------------------------------- -function filterActions(actions, query) { +// Pre-normalized haystacks, cached on the action object. The fold is length-preserving (1:1 per +// char) so the ranges FuzzyRangesNorm returns slice the ORIGINAL title/source text correctly. The +// action objects arrive from C++ and are stable for the dialog's lifetime, so we compute these once. +function titleNorm(a) { + if (a._tn === undefined) + a._tn = NormText(a.title, false); + return a._tn; +} +function otherNorm(a) { + if (a._on === undefined) + a._on = NormText((a.source || "") + " " + (a.group || ""), false); + return a._on; +} + +// Relevance score for a single field vs the current query needle, or -1 when there's no match. +// Higher is better: an earlier start and a more contiguous (fewer gaps) match beat a scattered late one. +function matchScoreNorm(haystackNorm) { + if (!searchNeedle) return -1; + var r = FuzzyRangesNorm(haystackNorm || "", searchNeedle); + if (!r) return -1; + var gaps = 0; + for (var i = 1; i < r.length; i++) + gaps += r[i][0] - r[i - 1][1]; + return 1000 - r[0][0] * 10 - gaps * 10; +} + +// Per-action score: title matches rank above a source/group-only match of equal quality. +function actionSearchScore(a) { + var title = matchScoreNorm(titleNorm(a)); + var other = matchScoreNorm(otherNorm(a)); + if (title < 0 && other < 0) return -1; + return Math.max(title < 0 ? -1e9 : title + 10000, other < 0 ? -1e9 : other); +} + +// The unified main-phase search: every action (command/plugin/setting) matching the query, ranked +// by relevance (not by action type). Sets matchIndex so rows highlight their match ranges. The query +// is normalized ONCE per pass - FuzzyRangesNorm then runs against each action's pre-normalized +// haystack, so per-keystroke cost is a cheap scan (no per-char normalize/regex). +function searchActions(actions, query) { var q = (query || "").trim(); var list = actions || []; matchIndex = {}; - if (!q) - return list.slice(0); + if (!q) { searchNeedle = ""; return list.slice(0); } + searchNeedle = NormText(q, false); - var out = []; + var scored = []; for (var i = 0; i < list.length; i++) { var a = list[i]; - var titleMatch = FuzzyRanges(a.title, q, false); - var sourceMatch = FuzzyRanges(a.source || "", q, false); - if (!titleMatch && !sourceMatch) - continue; - matchIndex[a.id] = { title: titleMatch, source: sourceMatch, useTitle: !!titleMatch }; - out.push(a); + var s = actionSearchScore(a); + if (s < 0) continue; + var titleMatch = FuzzyRangesNorm(titleNorm(a), searchNeedle); + matchIndex[a.id] = { title: titleMatch, source: FuzzyRangesNorm(otherNorm(a), searchNeedle), useTitle: !!titleMatch }; + scored.push({ a: a, s: s }); } - return out; + scored.sort(function (x, y) { + if (x.s !== y.s) return y.s - x.s; + if (x.a.title !== y.a.title) return x.a.title < y.a.title ? -1 : 1; + return x.a.id < y.a.id ? -1 : x.a.id > y.a.id ? 1 : 0; + }); + return scored.map(function (e) { return e.a; }); } +// Pure: how many rows must be materialized to cover the given starting index plus `size` more. +// Clamped to the total; used to decide "render the next window" on scroll / arrow-nav. +function revealTarget(total, fromIndex, size) { + return Math.min(total, Math.max(0, fromIndex) + size); +} + +// buildKey: the command-list signature that decides whether rows must be rebuilt (new search / phase) +// or just have their selection refreshed in place (arrow-nav / click). Cheap to compute. +function buildKey() { return phase + "|" + (query || "").trim(); } + function visibleFavourites(favourites, actions) { // why: a fav whose id has no live action (plugin unloaded/disabled) renders a dead // monogram tile whose click run()s to a silent no-op; drop it from the quick-bar. @@ -54,6 +132,24 @@ function visibleFavourites(favourites, actions) { }); } +// Numbered quick-launch slots (mirrors ActionRegistry::kFavLimit). Pure so the node-vm test +// can exercise the digit<->slot mapping without a DOM. +var K_FAV_LIMIT = 10; + +// Badge label for a 0-based fav-bar index: 0..8 -> "1".."9", index 9 (the 10th) -> "0". +function favSlotForIndex(i) { + if (i < 0 || i >= K_FAV_LIMIT) return null; + return i < 9 ? String(i + 1) : "0"; +} + +// Digit key -> 0-based fav-bar index (1..9 -> 0..8, 0 -> 9); -1 for anything else. +function favIndexForDigit(d) { + var c = String(d || "").charCodeAt(0); + if (c >= 49 && c <= 57) return c - 49; + if (c === 48) return 9; + return -1; +} + function resultCountText(total, shown, query) { return (query || "").trim() ? "Showing " + shown + " of " + total + " actions" : total + " actions"; } @@ -80,11 +176,125 @@ function shouldRenderActionList(query) { 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). +// Label for a closed-enum entry, looked up from its enum_options by value; falls back to the value. +// Pure so the node-vm test can exercise the dropdown label mapping. `value` is the current int value. +function dropdownLabel(options, value) { + var want = String(value == null ? "" : value); + for (var i = 0; i < (options || []).length; i++) + if (String(options[i].value) === want) + return options[i].label != null && options[i].label !== "" ? String(options[i].label) : String(options[i].key != null ? options[i].key : options[i].value); + return want; +} + +// Pure: the data:URI pictogram for a dropdown's selected value, or "" when none of the options has +// one (most settings have no pattern icon). Mirrors dropdownLabel so tests can drive it without DOM. +function dropdownIcon(options, value) { + var want = String(value == null ? "" : value); + for (var i = 0; i < (options || []).length; i++) + if (String(options[i].value) === want && options[i].icon) + return options[i].icon; + return ""; +} + +// Put an action's pattern pictogram into a tile (search row or favourites tile) when it has one, +// otherwise fall back to the monogram. Toggles the has-icon class so CSS neutralises the hue. +function fillTile(tile, a) { + tile.classList.remove("has-icon"); + if (a && a.icon) { + tile.textContent = ""; + var img = document.createElement("img"); + img.className = "tile-icon"; + img.src = a.icon; + img.alt = ""; + tile.appendChild(img); + tile.classList.add("has-icon"); + } else { + tile.textContent = a ? tileCode(a, ACTIONS) : ""; + } +} + +// Per-index control descriptors for the inline setting editor, derived from the C++ descriptor. +// Pure so the node-vm test can exercise the scalar/vector + control mapping without a DOM. +// Values are the current config value(s); vector options get one row per index, each labelled. +function settingControlRows(desc) { + if (!desc || !desc.editable) return []; + var rows = []; + var values = desc.cardinality === "vector" ? (desc.values || []) : [desc.value]; + var labels = desc.cardinality === "vector" ? (desc.index_labels || []) : []; + for (var i = 0; i < values.length; i++) { + rows.push({ + index: i, + kind: desc.control, + value: values[i], + label: labels[i] != null ? String(labels[i]) : (desc.cardinality === "vector" ? String(i + 1) : null), + enum_options: desc.enum_options || [], + min: typeof desc.min === "number" ? desc.min : null, + max: typeof desc.max === "number" ? desc.max : null, + unit: desc.unit || "", + is_int: !!desc.is_int + }); + } + return rows; +} + +// Pure: read the value a control would submit back for a setting. `el` is a DOM element (never +// passed in tests). Returns undefined for an unusable value (empty/invalid number, out of range), +// boolean for toggles, number for numeric, string otherwise. +function settingControlValue(row, el) { + if (!row || !el) return undefined; + switch (row.kind) { + case "toggle": return !!el.checked; + case "number": { + var raw = String(el.value || "").trim(); + if (raw === "") return undefined; + var n = row.is_int ? parseInt(raw, 10) : parseFloat(raw); + if (!isFinite(n)) return undefined; + if (row.min != null && n < row.min) return undefined; + if (row.max != null && n > row.max) return undefined; + return n; + } + case "dropdown": { + // value is stored on the toggle button's dataset (set when an option is picked). + var v = parseInt(el.dataset ? el.dataset.value : "", 10); + return isFinite(v) ? v : undefined; + } + case "combo": { + // Open enum: free text field (never a select), so read it as the seeded integer value. + var v = parseInt(el.value, 10); + return isFinite(v) ? v : undefined; + } + case "color": + case "text": + case "percent": { + // percent submission is a string ("10%", "0.5"); C++ parses + clamps it. Empty is invalid. + var raw = String(el.value || "").trim(); + return raw === "" ? undefined : raw; + } + default: return undefined; + } +} + +// Pure: assemble the value payload for a setting from its edited control rows. Returns the scalar +// for scalar settings, an array for vector settings, or undefined when any control is invalid. +function settingCollectedValue(desc, rows, values) { + if (!desc || !desc.editable) return undefined; + if (desc.cardinality === "vector") { + var out = []; + for (var i = 0; i < rows.length; i++) { + var v = settingControlValue(rows[i], values[i]); + if (v === undefined) return undefined; + out.push(v); + } + return out; + } + return settingControlValue(rows[0], values[0]); +} + +// The active list for the main phase. A typed query ranks every action (commands/plugins/settings) +// by relevance; an empty query shows the recent list (recents are a mixed bag - no discrimination). function commandList(actions, recents, query) { if (shouldRenderActionList(query)) - return filterActions(actions || [], query); + return searchActions(actions || [], query); return (recents || []).slice(0); } @@ -146,8 +356,8 @@ function monogramFor(item, list, titleOf, sourceOf, idOf) { 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. +// Action tile code - see monogramFor for the escalation ladder. Settings are actions now, so +// they share this ladder (title initial, then source, then a stable ordinal). function tileCode(action, actions) { return monogramFor(action, actions, function (o) { return o.title; }, @@ -155,24 +365,6 @@ function tileCode(action, actions) { 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() { if (clearEl) clearEl.hidden = !query; @@ -187,7 +379,6 @@ function stateFromPayload(payload) { sel: { zone: "list", i: 0 }, lastResizeHeight: 0, phase: "commands", - settingsResults: [], tabOptions: [] }; } @@ -203,17 +394,19 @@ function resetScrollPositions(list, doc) { doc.body.scrollTop = 0; } -// nextSel: pure arrow-nav transition. Down fav->list0; Down list->clamp; Up list@0->fav0; -// Up list->i-1; Left/Right clamp within fav. Returns a fresh {zone,i}. +// nextSel: pure arrow-nav transition. Down fav->list0; Down list wraps at the bottom (last -> first). +// Up list wraps at the top (first -> last) only when there's no fav bar above; with a fav bar, Up at +// the list top goes to fav0 (unchanged). Left/Right clamp within fav. Returns a fresh {zone,i}. function nextSel(sel, key, listLen, favLen) { var zone = sel.zone, i = sel.i; + var last = Math.max(0, listLen - 1); if (key === "ArrowDown") { if (zone === "fav") return { zone: "list", i: 0 }; - return { zone: "list", i: Math.min(i + 1, Math.max(0, listLen - 1)) }; + return { zone: "list", i: i >= last ? 0 : i + 1 }; } if (key === "ArrowUp") { if (zone === "list") { - if (i <= 0) return favLen ? { zone: "fav", i: 0 } : { zone: "list", i: 0 }; + if (i <= 0) return favLen ? { zone: "fav", i: 0 } : { zone: "list", i: last }; return { zone: "list", i: i - 1 }; } return { zone: zone, i: i }; @@ -245,8 +438,16 @@ window.HandleStudio = function (payload) { sel = next.sel; lastResizeHeight = next.lastResizeHeight; phase = next.phase; - settingsResults = next.settingsResults; tabOptions = next.tabOptions; + // Reset any half-open setting editor (the dialog was closed/reopened), restoring the search. + settingId = ""; settingDesc = null; settingRows = []; settingFieldEls = []; + settingPreviewIcon = null; + openDropDownEl = null; + // why: builtKey caches phase|query so renderCommandsList can skip a rebuild on arrow-nav. It + // survives an apply-then-reopen (which never goes through exitPhase), so without a reset the + // leftover editor card would be mistaken for the empty-query commands list and never rebuilt. + builtKey = ""; + if (headEl) headEl.hidden = false; if (qEl) { qEl.value = ""; qEl.placeholder = "Search " + ACTIONS.length + " actions"; @@ -254,16 +455,27 @@ window.HandleStudio = function (payload) { } render({ resize: true, resetScroll: true }); 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)); + } else if (payload.command === "setting_descriptor") { + // Inline editor loaded: render the card. Guard against a stale response for a different id. + if (payload.descriptor && payload.descriptor.id === settingId) + settingDesc = payload.descriptor; render({ resize: true }); + // why: keyboard focus must land on the field after the card is built, not stay on the hidden + // search input. fire on a timeout so the element is attached and its content selectable. + focusSettingEditor(); + } else if (payload.command === "apply_failed") { + flashHint("Couldn't apply that value"); } 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 }); + } else if (payload.command === "favourite_full") { + // Favourites are at the quick-launch cap - undo the optimistic pin and flash a hint. + var fid = payload.id; + if (fid && FAVS.indexOf(fid) !== -1) FAVS.splice(FAVS.indexOf(fid), 1); + render({ resize: true }); + flashHint("Favourites are full (" + (payload.limit || K_FAV_LIMIT) + " max)"); } }; @@ -284,7 +496,6 @@ 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 @@ -349,10 +560,19 @@ function renderFav() { var tile = document.createElement("button"); tile.className = "fav-tile" + (sel.zone === "fav" && sel.i === i ? " sel" : ""); tile.style.setProperty("--h", hue(id)); - tile.textContent = tileCode(a, ACTIONS); + fillTile(tile, a); tile.title = a.title; tile.setAttribute("aria-label", actionLabel(a, ACTIONS)); tile.onclick = function () { sel = { zone: "fav", i: i }; activateEntry(a); }; + // Numbered quick-launch badge (Alt/Option+digit), drawn on the corner. + var slot = favSlotForIndex(i); + if (slot) { + var badge = document.createElement("span"); + badge.className = "fav-slot"; + badge.textContent = slot; + badge.title = slot === "0" ? "Favourite 10 (Alt+0)" : "Favourite " + slot + " (Alt+" + slot + ")"; + tile.appendChild(badge); + } // 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"); @@ -430,35 +650,8 @@ function updateFavEyebrow(favs) { } // One settings row; has no star/tile because settings aren't pinnable. -function renderSettingRow(s, i) { - var row = document.createElement("div"); - row.className = "row" + (sel.zone === "list" && sel.i === i ? " sel" : ""); - row.setAttribute("aria-label", s.label); - - var tile = document.createElement("div"); - tile.className = "tile"; - tile.style.setProperty("--h", hue(s.opt_key)); - tile.textContent = settingCode(s, settingsResults); - - 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; -} - -// A command/action row (used for both recents and filtered results). +// A command/action row - used for search results, recents, and (because settings are actions now) +// the setting options too. All rows are pinnable, so every row carries a star. function renderActionRow(a, i) { var on = FAVS.indexOf(a.id) !== -1; var row = document.createElement("div"); @@ -468,7 +661,7 @@ function renderActionRow(a, i) { var tile = document.createElement("div"); tile.className = "tile"; tile.style.setProperty("--h", hue(a.id)); - tile.textContent = tileCode(a, ACTIONS); + fillTile(tile, a); var left = document.createElement("div"); left.className = "row-left"; @@ -507,78 +700,103 @@ function renderActionRow(a, i) { 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); +// Append rows [from, to) into listEl, always inserting before the bottom spacer so row order is preserved. +function appendActionRows(list, from, to) { + var spacer = spacerEl || ensureSpacer(); + for (var i = from; i < to; i++) { + var row = renderActionRow(list[i], i); + row.setAttribute("data-idx", i); + listEl.insertBefore(row, spacer); } - - 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; +// Ensure the bottom spacer exists as the last child of listEl. It is (re)created on rebuild because +// listEl.innerHTML="" destroys the old node. +function ensureSpacer() { + if (!spacerEl || spacerEl.parentNode !== listEl) { + spacerEl = document.createElement("div"); + spacerEl.className = "dial-spacer-bottom"; + listEl.appendChild(spacerEl); } - if (q && !settingsResults.length) { + return spacerEl; +} + +// Size the spacer to the un-rendered tail so the scrollbar reflects the full match count. +function setBottomSpacer(total) { + ensureSpacer(); + spacerEl.style.height = Math.max(0, total - renderEnd) * ROW_H + "px"; +} + +// Reveal rows up to `upto` (an exclusive index), appending without rebuilding the whole list. Used by +// the scroll handler (viewport + overscan) and by arrow-nav that runs off the end of the current window. +function revealTo(list, upto) { + var need = Math.min(list.length, upto); + if (need <= renderEnd) + return; + appendActionRows(list, renderEnd, need); + renderEnd = need; + setBottomSpacer(list.length); +} + +// Rebuild the list from the first window (new search / phase change), clearing stale rows. +function rebuildCommandsList(list) { + listEl.innerHTML = ""; + listEl.className = "dial-list"; + ensureSpacer(); + renderEnd = 0; + appendActionRows(list, 0, Math.min(list.length, K_ROWS)); + renderEnd = Math.min(list.length, K_ROWS); + setBottomSpacer(list.length); +} + +// Toggle the .sel class in place - arrow-nav/click don't rebuild the DOM, just re-highlight the row. +function updateSelection() { + var rows = listEl ? listEl.querySelectorAll(".row") : []; + for (var i = 0; i < rows.length; i++) { + var idx = parseInt(rows[i].getAttribute("data-idx"), 10); + rows[i].classList.toggle("sel", sel.zone === "list" && idx === sel.i); + } +} + +function renderCommandsList() { + var list = currentList(); + var total = list.length; + if (sel.zone === "list") + sel.i = Math.max(0, Math.min(sel.i, total - 1)); + var showList = shouldRenderActionList(query); + // Recents are not filtered, so clear any stale match marks from a previous typed query. + if (!showList) + matchIndex = {}; + + if (!total) { + listEl.innerHTML = ""; + spacerEl = null; listEl.className = "dial-list empty"; if (countEl) countEl.hidden = true; var empty = document.createElement("div"); empty.className = "dial-empty"; - empty.textContent = "No settings match"; + empty.textContent = showList ? ("No actions match (Total: " + ACTIONS.length + ")") : "No actions yet"; listEl.appendChild(empty); + renderEnd = 0; + builtKey = buildKey() + "|0"; return; } - if (sel.zone === "list") - sel.i = Math.max(0, Math.min(sel.i, settingsResults.length - 1)); + + var key = buildKey() + "|" + total; + if (key !== builtKey) { + builtKey = key; + rebuildCommandsList(list); + } else if (sel.i >= renderEnd) { + // Arrow-nav walked past the rendered window - reveal enough to keep the selection visible. + revealTo(list, revealTarget(total, sel.i, K_ROWS)); + } + listEl.className = "dial-list"; if (countEl) { countEl.hidden = false; - countEl.textContent = showingRecents ? settingsResults.length + " recent" : settingsResults.length + " matches"; + countEl.textContent = showList ? resultCountText(ACTIONS.length, total, query) : total + " recent"; } - 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)); }); + updateSelection(); } // A tab row: no star/unpin (tabs aren't pinnable), tile monogram from the title. Uses tabTitle so @@ -646,13 +864,349 @@ function renderPercentList() { listEl.appendChild(ph); } +// ---- inline setting editor (DOM stage) --------------------------------------- + +// Collapse every open custom dropdown except `keep` (null collapses all). The menu list elements +// are the .ed-dropdown-menu siblings of the toggle buttons we track via openDropDownEl. +function closeOtherDropDowns(keep) { + if (openDropDownEl && openDropDownEl !== keep && openDropDownEl.parentNode) { + var m = openDropDownEl.parentNode.querySelector(".ed-dropdown-menu"); + if (m) m.hidden = true; + openDropDownEl.classList.remove("open"); + } + if (!keep) + openDropDownEl = null; +} + +// Collapse the currently open dropdown, if any (kept for the editor's export/import-adjacent helpers). +function closeEditorDropDown() { closeOtherDropDowns(null); } + +// Place an open dropdown menu as a fixed overlay just under its toggle, so the menu floats over the +// card (never resizing it) and is clamped to the popup's bottom edge with an internal scrollbar for +// long option lists. position:fixed escapes the card/launcher overflow clipping that an absolute +// menu would otherwise hit, keeping every option reachable within the window. +function positionDropDownMenu(btn, menu) { + var lrect = (document.querySelector(".launcher") || { getBoundingClientRect: function () { return { top: 0, bottom: window.innerHeight }; } }).getBoundingClientRect(); + var rect = btn.getBoundingClientRect(); + // Available room above and below the toggle, within the popup. Opening the menu must not push it + // past the window edge (that's the unreachable-overflow bug) - pick whichever side has more room + // and clamp the box to it. Overflow-y:auto scrolls any long list inside the menu itself. + var spaceBelow = lrect.bottom - (rect.bottom + 8); + var spaceAbove = (rect.top - 8) - lrect.top; + var openUp = spaceBelow < spaceAbove; + var maxH = Math.max(0, Math.min(openUp ? spaceAbove : spaceBelow, 200)); + menu.style.position = "fixed"; + menu.style.width = rect.width + "px"; + menu.style.left = rect.left + "px"; + menu.style.maxHeight = maxH + "px"; + if (openUp) { + // bottom edge sits just above the toggle; the box grows upward to content height. + menu.style.top = "auto"; + menu.style.bottom = (lrect.bottom - rect.top + 4) + "px"; + } else { + menu.style.top = (rect.bottom + 4) + "px"; + menu.style.bottom = "auto"; + } +} + +// Build the control element for one row (toggle/number/dropdown/combo/text/color) and seed it with +// the current value. Returns {el, node, extra} - node is what is appended, extra carries a datalist. +function settingInputFor(row) { + var el; + if (row.kind === "toggle") { + el = document.createElement("input"); + el.type = "checkbox"; + el.checked = !!row.value; + var sw = document.createElement("label"); + sw.className = "ed-switch"; + sw.appendChild(el); + var slider = document.createElement("span"); + slider.className = "ed-slider"; + sw.appendChild(slider); + return { el: el, node: sw }; + } + if (row.kind === "number") { + el = document.createElement("input"); + el.type = "number"; + el.step = row.is_int ? 1 : "any"; + if (row.min != null) el.min = row.min; + if (row.max != null) el.max = row.max; + if (row.value != null && row.value !== "") el.value = row.value; + return { el: el, node: el }; + } + if (row.kind === "dropdown") { + // Native popups are flaky in the embedded webview) */ +.ed-dropdown { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; } +.ed-dropdown-toggle { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + height: 30px; + padding: 0 8px; + border: 1px solid var(--border, var(--orca-border, #ddd)); + border-radius: 6px; + background: var(--panel, var(--orca-bg, #fff)); + color: var(--text, var(--orca-fg, #1b1c1e)); + font: inherit; + text-align: left; + cursor: pointer; +} +.ed-dropdown-toggle.open { border-color: var(--main-color, var(--orca-accent, #009688)); } +.ed-dropdown-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.ed-dropdown-caret { flex: 0 0 auto; font-size: 10px; color: var(--muted, var(--orca-muted, #6b7280)); transition: transform .12s; } +.ed-dropdown-toggle.open .ed-dropdown-caret { transform: rotate(180deg); } +.ed-dropdown-menu { + /* Overlay popover, not in-flow: opening it must NOT grow the editor card or the popup window. + position/left/top/width/max-height are set from JS on open (position:fixed escapes the + scrollable card and launcher overflow, so the menu clips to the popup and scrolls itself). + These are only the visual defaults. */ + position: absolute; + z-index: 30; + border: 1px solid var(--border, var(--orca-border, #ddd)); + border-radius: 6px; + background: var(--panel, var(--orca-bg, #fff)); + box-shadow: 0 8px 24px rgba(0,0,0,.16); + max-height: 200px; + overflow-y: auto; +} +.ed-dropdown-option { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 6px 8px; + border: 0; + background: none; + color: var(--text, var(--orca-fg, #1b1c1e)); + font: inherit; + text-align: left; + cursor: pointer; +} +.ed-dropdown-option:hover { background: var(--row-hover, rgba(127,127,127,.16)); } +.ed-dropdown-option.sel { font-weight: 600; color: var(--main-color, var(--orca-accent, #009688)); } +.ed-option-label { flex: 1 1 auto; min-width: 0; } +/* Pattern pictograms: filled with the option's icon, and (for values with none) absent. */ +.ed-option-icon, +.ed-dropdown-icon { + flex: 0 0 auto; + width: 16px; + height: 16px; +} +.ed-dropdown-icon[hidden] { display: none; } +/* toggle switch */ +.ed-switch { position: relative; flex: 0 0 auto; width: 36px; height: 20px; } +.ed-switch input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; } +.ed-slider { + position: absolute; + inset: 0; + background: #c4c4c4; + border-radius: 10px; + transition: background .15s; +} +.ed-slider::before { + content: ""; + position: absolute; + width: 16px; + height: 16px; + left: 2px; + top: 2px; + background: #fff; + border-radius: 50%; + transition: transform .15s; +} +.ed-switch input:checked + .ed-slider { background: var(--main-color, var(--orca-accent, #009688)); } +.ed-switch input:checked + .ed-slider::before { transform: translateX(16px); } +.editor-readonly { padding: 4px 0; font-size: 12px; color: var(--muted, var(--orca-muted, #6b7280)); } +.editor-actions { display: flex; gap: 8px; padding-top: 4px; } +.ed-btn { + padding: 6px 12px; + border: 1px solid var(--border, var(--orca-border, #ddd)); + border-radius: 6px; + background: var(--panel, var(--orca-bg, #fff)); + color: var(--text, var(--orca-fg, #1b1c1e)); + font: inherit; + cursor: pointer; +} +.ed-btn:hover { background: var(--row-hover, rgba(127,127,127,.16)); } +.ed-btn-primary { background: var(--main-color, var(--orca-accent, #009688)); border-color: var(--main-color, var(--orca-accent, #009688)); color: #fff; } +.ed-btn-primary:hover { filter: brightness(1.05); } +.editor-hint { font-size: 10px; text-align: center; color: var(--muted, var(--orca-muted, #6b7280)); } diff --git a/resources/web/js/fuzzy-search.js b/resources/web/js/fuzzy-search.js index 9e00e40eae..36dd9edb10 100644 --- a/resources/web/js/fuzzy-search.js +++ b/resources/web/js/fuzzy-search.js @@ -7,6 +7,10 @@ // Fold per-character so matched offsets stay in ORIGINAL string coordinates (highlighting slices the // original text; a separately-folded string would desync offsets). function FoldChar(ch) { + // why: fast path - ASCII is already NFD-stable and diacritic-free, so the normalize/regex below are + // no-ops. This skip is the hot cost in the Speed Dial search (thousands of settings per keystroke). + if (ch.length === 1 && ch.charCodeAt(0) < 0x80) + return ch; return ch.normalize("NFD").replace(/\p{Diacritic}/gu, ""); // accents always folded } @@ -15,6 +19,41 @@ function Norm(ch, caseSensitive) { return caseSensitive ? folded : folded.toLowerCase(); // case-sensitivity is the only toggle } +// Pre-normalize a whole haystack with the SAME per-char fold FuzzyRanges uses, so a caller can match +// it repeatedly against one cached string. The fold is 1:1 in length, so indices stay aligned to the +// ORIGINAL text - the highlight ranges that FuzzyRangesNorm returns slice the original correctly. +// Iterate by UTF-16 code unit (not Array.from code point) to mirror FuzzyRanges' own indexing exactly. +function NormText(text, caseSensitive) { + const src = text || ""; + let out = ""; + for (let i = 0; i < src.length; i++) + out += Norm(src[i], caseSensitive); + return out; +} + +// Match a PRE-normalized haystack against a PRE-normalized needle (both produced by NormText with the +// same caseSensitive flag). Skipping the per-character fold makes repeated matching (per keystroke over a +// cached pool) cheap. Returns ranges in original coordinates, or null on no match. +function FuzzyRangesNorm(haystackNorm, needleNorm) { + const t = haystackNorm || ""; + const needle = needleNorm || ""; + if (!needle) + return null; + const ranges = []; + let qi = 0; + for (let i = 0; i < t.length && qi < needle.length; i++) { + if (t[i] === needle[qi]) { + const last = ranges[ranges.length - 1]; + if (last && last[1] === i) + last[1] = i + 1; + else + ranges.push([i, i + 1]); + qi++; + } + } + return qi === needle.length ? ranges : null; +} + function EscapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index 3b9e568a96..3e56c857cc 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -16,18 +16,27 @@ #include #include #include +#include #include #include #include +#include #include +#include #include #include +#include #include +#include #include +#include +#include +#include #include +#include namespace Slic3r { namespace GUI { @@ -131,6 +140,60 @@ std::unique_ptr make_action(const std::string& plugin_key, const std: constexpr const char* kCommandPrefix = "orca_command"; constexpr const char* kOrcaSourceKey = "orca"; constexpr const char* kOrcaSourceName = "OrcaSlicer"; +constexpr const char* kSettingPrefix = "orca_setting"; + +// Display context for a setting action's eyebrow, e.g. the "Process" in "Process : Quality : Layers". +// Keyed by the option's preset type so the palette reads like the settings sidebar tabs. +std::string setting_type_context(Preset::Type type) +{ + switch (type) { + case Preset::TYPE_FILAMENT: + case Preset::TYPE_SLA_MATERIAL: return _u8L("Filament"); + case Preset::TYPE_PRINTER: return _u8L("Printer"); + case Preset::TYPE_PRINT: + case Preset::TYPE_SLA_PRINT: + default: return _u8L("Process"); + } +} + +// A config setting exposed as a first-class action: selecting it jumps the sidebar to the option. +// The id is keyed by opt_key+type (NOT the display label), so renaming/localizing never re-keys +// the action; title/group/source are purely for display + search. run() performs the jump, and +// the generic registry run() bumps stats so a jump shows up in "recents" like any other action. +struct SettingAction : AppAction +{ + std::string opt_key; + Preset::Type type; + std::wstring category; // localized category, forwarded to jump_to_option + + static std::string id_for(const std::string& opt_key, Preset::Type type) + { return std::string(kSettingPrefix) + ":" + opt_key + ":" + std::to_string(int(type)); } + + SettingAction(std::string opt_key_in, Preset::Type type_in, std::string title, std::string group, + std::wstring category_in, std::string source_name) + : AppAction(AppActionId{id_for(opt_key_in, type_in)}, std::move(title), kOrcaSourceKey, std::move(source_name)) + , opt_key(std::move(opt_key_in)) + , type(type_in) + , category(std::move(category_in)) + { + // A setting is a two-phase command: activating it opens the inline editor (the "setting" + // phase) instead of running. Native run() is a no-op fallback; the editor applies through + // apply_setting(). + this->kind = AppActionKind::Command; + this->group = std::move(group); + this->input = "setting"; + } + + AppActionRunResult run(const std::string& /*param*/) const override + { + // Two-phase: the palette collects the edit; native run() is a no-op fallback. + return {AppActionRunResult::Level::Success}; + } + + // The current value's pattern pictogram (e.g. the selected infill pattern), for the search-result + // tile. Defined below after the icon helper it delegates to. + std::string icon() const override; +}; // 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 @@ -219,9 +282,9 @@ AppActionRunResult run_native_command(const std::string& command_key, const std: } 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") + // "go_to_tab" is two-phase: the palette collects the tab after activating it, so native + // dispatch here is a no-op (the jump goes through the go_to_tab web command). + if (command_key == "go_to_tab") return {AppActionRunResult::Level::Success}; return {AppActionRunResult::Level::Info, _L("Unknown command.")}; } @@ -253,10 +316,9 @@ std::vector> native_commands() // 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. + // Two-phase commands: activating them collects input in the palette, then runs. Settings are + // not a command here - they're materialised as first-class SettingActions (see materialize_). 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"))); @@ -268,19 +330,305 @@ std::vector> native_commands() 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 settings_inputs() +// ---- inline setting editor helpers ------------------------------------------ + +// The inline editor "control" kind for an option, or "" when it can't be edited inline +// (plugin-backed values, points, serialized strings, etc.). readonly/legend options are +// filtered out of the search entirely in materialize_setting_actions(), so they never +// reach here. +std::string setting_control(const ConfigOptionDef& def) { - std::vector 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; + if (def.readonly || def.gui_type == ConfigOptionDef::GUIType::legend || + def.gui_type == ConfigOptionDef::GUIType::one_string || def.is_plugin_backed()) + return ""; + // Serialized vectors are entered as ONE semicolon-separated field (e.g. post_process), which the + // per-index editor doesn't model - keep them in the open-in-sidebar bucket. + if (def.gui_flags.find("serialized") != std::string::npos) + return ""; + switch (def.gui_type) { + case ConfigOptionDef::GUIType::color: return "color"; + case ConfigOptionDef::GUIType::i_enum_open: + case ConfigOptionDef::GUIType::f_enum_open: return "combo"; + default: break; + } + switch (def.type) { + case coBool: + case coBools: return "toggle"; + case coEnum: + case coEnums: return def.enum_values.empty() ? "combo" : "dropdown"; + case coInt: + case coInts: + case coFloat: + case coFloats: + case coPercent: + case coPercents: return "number"; + case coFloatOrPercent: + case coFloatsOrPercents: return "percent"; + case coString: + case coStrings: return "text"; + default: return ""; + } +} + +// Self-contained base64 encoder (for the tiny pictogram SVGs), avoiding a dependency on the exact +// wxBase64Encode overload/return type across wx versions. +std::string base64_encode(const std::string& data) +{ + static const char* tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + auto enc = [&](unsigned n, int pad) { + // pad = number of extraneous bytes in the final group (0, 1 or 2): + // 0 leftover -> 4 chars from all 24 bits + // 2 leftover (pad=1) -> 3 chars then '=' + // 1 leftover (pad=2) -> 2 chars then "==" + // The '=' padding always comes LAST; a misplaced '=' decodes as garbage in the webview. + std::string out; + out.push_back(tbl[(n >> 18) & 63]); + out.push_back(tbl[(n >> 12) & 63]); + out.push_back(pad >= 2 ? '=' : tbl[(n >> 6) & 63]); + out.push_back(pad >= 1 ? '=' : tbl[n & 63]); + return out; + }; + std::string out; + out.reserve(((data.size() + 2) / 3) * 4); + size_t i = 0; + for (; i + 3 <= data.size(); i += 3) + out += enc(((unsigned char) data[i]) << 16 | ((unsigned char) data[i + 1]) << 8 | ((unsigned char) data[i + 2]), 0); + if (i + 1 == data.size()) + out += enc(((unsigned char) data[i]) << 16, 2); + else if (i + 2 == data.size()) + out += enc(((unsigned char) data[i]) << 16 | ((unsigned char) data[i + 1]) << 8, 1); + return out; +} + +// data:URI for the pattern pictogram icons/param_.svg, or "" when there is no such icon. +// This mirrors the sidebar Choice field (Field.cpp add_item_bitmaps), which loads param_.svg +// per enum value - most settings have no icon, only pattern-style enums (infill/support patterns). +// Base64 data URIs are used so the embedded webview renders them identically on every backend +// (no file:// subresource / CORS restrictions). +std::string setting_icon_for_key(const std::string& key) +{ + if (key.empty()) + return {}; + + const std::string path = (boost::filesystem::path(resources_dir()) / "images" / ("param_" + key + ".svg")).string(); + // Non-throwing stat: a throwing filesystem_error here would propagate out of snapshot() and + // abort the app (the palette opener). exists(fs ::error_code) never throws. + boost::system::error_code ec; + if (!boost::filesystem::exists(path, ec)) + return {}; + + std::ifstream in(path, std::ios::binary); + if (!in) + return {}; + std::string data((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + if (data.empty()) + return {}; + + return "data:image/svg+xml;base64," + base64_encode(data); +} + +// [{value,key,label,icon}...] for an enum/combo. Ordered by enum_values when present, else by the +// keys_map iteration. label falls back to the key when enum_labels doesn't provide one. `icon` is +// the value's pattern pictogram when one exists, empty otherwise. +nlohmann::json setting_enum_options(const ConfigOptionDef& def) +{ + nlohmann::json out = nlohmann::json::array(); + auto label_at = [&](size_t i, const std::string& key) -> std::string { + return (i < def.enum_labels.size() && !def.enum_labels[i].empty()) ? def.enum_labels[i] : key; + }; + if (def.enum_keys_map != nullptr) { + std::vector ordered; + if (!def.enum_values.empty()) + ordered = def.enum_values; + else + for (const auto& kv : *def.enum_keys_map) + ordered.push_back(kv.first); + for (size_t i = 0; i < ordered.size(); ++i) { + auto it = def.enum_keys_map->find(ordered[i]); + if (it == def.enum_keys_map->end()) + continue; + out.push_back({{"value", it->second}, + {"key", ordered[i]}, + {"label", label_at(i, ordered[i])}, + {"icon", setting_icon_for_key(ordered[i])}}); + } + } else { + for (size_t i = 0; i < def.enum_values.size(); ++i) + out.push_back({{"value", (long long) i}, + {"key", def.enum_values[i]}, + {"label", label_at(i, def.enum_values[i])}, + {"icon", setting_icon_for_key(def.enum_values[i])}}); + } + return out; +} + +// The pattern pictogram for a setting's CURRENT value (its enum int), empty when it isn't a +// pattern-style enum or the value has no icon. Used for the search-result tile. +std::string setting_action_icon(const SettingAction& a) +{ + Tab* tab = wxGetApp().get_tab(a.type); + if (!tab || !tab->get_config()) + return {}; + DynamicPrintConfig* config = tab->get_config(); + const ConfigOptionDef* def = config->def()->get(a.opt_key); + if (!def || def->type != coEnum || (int(def->type) & int(coVectorType)) != 0) + return {}; + // Read the value WITHOUT config->opt_int(): the non-const overload routes through a type-checked + // option() that returns null for enum values (type() is coEnum, not coInt) and + // would deref null. Pull the ConfigOption* and dynamic_cast instead (succeeds: enums derive from + // ConfigOptionInt), falling back to the def default when the option is absent. + const ConfigOption* opt = (config->has(a.opt_key) ? config->option(a.opt_key) : def->default_value.get()); + const ConfigOptionInt* int_opt = dynamic_cast(opt); + if (!int_opt) + return {}; + const int value = int_opt->getInt(); + if (def->enum_keys_map) + for (const auto& kv : *def->enum_keys_map) + if (kv.second == value) + return setting_icon_for_key(kv.first); + return {}; +} + +std::string SettingAction::icon() const { return setting_action_icon(*this); } + +// Current value of the option at vector index `idx` as JSON (bool/number/string), or null for a +// type the inline editor doesn't render. `config` is the tab's live config; when an option is +// absent the def's default is shown. +nlohmann::json setting_value_json(const DynamicPrintConfig& config, const ConfigOptionDef& def, size_t idx) +{ + const ConfigOption* opt = config.option(def.opt_key); + const ConfigOption* root = opt ? opt : def.default_value.get(); + if (!root) + return nullptr; + switch (def.type) { + case coBool: return root->getBool(); + case coInt: return root->getInt(); + case coFloat: return root->getFloat(); + case coPercent: return root->getFloat(); + case coString: return static_cast(root)->value; + case coEnum: return root->getInt(); + case coBools: { + if (auto v = dynamic_cast(root)) + return bool(v->get_at(idx)); + if (auto v = dynamic_cast(root)) + return bool(v->get_at(idx) != 0); + return nullptr; + } + case coInts: { + if (auto v = dynamic_cast(root)) + return v->get_at(idx); + if (auto v = dynamic_cast(root)) { + const int nil = ConfigOptionIntsNullable::nil_value(); + int val = v->get_at(idx); + return val == nil ? nlohmann::json(nullptr) : nlohmann::json(val); + } + return nullptr; + } + case coFloats: { + if (auto v = dynamic_cast(root)) + return v->get_at(idx); + if (auto v = dynamic_cast(root)) { + double val = v->get_at(idx); + return std::isnan(val) ? nlohmann::json(nullptr) : nlohmann::json(val); + } + return nullptr; + } + case coPercents: { + if (auto v = dynamic_cast(root)) + return v->get_at(idx); + if (auto v = dynamic_cast(root)) { + double val = v->get_at(idx); + return std::isnan(val) ? nlohmann::json(nullptr) : nlohmann::json(val); + } + return nullptr; + } + case coStrings: return static_cast(root)->get_at(idx); + case coEnums: { + if (auto v = dynamic_cast(root)) + return v->get_at(idx); + if (auto v = dynamic_cast(root)) { + const int nil = ConfigOptionEnumsGenericNullable::nil_value(); + int val = v->get_at(idx); + return val == nil ? nlohmann::json(nullptr) : nlohmann::json(val); + } + return nullptr; + } + case coFloatOrPercent: return root->serialize(); + case coFloatsOrPercents: { + if (auto v = dynamic_cast(root)) { + auto ss = v->vserialize(); + return idx < ss.size() ? ss[idx] : nullptr; + } + if (auto v = dynamic_cast(root)) { + auto ss = v->vserialize(); + return idx < ss.size() ? ss[idx] : nullptr; + } + return nullptr; + } + default: return nullptr; + } +} + +// How many scalar values the option currently has (1 for scalars, the array length for vectors). +size_t setting_value_count(const DynamicPrintConfig& config, const ConfigOptionDef& def) +{ + if ((int(def.type) & int(coVectorType)) == 0) + return 1; + // size() lives on ConfigOptionVectorBase, not ConfigOption - dynamic_cast to it covers every + // vector type (and their nullable variants) polymorphically. + const ConfigOption* opt = config.option(def.opt_key); + if (opt) + if (auto v = dynamic_cast(opt)) + return v->size(); + if (def.default_value) + if (auto v = dynamic_cast(def.default_value.get())) + return v->size(); + return 1; +} + +// boost::any for a single element, matching what Slic3r::GUI::change_opt_value expects. +boost::any setting_any_from_json(const ConfigOptionDef& def, const nlohmann::json& v) +{ + if (!v.is_null()) { + switch (def.type) { + case coBool: return boost::any(v.is_boolean() ? v.get() : v.get() != 0); + case coInt: return boost::any(v.get()); + case coFloat: + case coPercent: return boost::any(v.get()); + case coString: return boost::any(v.get()); + case coEnum: return boost::any(v.get()); + case coBools: return boost::any(static_cast(v.get() ? 1 : 0)); + case coInts: return boost::any(v.get()); + case coFloats: + case coPercents: return boost::any(v.get()); + case coStrings: return boost::any(v.get()); + case coFloatOrPercent: + case coFloatsOrPercents: { + // change_opt_value detects "percent" via a trailing '%', so trim whitespace first or a + // stray space (e.g. "10% ") would be misread as mm. + std::string s = v.is_string() ? v.get() : std::to_string(v.get()); + boost::trim(s); + // An empty string would make change_opt_value's str.back() UB - bail out to a rejected apply. + return s.empty() ? boost::any() : boost::any(s); + } + case coEnums: return boost::any(v.get()); + default: break; + } + } + // Coerce numeric types that may arrive as a different JSON numeric type. + if (v.is_number()) { + switch (def.type) { + case coInt: + case coEnums: return boost::any(v.get()); + case coFloat: + case coPercent: + case coInts: + case coFloats: + case coPercents: return boost::any(v.get()); + default: break; + } + } + return boost::any(); } } // namespace @@ -411,7 +759,9 @@ void ActionRegistry::remove(const std::string& id) void ActionRegistry::seed_state(AppAction& a) const { - auto favs = read_string_array("favourite_actions"); + // Favourites carry the quick-launch order, so the persisted list is the source of truth + // (not re-derived from the frecency sort). Cap it so stale configs can't exceed kFavLimit. + auto favs = favourite_ids(); a.favourite = std::find(favs.begin(), favs.end(), a.id()) != favs.end(); nlohmann::json stats = read_section("stats", nlohmann::json::object()); @@ -469,18 +819,40 @@ AppActionRunResult ActionRegistry::run(const std::string& id, const std::string& return o; } -void ActionRegistry::set_favourite(const std::string& id, bool on) +bool ActionRegistry::set_favourite(const std::string& id, bool on) { assert(wxThread::IsMain()); - auto favs = read_string_array("favourite_actions"); + // Start from the capped, deduped list so a persisted config can never be written back larger. + auto favs = favourite_ids(); auto it = std::find(favs.begin(), favs.end(), id); - if (on && it == favs.end()) + if (on && it == favs.end()) { + if (favs.size() >= kFavLimit) + return false; // bar is full - the caller surfaces a hint favs.push_back(id); + } if (!on && it != favs.end()) favs.erase(it); write_section("favourite_actions", nlohmann::json(favs)); if (AppAction* live = find(id)) live->favourite = on; + return true; +} + +std::vector ActionRegistry::favourite_ids() const +{ + assert(wxThread::IsMain()); + // Enforce the cap + dedupe on read so the persisted order can never grow past kFavLimit, + // even from an older config. The pinned order is intentionally preserved (slice, not sort). + std::vector favs = read_string_array("favourite_actions"); + std::vector out; + out.reserve(std::min(favs.size(), kFavLimit)); + for (const auto& id : favs) { + if (out.size() >= kFavLimit) + break; + if (std::find(out.begin(), out.end(), id) == out.end()) + out.push_back(id); + } + return out; } void ActionRegistry::reorder_favourites(const std::vector& ids) @@ -496,9 +868,78 @@ void ActionRegistry::reorder_favourites(const std::vector& ids) for (const auto& id : cur) if (std::find(next.begin(), next.end(), id) == next.end()) next.push_back(id); + // never write the bar back larger than the quick-launch slots + if (next.size() > kFavLimit) + next.resize(kFavLimit); write_section("favourite_actions", nlohmann::json(next)); } +void ActionRegistry::materialize_setting_actions() +{ + assert(wxThread::IsMain()); + + // Reuse the Sidebar's live searcher: it's the only OptionsSearcher whose groups_and_categories + // map is populated (Tab::add_key feeds it at build time), and it already mirrors the current + // configs/mode/printer-technology - i.e. exactly what the sidebar's own search would show. A + // fresh OptionsSearcher has an empty groups_and_categories, so append_options() would drop every + // option and nothing would materialise. Turn each visible option into a SettingAction. + const std::vector& options = wxGetApp().sidebar().get_searcher().all_options(); + + // Load the persisted per-action state ONCE (not per-option) so a re-materialised setting keeps + // its recency/favourite; mirroring seed_state but amortised over the whole option set. + nlohmann::json stats = read_section("stats", nlohmann::json::object()); + if (!stats.is_object()) + stats = nlohmann::json::object(); + const std::vector favs = favourite_ids(); + + std::unordered_set seen; + for (const Search::Option& opt : options) { + // Omit rows the inline editor can't represent and that aren't useful as a jump target: + // readonly (e.g. the detected thread count) and legend (static text) GUI rows. They remain + // in the sidebar's own search; only the Speed Dial pool drops them. + Tab* tab = wxGetApp().get_tab(opt.type); + if (tab && tab->get_config()) { + const ConfigOptionDef* def = tab->get_config()->def()->get(opt.opt_key()); + if (!def || def->readonly || def->gui_type == ConfigOptionDef::GUIType::legend) + continue; + } + + const std::string id = SettingAction::id_for(opt.opt_key(), opt.type); + seen.insert(id); + + const std::wstring label_w = opt.label_local.empty() ? opt.label : opt.label_local; + + // Eyebrow/source = the full settings path "Process : Quality : Layers" (localized). The JS + // renders group || source and searches source + " " + group, so putting the whole path in + // source both displays it and makes it matchable by any segment (e.g. a "quality" query). + std::wstring path = boost::nowide::widen(setting_type_context(opt.type)); + if (!opt.category_local.empty()) + path += L" : " + opt.category_local; + if (!opt.group_local.empty()) + path += L" : " + opt.group_local; + + // title = the option leaf name (last label segment); group stays empty so the source path + // (above) is the single display/search breadcrumb rather than being duplicated. + auto action = std::make_unique(opt.opt_key(), opt.type, boost::nowide::narrow(label_w), + std::string(), opt.category_local, boost::nowide::narrow(path)); + action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); + if (auto it = stats.find(id); it != stats.end() && it->is_object()) { + action->count = it->value("count", 0); + action->last = it->value("last", 0LL); + } + m_actions.insert_or_assign(action->id(), std::shared_ptr(std::move(action))); + } + + // Drop SettingActions whose option no longer exists in the current configs (e.g. the printer + // technology / UI mode changed). Non-setting actions are untouched. + for (auto it = m_actions.begin(); it != m_actions.end();) { + if (it->first.rfind(kSettingPrefix, 0) == 0 && !seen.count(it->first)) + it = m_actions.erase(it); + else + ++it; + } +} + bool ActionRegistry::should_ask(const std::string& id) const { assert(wxThread::IsMain()); @@ -517,9 +958,13 @@ void ActionRegistry::suppress_ask(const std::string& id) // ---- snapshot --------------------------------------------------------------- -nlohmann::json ActionRegistry::snapshot() const +nlohmann::json ActionRegistry::snapshot() { assert(wxThread::IsMain()); + // Settings are first-class actions; make sure the current visible option set is materialised + // before we serialise the pool (tabs_list is built by the time the palette opens). + materialize_setting_actions(); + std::vector sorted; sorted.reserve(m_actions.size()); for (const auto& entry : m_actions) @@ -544,7 +989,8 @@ nlohmann::json ActionRegistry::snapshot() const {"source", a->source_name()}, {"group", a->group}, {"input", a->input}, - {"shortcut", ""}}); + {"shortcut", ""}, + {"icon", a->icon()}}); }; nlohmann::json actions = nlohmann::json::array(); @@ -554,7 +1000,8 @@ nlohmann::json ActionRegistry::snapshot() const // 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 // reorder the favourites bar). The page (js) filters out ids with no live action itself. - nlohmann::json favourites(read_string_array("favourite_actions")); + // Cap on read so the bar cannot exceed the quick-launch slots (kFavLimit). + nlohmann::json favourites(favourite_ids()); // Recent = the last-N launched actions by recency (only actions with a run history). constexpr size_t kRecentLimit = 5; @@ -576,49 +1023,6 @@ nlohmann::json ActionRegistry::snapshot() const 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(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 + / 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 @@ -640,37 +1044,140 @@ nlohmann::json ActionRegistry::tab_options() const return out; } -// ---- settings recents (persisted, most-recent-first, capped at 8) ----------- +// ---- inline setting editor (read the current value) -------------------------- -nlohmann::json ActionRegistry::settings_recent() const +nlohmann::json ActionRegistry::setting_descriptor(const std::string& id) const { assert(wxThread::IsMain()); - return read_section("recent_settings", nlohmann::json::array()); + const AppAction* a = by_id(id); + const SettingAction* sa = dynamic_cast(a); + if (!sa) + return nlohmann::json::object(); + Tab* tab = wxGetApp().get_tab(sa->type); + if (!tab) + return nlohmann::json::object(); + DynamicPrintConfig* config = tab->get_config(); + if (!config) + return nlohmann::json::object(); + const ConfigOptionDef* def = config->def()->get(sa->opt_key); + if (!def) + return nlohmann::json::object(); + + const std::string control = setting_control(*def); + const bool vector = (int(def->type) & int(coVectorType)) != 0; + + nlohmann::json d = {{"id", sa->id()}, + {"opt_key", sa->opt_key}, + {"type", int(sa->type)}, + {"title", a->title()}, + {"breadcrumb", a->source_name()}, + {"category", boost::nowide::narrow(sa->category)}, + {"unit", def->sidetext}, + {"tooltip", def->tooltip}, + {"editable", !control.empty()}, + {"control", control}, + {"cardinality", vector ? "vector" : "scalar"}}; + + if (!control.empty()) { + // Hide unbounded min/max so the page doesn't clamp a sane value to ±FLT_MAX. + if (def->min > -FLT_MAX) + d["min"] = def->min; + if (def->max < FLT_MAX) + d["max"] = def->max; + if (control == "number") + d["is_int"] = (def->type == coInt || def->type == coInts); + if (control == "dropdown" || control == "combo") + d["enum_options"] = setting_enum_options(*def); + if (vector) { + nlohmann::json values = nlohmann::json::array(); + nlohmann::json labels = nlohmann::json::array(); + const size_t n = setting_value_count(*config, *def); + for (size_t i = 0; i < n; ++i) { + values.push_back(setting_value_json(*config, *def, i)); + labels.push_back(std::to_string(i + 1)); + } + d["values"] = std::move(values); + d["index_labels"] = std::move(labels); + } else { + d["value"] = setting_value_json(*config, *def, 0); + } + } + return d; } -void ActionRegistry::record_setting_recent( - const std::string& opt_key, int type, const std::string& label, const std::string& category, const std::string& group) +// ---- inline setting editor (write the edited value back) --------------------- + +bool ActionRegistry::apply_setting(const std::string& id, const nlohmann::json& value) { assert(wxThread::IsMain()); - if (opt_key.empty()) - return; + const AppAction* a = by_id(id); + const SettingAction* sa = dynamic_cast(a); + if (!sa) + return false; + Tab* tab = wxGetApp().get_tab(sa->type); + if (!tab) + return false; + DynamicPrintConfig* config = tab->get_config(); + if (!config) + return false; + const ConfigOptionDef* def = config->def()->get(sa->opt_key); + if (!def) + return false; + const std::string control = setting_control(*def); + if (control.empty()) + return false; - 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; - }; + const bool vector = (int(def->type) & int(coVectorType)) != 0; + const size_t n = vector ? (value.is_array() ? value.size() : 0) : 1; + if (vector && n == 0) + return false; - 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); + for (size_t i = 0; i < n; ++i) { + const nlohmann::json& elem = vector ? value[i] : value; + boost::any any = setting_any_from_json(*def, elem); + if (any.empty()) + return false; + if (control == "number" && elem.is_number()) { + const double d = elem.get(); + if (d < def->min || d > def->max) + return false; + } + if (control == "percent" && elem.is_string()) { + // "mm or %" value: strip a trailing %/whitespace, clamp the numeric part to [min,max]. + // Reject anything that isn't a well-formed number (which change_opt_value would throw on). + std::string s = elem.get(); + boost::trim(s); + if (!s.empty() && s.back() == '%') + s.pop_back(); + boost::trim(s); + if (s.empty()) + return false; + char* end = nullptr; + const double d = std::strtod(s.c_str(), &end); + if (end == s.c_str() || *end != '\0') + return false; + if (d < def->min || d > def->max) + return false; + } + Slic3r::GUI::change_opt_value(*config, sa->opt_key, any, int(i)); + } + + // Mark the preset modified like a sidebar edit. Scalar options also get the standard + // post-change hook so dependent settings refresh; vector options have no unambiguous scalar + // value to pass, so on_value_change is skipped (the config write + dirty flag is still correct). + tab->update_dirty(); + if (!vector) { + boost::any any = setting_any_from_json(*def, value); + if (!any.empty()) + tab->on_value_change(sa->opt_key, any); + } + + // The config write is separate from the on-screen Field, so repaint the field(s) that display + // this option (on whatever page they live, not just the active page) - otherwise the sidebar + // shows the "modified" arrow but keeps the stale value pushed to the last edit/reload. + if (Page* page = nullptr; tab->get_field(sa->opt_key, &page) && page) + page->reload_config(); + return true; } }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index 7b718b4c09..1c481c1da9 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -31,6 +32,14 @@ struct AppActionRunResult wxString message; // empty = "nothing worth showing" }; +// Tag carrying a precomputed action id, used by the explicit-id ctor below. It exists so the +// id ctor and the compose-from-prefix ctor are NOT both reachable from a `const char*` first +// argument (which would make calls like AppAction("orca_command", ...) ambiguous). +struct AppActionId +{ + std::string id; +}; + // A speed-dial action: identity + user-state seeded from config + how to run itself. // Abstract base - the only virtual is run(); concrete subclasses know how to run // and what their source is. @@ -72,6 +81,11 @@ struct AppAction // commands (e.g. a layer percentage); plugins ignore it. virtual AppActionRunResult run(const std::string& param = {}) const = 0; + // Optional data:URI for a small pictogram to show in the palette row/tile/the editor + // (e.g. the current infill/pattern). Empty string = fall back to the monogram. Only + // SettingAction overrides this; the base returns an empty string. + virtual std::string icon() const { return {}; } + protected: // The definition is constructor-set and immutable. Refreshes replace an action // instead of mutating identity after the registry has indexed it by id. @@ -83,6 +97,14 @@ protected: m_source_key(std::move(source_key)), m_source_name(std::move(source_name)) {} + // Explicit-id ctor: for actions whose id must NOT be derived from the display title + // (e.g. a setting action keyed by opt_key+type, so a rename/localization never re-keys it). + AppAction(AppActionId id, std::string title, std::string source_key, std::string source_name) + : m_id(std::move(id.id)), + m_title(std::move(title)), + m_source_key(std::move(source_key)), + m_source_name(std::move(source_name)) {} + private: std::string m_id; // ::<source_key> - stable identity + AppConfig key std::string m_title; // display name @@ -122,34 +144,27 @@ public: // Always-clean read surface. UI thread only. const AppAction* by_id(const std::string& id) const; + // Hard cap on the favourites bar: the numbered quick-launch slots (Alt/Option+1..9, 0). + static constexpr size_t kFavLimit = 10; + // Dispatch + write-through (registry is the only thing that touches AppConfig). AppActionRunResult run(const std::string& id, const std::string& param = {}); // runs + bumps stats - void set_favourite(const std::string& id, bool on); + // Pin/unpin. Returns false when `on` would exceed kFavLimit (the bar is full) so the + // caller can surface a "favourites are full" hint instead of silently dropping the pin. + bool set_favourite(const std::string& id, bool on); void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order + // Ordered pinned list (the source of truth), capped at kFavLimit and deduped, matching the + // visible bar the palette renders. + std::vector<std::string> favourite_ids() const; + // Run-confirm gate, keyed by action id (per-action "don't ask again"). bool should_ask(const std::string& id) const; void suppress_ask(const std::string& id); // Flat, frecency-sorted snapshot for the webview: // {actions:[...], favourites:[...], recent:[...]} (recent = last-N launched by recency). - 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); + nlohmann::json snapshot(); // "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/ @@ -158,10 +173,25 @@ public: // plugins) aren't separate pages and are not listed. Call on the UI thread; null-safe. nlohmann::json tab_options() const; + // Inline setting editor descriptor for a SettingAction id. Returns the JSON the palette + // renders: {id, opt_key, type, title, breadcrumb, category, group, unit, tooltip, editable, + // control ("toggle|number|dropdown|combo|text|color"), cardinality ("scalar"|"vector"), + // value|values, index_labels[], enum_options[], min|max}. Empty object for a non-setting id. + nlohmann::json setting_descriptor(const std::string& id) const; + // Apply an edit submitted by the palette. `value` is the control's JSON payload (scalar, or an + // array for vector settings). Writes the value(s) into the global preset config and marks the + // preset dirty, exactly like a sidebar edit. Returns false on a bad id/type/value. + bool apply_setting(const std::string& id, const nlohmann::json& value); + private: void seed_state(AppAction& a) const; // favourite/stats from config AppAction* find(const std::string& id); + // (Re)materialise the current visible config settings as SettingActions from the live + // searcher (respecting printer-tech + user-mode + visibility filtering), removing stale ones. + // Called at the top of snapshot() so the palette always reflects the current configs. + void materialize_setting_actions(); + // Loader callbacks (marshalled to the UI thread) land here. refresh_source rebuilds // one plugin's whole action set; refresh_capability touches a single capability. void refresh_source(const std::string& plugin_key, ActionChange change); diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index 82e499ac55..6ea121c16d 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -198,7 +198,8 @@ void KBShortcutsDialog::fill_shortcuts() // Switch table page { ctrl + L("Tab"), L("Switch table page")}, // Open speed dial - { ctrl + "K", L("Open speed dial") }, + { "Space", L("Open speed dial") }, + { alt + "1..9,0", L("Run a Speed Dial favourite") }, //DEL #ifdef __APPLE__ {"fn+⌫", L("Delete Selected")}, diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 2b4c8754c8..a6a665d3f7 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1,6 +1,7 @@ #include "MainFrame.hpp" #include <wx/panel.h> +#include <wx/textentry.h> #include <wx/notebook.h> #include <wx/listbook.h> #include <wx/simplebook.h> @@ -701,8 +702,18 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ } return;} #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; } + // Orca: open the speed dial from any page with a bare Space. Only when no modifier is held (so + // editing shortcuts like Ctrl+Shift+Space in the canvas still reach it) and while no text field + // is focused, so typing a space into the search box or a parameter value isn't hijacked. + if (!evt.CmdDown() && !evt.ShiftDown() && !evt.AltDown() && evt.GetKeyCode() == WXK_SPACE) { + wxWindow* focus = wxWindow::FindFocus(); + if (focus && dynamic_cast<wxTextEntryBase*>(focus)) { + evt.Skip(); // typing in a text field - let the space reach it + return; + } + 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.ShiftDown() && evt.GetKeyCode() == 'G') { m_plater->apply_background_progress(); @@ -3350,7 +3361,7 @@ void MainFrame::init_menubar_as_editor() "", nullptr, []() { return true; }, this, 1); parent_menu->AppendSeparator(); append_menu_item( - parent_menu, wxID_ANY, _L("Open speed dial...") + sep + ctrl_t + "K", "", + parent_menu, wxID_ANY, _L("Open speed dial...") + sep + "Space", "", [](wxCommandEvent &) { wxGetApp().open_speed_dial(); }, "", nullptr, []() { return true; }, this); //parent_menu->Insert(1, preference_item); @@ -3378,7 +3389,7 @@ void MainFrame::init_menubar_as_editor() top_menu->AppendSeparator(); append_menu_item( - top_menu, wxID_ANY, _L("Open speed dial...") + "\t" + ctrl + "K", "", + top_menu, wxID_ANY, _L("Open speed dial...") + "\t" + "Space", "", [](wxCommandEvent &) { wxGetApp().open_speed_dial(); }, "", nullptr, []() { return true; }, this); top_menu->AppendSeparator(); @@ -3522,7 +3533,7 @@ void MainFrame::init_menubar_as_editor() // On Mac, the Apple menu ignores non-standard custom items, so add Preset Bundle to the File menu fileMenu->AppendSeparator(); append_menu_item( - fileMenu, wxID_ANY, _L("Open speed dial...") + sep + ctrl_t + "K", "", + fileMenu, wxID_ANY, _L("Open speed dial...") + sep + "Space", "", [](wxCommandEvent&) { wxGetApp().open_speed_dial(); }, "", nullptr, []() { return true; }, this); append_menu_item( diff --git a/src/slic3r/GUI/ParamsPanel.cpp b/src/slic3r/GUI/ParamsPanel.cpp index a0834666ad..5a49df1280 100644 --- a/src/slic3r/GUI/ParamsPanel.cpp +++ b/src/slic3r/GUI/ParamsPanel.cpp @@ -324,7 +324,9 @@ ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, c wxID_ANY, wxDefaultPosition, wxDefaultSize, - wxVSCROLL) // hide hori-bar will cause hidden field mis-position + wxVSCROLL // hide hori-bar will cause hidden field mis-position + | wxTAB_TRAVERSAL // Allows for traversal via tab key + ) { // ShowScrollBar(GetHandle(), SB_BOTH, FALSE); Bind(wxEVT_SCROLL_CHANGED, [this](auto &e) { diff --git a/src/slic3r/GUI/Search.hpp b/src/slic3r/GUI/Search.hpp index 4ae43dbca0..859a012f46 100644 --- a/src/slic3r/GUI/Search.hpp +++ b/src/slic3r/GUI/Search.hpp @@ -148,6 +148,10 @@ public: void show_dialog(Preset::Type type, wxWindow *parent, TextInput *input, wxWindow *ssearch_btn); void dlg_sys_color_changed(); void dlg_msw_rescale(); + + // The full gated option set built by init() (after visibility/mode/printer-tech filtering). + // Used by the Speed Dial to materialise config settings as first-class actions. + const std::vector<Option>& all_options() const { return options; } }; //------------------------------------------ diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index 8a0aca5445..593d8a7344 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -11,6 +11,8 @@ #include <libslic3r/Preset.hpp> +#include <boost/nowide/convert.hpp> + #include <algorithm> #include <wx/display.h> @@ -127,9 +129,14 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload) if (command == "request_actions") { m_page_ready = true; send_actions(); - } else if (command == "toggle_favourite") - wxGetApp().action_registry().set_favourite(payload.value("id", ""), payload.value("fav", false)); - else if (command == "reorder_favourites") { + } else if (command == "toggle_favourite") { + // set_favourite() refuses once the bar hits kFavLimit; tell the page so it can undo the + // star and show a "favourites are full" hint instead of silently losing the pin. + const std::string fav_id = payload.value("id", ""); + const bool ok = wxGetApp().action_registry().set_favourite(fav_id, payload.value("fav", false)); + if (!ok) + call_web_handler({{"command", "favourite_full"}, {"limit", (int) ActionRegistry::kFavLimit}, {"id", fav_id}}); + } else if (command == "reorder_favourites") { std::vector<std::string> ids; if (payload.contains("ids") && payload["ids"].is_array()) for (const auto& id : payload["ids"]) @@ -138,21 +145,6 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload) 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 == "search_settings") - search_settings(payload.value("q", "")); else if (command == "search_tabs") search_tabs(); else if (command == "go_to_tab") { @@ -163,6 +155,27 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload) if (wxGetApp().mainframe) wxGetApp().mainframe->select_tab(from_u8(tab_id)); } + } else if (command == "setting_descriptor") { + // Inline editor: hand the page the descriptor for the setting it's editing. + const std::string id = payload.value("id", ""); + call_web_handler( + {{"command", "setting_descriptor"}, {"descriptor", wxGetApp().action_registry().setting_descriptor(id)}}); + } else if (command == "set_setting") { + // Inline editor submit. Apply the value; on success close the dialog. + const std::string id = payload.value("id", ""); + const nlohmann::json value = payload.contains("value") ? payload["value"] : nlohmann::json(nullptr); + if (id.empty() || !wxGetApp().action_registry().apply_setting(id, value)) { + call_web_handler({{"command", "apply_failed"}, {"id", id}}); + return; + } + Hide(); + } else if (command == "open_setting_in_sidebar") { + // Non-inline-editable setting (points, plugin-backed, float-or-percent): jump the sidebar. + Hide(); + const std::string opt_key = payload.value("opt_key", ""); + const std::string category = payload.value("category", ""); + if (!opt_key.empty()) + wxGetApp().sidebar().jump_to_option(opt_key, Preset::Type(payload.value("type", int(Preset::TYPE_INVALID))), boost::nowide::widen(category)); } else if (command == "resize") resize_to_content(json_int_or(payload, "height", 0)); } @@ -179,18 +192,6 @@ void SpeedDialWebDialog::search_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) { if (height <= 0) diff --git a/src/slic3r/GUI/SpeedDialDialog.hpp b/src/slic3r/GUI/SpeedDialDialog.hpp index 74e806dd85..26734dd841 100644 --- a/src/slic3r/GUI/SpeedDialDialog.hpp +++ b/src/slic3r/GUI/SpeedDialDialog.hpp @@ -22,7 +22,6 @@ private: void resize_to_content(int height); void run_action(const std::string& id, const std::string& title, const std::string& param = ""); void send_actions(); - void search_settings(const std::string& query); void search_tabs(); bool m_page_ready{false}; From 846a95374f1ca0e287e919138afbaf92dc4d64c3 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Tue, 8 Sep 2026 18:23:51 +0800 Subject: [PATCH 03/29] Fixed unit test issue. Added basic object manipulation to actions. Added calibration wizards to actions. Added View controls to actions --- src/slic3r/GUI/ActionRegistry.cpp | 257 ++++++++++++++++++++++- tests/slic3rutils/test_action_source.cpp | 4 +- 2 files changed, 258 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index 3e56c857cc..bb8c57b615 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -1,5 +1,6 @@ #include "ActionRegistry.hpp" +#include "calib_dlg.hpp" #include "GCodeViewer.hpp" #include "GLCanvas3D.hpp" #include "GUI.hpp" @@ -231,6 +232,15 @@ void go_to_layer(Plater* plater, const std::string& param) } } +// Select a named camera view ("top"/"front"/...); Plater::select_view dispatches to the current +// panel. Shared by the view_* speed-dial commands. +AppActionRunResult view_command(Plater* plater, const std::string& dir) +{ + if (plater) + plater->select_view(dir); + return {AppActionRunResult::Level::Success}; +} + // 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) @@ -267,6 +277,8 @@ AppActionRunResult run_native_command(const std::string& command_key, const std: } if (command_key == "slice_and_preview") { if (plater) { + // Actually re-slice (respects the toolbar's current plate/all selection), then show the result. + plater->reslice(); plater->select_view_3D("Preview", false); if (app.mainframe) app.mainframe->select_tab(TAB_ID_PREVIEW); @@ -286,6 +298,200 @@ AppActionRunResult run_native_command(const std::string& command_key, const std: // dispatch here is a no-op (the jump goes through the go_to_tab web command). if (command_key == "go_to_tab") return {AppActionRunResult::Level::Success}; + + // ---- Slice -> Export pipeline. Each Plater method self-guards (empty model / error / + // background-invalid) and then opens its own save dialog / show_error, mirroring the File menu. + if (command_key == "export_gcode") { + if (plater) + plater->export_gcode(false); + return {AppActionRunResult::Level::Success}; + } + if (command_key == "export_stl") { + if (plater) + plater->export_stl(); + return {AppActionRunResult::Level::Success}; + } + if (command_key == "export_3mf") { + if (plater) + plater->export_core_3mf(); + return {AppActionRunResult::Level::Success}; + } + if (command_key == "export_sliced_file") { + if (plater) + plater->export_gcode_3mf(); + return {AppActionRunResult::Level::Success}; + } + if (command_key == "export_all_sliced_file") { + if (plater) + plater->export_gcode_3mf(true); + return {AppActionRunResult::Level::Success}; + } + + // ---- Calibration wizards. Each mirrors the menu handler (MainFrame.cpp): recreate the dialog + // fresh per launch. The palette hides itself and defers dispatch off the webview callback, so a + // ShowModal() here is safe (same path as open_preferences). The 3D panel is ensured below. + auto calib = [&](auto&& open) -> AppActionRunResult { + if (!plater) + return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")}; + // Auto-switch to the Prepare (3D) view instead of prompting: set the 3D panel + // synchronously (the wizard's new_project also re-establishes it) and select the + // Prepare notebook page so the tab label matches. The palette is hidden and this + // dispatch is deferred off the webview callback, so a modal on a switched tab is safe. + if (!plater->is_view3D_shown()) { + plater->select_view_3D("3D"); + if (MainFrame* mf = wxGetApp().mainframe; mf) + mf->select_tab(TAB_ID_PREPARE); + } + open(plater); + return {AppActionRunResult::Level::Success}; + }; + if (command_key == "calib_temperature") + return calib([](Plater* p) { + Temp_Calibration_Dlg* dlg = new Temp_Calibration_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); + dlg->ShowModal(); + dlg->Destroy(); + }); + if (command_key == "calib_max_volumetric") + return calib([](Plater* p) { + MaxVolumetricSpeed_Test_Dlg* dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); + dlg->ShowModal(); + dlg->Destroy(); + }); + if (command_key == "calib_pressure_advance") + return calib([](Plater* p) { + PA_Calibration_Dlg* dlg = new PA_Calibration_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); + dlg->ShowModal(); + dlg->Destroy(); + }); + if (command_key == "calib_flow_ratio") + return calib([](Plater* p) { + FlowRateCalibrationDialog* dlg = new FlowRateCalibrationDialog((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); + dlg->ShowModal(); + dlg->Destroy(); + }); + if (command_key == "calib_retraction") + return calib([](Plater* p) { + Retraction_Test_Dlg* dlg = new Retraction_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); + dlg->ShowModal(); + dlg->Destroy(); + }); + if (command_key == "calib_cornering") + return calib([](Plater* p) { + Cornering_Test_Dlg* dlg = new Cornering_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); + dlg->ShowModal(); + dlg->Destroy(); + }); + if (command_key == "calib_input_shaping_freq") + return calib([](Plater* p) { + Input_Shaping_Freq_Test_Dlg* dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); + dlg->ShowModal(); + dlg->Destroy(); + }); + if (command_key == "calib_input_shaping_damp") + return calib([](Plater* p) { + Input_Shaping_Damp_Test_Dlg* dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); + dlg->ShowModal(); + dlg->Destroy(); + }); + if (command_key == "calib_vfa") + return calib([](Plater* p) { + VFA_Test_Dlg* dlg = new VFA_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); + dlg->ShowModal(); + dlg->Destroy(); + }); + + // ---- View controls. select_view dispatches to the current panel; named views + perspective + // toggle + fit-to-bed mirror the View menu items (MainFrame.cpp). reset_window_layout is direct. + if (command_key == "view_top") + return view_command(plater, "top"); + if (command_key == "view_bottom") + return view_command(plater, "bottom"); + if (command_key == "view_front") + return view_command(plater, "front"); + if (command_key == "view_rear") + return view_command(plater, "rear"); + if (command_key == "view_left") + return view_command(plater, "left"); + if (command_key == "view_right") + return view_command(plater, "right"); + if (command_key == "view_iso") + return view_command(plater, "iso"); + if (command_key == "view_default") { + if (plater) { + plater->select_view("plate"); + if (GLCanvas3D* canvas = plater->get_current_canvas3D()) + canvas->zoom_to_bed(); + } + return {AppActionRunResult::Level::Success}; + } + if (command_key == "view_fit_bed") { + if (plater) + if (GLCanvas3D* canvas = plater->get_current_canvas3D()) + canvas->zoom_to_bed(); + return {AppActionRunResult::Level::Success}; + } + if (command_key == "view_toggle_perspective") { + if (plater) + plater->get_camera().select_next_type(); + return {AppActionRunResult::Level::Success}; + } + if (command_key == "reset_window_layout") { + if (plater) + plater->reset_window_layout(); + return {AppActionRunResult::Level::Success}; + } + + // ---- Object / interaction operations (single-phase). Each mirrors a toolbar/menu action and is + // guarded by an existing can_* / selection check so nothing crashes on empty selection or a busy + // background worker, and returns a friendly Info instead. Structural ops self-update()/schedule a + // re-slice; transform ops (mirror/center/drop) post their own schedule-background event. We only + // need the underlying object (not a specific object index), so a non-capturing lambda is used as + // the guard/op pair below. Rotate/scale by angle/factor, duplicate (modal count dialog) and + // cut/segment/merge (unimplemented on Plater) are deliberately left out of this MVP. + auto obj = [&](bool (*ok)(Plater*), void (*op)(Plater*)) -> AppActionRunResult { + if (!plater) + return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")}; + // Object ops read the Prepare (3D) canvas selection, so ensure that view before guarding so + // a launch from the Preview/other tab doesn't report a spuriously empty selection. + if (!plater->is_view3D_shown()) { + plater->select_view_3D("3D"); + if (MainFrame* mf = wxGetApp().mainframe; mf) + mf->select_tab(TAB_ID_PREPARE); + } + if (!ok(plater)) + return {AppActionRunResult::Level::Info, _L("Select an object first.")}; + op(plater); + return {AppActionRunResult::Level::Success}; + }; + if (command_key == "obj_delete") + return obj([](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->remove_selected(); }); + if (command_key == "obj_delete_all") + return obj([](Plater* p) { return p->can_delete_all(); }, [](Plater* p) { p->delete_all_objects_from_model(); }); + if (command_key == "obj_mirror_x") + return obj([](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::X); }); + if (command_key == "obj_mirror_y") + return obj([](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Y); }); + if (command_key == "obj_mirror_z") + return obj([](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Z); }); + if (command_key == "obj_split_objects") + return obj([](Plater* p) { return p->can_split_to_objects(); }, [](Plater* p) { p->split_object(true); }); + if (command_key == "obj_split_parts") + return obj([](Plater* p) { return p->can_split_to_volumes(); }, [](Plater* p) { p->split_volume(); }); + if (command_key == "obj_center") + return obj([](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); }); + if (command_key == "obj_drop") + return obj([](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); }); + if (command_key == "obj_fit_volume") + return obj([](Plater* p) { return p->can_scale_to_print_volume(); }, [](Plater* p) { p->scale_selection_to_fit_print_volume(); }); + if (command_key == "obj_instances_up") + return obj([](Plater* p) { return p->can_increase_instances(); }, [](Plater* p) { p->increase_instances(); }); + if (command_key == "obj_instances_down") + return obj([](Plater* p) { return p->can_decrease_instances(); }, [](Plater* p) { p->decrease_instances(); }); + if (command_key == "obj_arrange") + return obj([](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->arrange(); }); + // Auto-orient has no dedicated can_*; can_arrange covers "objects exist + UI worker idle". + if (command_key == "obj_orient") + return obj([](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); }); return {AppActionRunResult::Level::Info, _L("Unknown command.")}; } @@ -315,7 +521,7 @@ 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"))); + out.push_back(make_command("slice_and_preview", _u8L("Slice and Preview"), _u8L("Slice & Export"))); // Two-phase commands: activating them collects input in the palette, then runs. Settings are // not a command here - they're materialised as first-class SettingActions (see materialize_). out.push_back(make_command("go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"), "percent")); @@ -327,6 +533,55 @@ std::vector<std::unique_ptr<AppAction>> native_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"))); + + // Slice -> Export pipeline. Each runs a public Plater method; the methods self-guard (empty + // model / error / background-invalid) and open their own save dialog / show_error. + out.push_back(make_command("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"))); + out.push_back(make_command("export_stl", _u8L("Export STL"), _u8L("Slice & Export"))); + out.push_back(make_command("export_3mf", _u8L("Export 3MF"), _u8L("Slice & Export"))); + out.push_back(make_command("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"))); + out.push_back(make_command("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"))); + + // Calibration wizards (one command per dialog mirroring the Calibration menu, MainFrame.cpp). + out.push_back(make_command("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"))); + out.push_back(make_command("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"))); + out.push_back(make_command("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"))); + out.push_back(make_command("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"))); + out.push_back(make_command("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"))); + out.push_back(make_command("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"))); + out.push_back(make_command("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"))); + out.push_back(make_command("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"))); + out.push_back(make_command("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"))); + + // View controls (mirror the View menu; most duplicate the Ctrl+0..6 shortcuts). + out.push_back(make_command("view_top", _u8L("View: Top"), _u8L("View"))); + out.push_back(make_command("view_bottom", _u8L("View: Bottom"), _u8L("View"))); + out.push_back(make_command("view_front", _u8L("View: Front"), _u8L("View"))); + out.push_back(make_command("view_rear", _u8L("View: Rear"), _u8L("View"))); + out.push_back(make_command("view_left", _u8L("View: Left"), _u8L("View"))); + out.push_back(make_command("view_right", _u8L("View: Right"), _u8L("View"))); + out.push_back(make_command("view_iso", _u8L("View: Isometric"), _u8L("View"))); + out.push_back(make_command("view_default", _u8L("View: Default"), _u8L("View"))); + out.push_back(make_command("view_fit_bed", _u8L("Fit Bed to View"), _u8L("View"))); + out.push_back(make_command("view_toggle_perspective", _u8L("Toggle Perspective"), _u8L("View"))); + out.push_back(make_command("reset_window_layout", _u8L("Reset Window Layout"), _u8L("View"))); + + // Object operations (single-phase). Each maps to a public Plater method guarded by a can_* / + // selection check in run_native_command; structural ops self-update()/schedule re-slice. + out.push_back(make_command("obj_delete", _u8L("Delete Selected"), _u8L("Object"))); + out.push_back(make_command("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"))); + out.push_back(make_command("obj_mirror_x", _u8L("Mirror X"), _u8L("Object"))); + out.push_back(make_command("obj_mirror_y", _u8L("Mirror Y"), _u8L("Object"))); + out.push_back(make_command("obj_mirror_z", _u8L("Mirror Z"), _u8L("Object"))); + out.push_back(make_command("obj_split_objects", _u8L("Split to Objects"), _u8L("Object"))); + out.push_back(make_command("obj_split_parts", _u8L("Split to Parts"), _u8L("Object"))); + out.push_back(make_command("obj_center", _u8L("Center Selected on Plate"), _u8L("Object"))); + out.push_back(make_command("obj_drop", _u8L("Drop to Bed"), _u8L("Object"))); + out.push_back(make_command("obj_fit_volume", _u8L("Scale to Fit Print Volume"), _u8L("Object"))); + out.push_back(make_command("obj_instances_up", _u8L("Increase Instances"), _u8L("Object"))); + out.push_back(make_command("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object"))); + out.push_back(make_command("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object"))); + out.push_back(make_command("obj_orient", _u8L("Auto-Orient"), _u8L("Object"))); return out; } diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index b6721e22d6..8687115998 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -19,7 +19,7 @@ class TestAppAction final : public AppAction public: TestAppAction() : AppAction("test", "Action title", "src-key", "Action source") {} - AppActionRunResult run() const override { return {}; } + AppActionRunResult run(const std::string& param = {}) const override { return {}; } }; } // namespace @@ -33,7 +33,7 @@ TEST_CASE("AppAction composes a stable id from prefix:title:source_key", "[speed TEST_CASE("AppAction definitions are immutable after construction", "[speeddial][actions]") { - using StringAccessor = const std::string& (AppAction::*)() const; + using StringAccessor = const std::string& (AppAction::*) () const; STATIC_CHECK(std::is_same_v<decltype(&AppAction::id), StringAccessor>); STATIC_CHECK(std::is_same_v<decltype(&AppAction::title), StringAccessor>); From cae23a933e92d072b2fa2c69bac3a173e073bf93 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Wed, 9 Sep 2026 11:29:18 +0800 Subject: [PATCH 04/29] Plate controls --- src/slic3r/GUI/ActionRegistry.cpp | 173 ++++++++++++++++++++++- src/slic3r/GUI/ActionRegistry.hpp | 5 + src/slic3r/GUI/Plater.cpp | 31 ++-- src/slic3r/GUI/Plater.hpp | 3 + tests/slic3rutils/test_action_source.cpp | 8 ++ 5 files changed, 207 insertions(+), 13 deletions(-) diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index bb8c57b615..ef31401909 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -10,6 +10,7 @@ #include "MainFrame.hpp" #include "Notebook.hpp" #include "Plater.hpp" +#include "PlateSettingsDialog.hpp" #include "Search.hpp" #include "Tab.hpp" #include "slic3r/plugin/PluginManager.hpp" @@ -33,6 +34,7 @@ #include <cmath> #include <cstdlib> #include <ctime> +#include <exception> #include <fstream> #include <iterator> #include <string> @@ -142,6 +144,7 @@ constexpr const char* kCommandPrefix = "orca_command"; constexpr const char* kOrcaSourceKey = "orca"; constexpr const char* kOrcaSourceName = "OrcaSlicer"; constexpr const char* kSettingPrefix = "orca_setting"; +constexpr const char* kPlateGotoPrefix = "orca_plate_goto"; // Display context for a setting action's eyebrow, e.g. the "Process" in "Process : Quality : Layers". // Keyed by the option's preset type so the palette reads like the settings sidebar tabs. @@ -492,6 +495,80 @@ AppActionRunResult run_native_command(const std::string& command_key, const std: // Auto-orient has no dedicated can_*; can_arrange covers "objects exist + UI worker idle". if (command_key == "obj_orient") return obj([](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); }); + + // ---- Plate management. Plates are a filament (FFF) feature: SLA builds a single plate with + // no plate UI, and gcode-only mode has no editable project - so gate every plate op on FFF + + // the normal editor (mirroring where the plate toolbar/menu live). These act on the CURRENT + // plate (delete/duplicate take -1) except plate_goto, which jumps to the index in `param`. + auto plate_plater = [&]() -> Plater* { + return (plater && plater->printer_technology() == ptFFF && !plater->only_gcode_mode()) ? plater : nullptr; + }; + const AppActionRunResult plate_unavailable{AppActionRunResult::Level::Info, _L("Plates are a filament (FFF) feature.")}; + + if (command_key == "plate_add") { + if (Plater* p = plate_plater(); p) { + if (!p->can_add_plate()) + return {AppActionRunResult::Level::Info, _L("Cannot add another plate (maximum reached).")}; + p->add_plate(); + return {AppActionRunResult::Level::Success}; + } + return plate_unavailable; + } + if (command_key == "plate_duplicate") { + if (Plater* p = plate_plater(); p) { + if (!p->can_add_plate()) + return {AppActionRunResult::Level::Info, _L("Cannot duplicate a plate (maximum reached).")}; + p->duplicate_plate(); + return {AppActionRunResult::Level::Success}; + } + return plate_unavailable; + } + if (command_key == "plate_delete") { + if (Plater* p = plate_plater(); p) { + if (!p->can_delete_plate()) + return {AppActionRunResult::Level::Info, _L("Cannot delete the only plate.")}; + p->delete_plate(); + return {AppActionRunResult::Level::Success}; + } + return plate_unavailable; + } + if (command_key == "plate_rename") { + if (Plater* p = plate_plater(); p) { + PartPlate* curr = p->get_partplate_list().get_curr_plate(); + PlateNameEditDialog dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, _L("Edit Plate Name")); + dlg.set_plate_name(from_u8(curr->get_plate_name())); + if (dlg.ShowModal() == wxID_YES) + curr->set_plate_name(dlg.get_plate_name().ToUTF8().data()); + return {AppActionRunResult::Level::Success}; + } + return plate_unavailable; + } + if (command_key == "plate_toggle_lock") { + if (Plater* p = plate_plater(); p) { + PartPlateList& plates = p->get_partplate_list(); + const int index = plates.get_curr_plate_index(); + p->take_snapshot("lock partplate"); + plates.lock_plate(index, !plates.is_locked(index)); + return {AppActionRunResult::Level::Success}; + } + return plate_unavailable; + } + if (command_key == "plate_goto") { + if (Plater* p = plate_plater(); p) { + PartPlateList& plates = p->get_partplate_list(); + const int count = plates.get_plate_count(); + if (count <= 0) + return {AppActionRunResult::Level::Info, _L("No plates available.")}; + int index = 0; + try { + index = std::stoi(param); + } catch (const std::exception&) {} + index = std::clamp(index, 0, count - 1); + p->select_plate(index, false); + return {AppActionRunResult::Level::Success}; + } + return plate_unavailable; + } return {AppActionRunResult::Level::Info, _L("Unknown command.")}; } @@ -515,6 +592,29 @@ struct CommandAction : AppAction 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)); } +// A dynamic "Go to Plate N" action, one per live plate, rebuilt on every snapshot() (so a +// rename/move immediately shows up). id is keyed by plate index, NOT the display title, so +// renaming a plate never re-keys it - the same contract as SettingAction. A pinned "Go to +// Plate N" whose plate is deleted simply stops resolving (visibleFavourites drops dead pins). +struct PlateAction : AppAction +{ + int plate_index; + + static std::string id_for(int index) + { return AppAction::compose_id(kPlateGotoPrefix, std::to_string(index), kOrcaSourceKey); } + + PlateAction(int index, std::string title, std::string source_name) + : AppAction(AppActionId{id_for(index)}, std::move(title), kOrcaSourceKey, std::move(source_name)) + , plate_index(index) + { + this->kind = AppActionKind::Command; + this->group = _u8L("Plate"); + } + + AppActionRunResult run(const std::string& /*param*/) const override + { return run_native_command("plate_goto", std::to_string(plate_index)); } +}; + // The built-in palette commands, registered once at init(). std::vector<std::unique_ptr<AppAction>> native_commands() { @@ -582,6 +682,14 @@ std::vector<std::unique_ptr<AppAction>> native_commands() out.push_back(make_command("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object"))); out.push_back(make_command("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object"))); out.push_back(make_command("obj_orient", _u8L("Auto-Orient"), _u8L("Object"))); + + // Plate management. These act on the CURRENT plate (like Plater::delete_plate(-1)); the + // per-plate "Go to Plate N" actions are dynamic and materialised in materialize_plate_actions(). + out.push_back(make_command("plate_add", _u8L("Add Plate"), _u8L("Plate"))); + out.push_back(make_command("plate_duplicate", _u8L("Duplicate Plate"), _u8L("Plate"))); + out.push_back(make_command("plate_delete", _u8L("Delete Plate"), _u8L("Plate"))); + out.push_back(make_command("plate_rename", _u8L("Rename Plate"), _u8L("Plate"))); + out.push_back(make_command("plate_toggle_lock", _u8L("Toggle Plate Lock"), _u8L("Plate"))); return out; } @@ -1195,6 +1303,65 @@ void ActionRegistry::materialize_setting_actions() } } +void ActionRegistry::materialize_plate_actions() +{ + assert(wxThread::IsMain()); + + // Plates are a filament (FFF) feature: SLA has a single plate and no plate UI, and gcode-only + // mode has no editable project - so no "Go to Plate N" actions are offered there. + Plater* plater = wxTheApp ? wxGetApp().plater() : nullptr; + if (!plater || plater->printer_technology() != ptFFF || plater->only_gcode_mode()) { + // Drop any stale plate actions (e.g. the printer technology switched to SLA). + for (auto it = m_actions.begin(); it != m_actions.end();) { + if (it->first.rfind(kPlateGotoPrefix, 0) == 0) + it = m_actions.erase(it); + else + ++it; + } + return; + } + + // Persisted per-action state, read ONCE (mirrors materialize_setting_actions) so a relisted + // "Go to Plate N" keeps its recency/favourite when the plate is renamed - the id is index-keyed. + nlohmann::json stats = read_section("stats", nlohmann::json::object()); + if (!stats.is_object()) + stats = nlohmann::json::object(); + const std::vector<std::string> favs = favourite_ids(); + + const std::vector<PartPlate*>& list = plater->get_partplate_list().get_plate_list(); + std::unordered_set<std::string> seen; + for (size_t i = 0; i < list.size(); ++i) { + PartPlate* plate = list[i]; + if (!plate) + continue; + const std::string id = PlateAction::id_for(int(i)); + seen.insert(id); + + // "Go to Plate N" + " (name)" when the plate is named, matching the object-list label. + std::string title(_u8L("Go to Plate")); + title += " " + std::to_string(i + 1); + const std::string name = plate->get_plate_name(); + if (!name.empty()) + title += " (" + name + ")"; + + auto action = std::make_unique<PlateAction>(int(i), title, kOrcaSourceName); + action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); + if (auto it = stats.find(id); it != stats.end() && it->is_object()) { + action->count = it->value("count", 0); + action->last = it->value("last", 0LL); + } + m_actions.insert_or_assign(action->id(), std::shared_ptr<AppAction>(std::move(action))); + } + + // Drop plate actions whose index no longer exists (a plate was deleted / moved to the front). + for (auto it = m_actions.begin(); it != m_actions.end();) { + if (it->first.rfind(kPlateGotoPrefix, 0) == 0 && !seen.count(it->first)) + it = m_actions.erase(it); + else + ++it; + } +} + bool ActionRegistry::should_ask(const std::string& id) const { assert(wxThread::IsMain()); @@ -1216,9 +1383,11 @@ void ActionRegistry::suppress_ask(const std::string& id) nlohmann::json ActionRegistry::snapshot() { assert(wxThread::IsMain()); - // Settings are first-class actions; make sure the current visible option set is materialised - // before we serialise the pool (tabs_list is built by the time the palette opens). + // Settings and plates are first-class actions; make sure the current visible option set and the + // live plate list are materialised before we serialise the pool (tabs_list is built by the time + // the palette opens). materialize_setting_actions(); + materialize_plate_actions(); std::vector<const AppAction*> sorted; sorted.reserve(m_actions.size()); diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index 1c481c1da9..d7bd1fa583 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -192,6 +192,11 @@ private: // Called at the top of snapshot() so the palette always reflects the current configs. void materialize_setting_actions(); + // (Re)materialise one "Go to Plate N" action per live plate, so the palette lists every plate + // directly on each spawn (no second-phase picker). FFF-editor only; SLA/gcode modes have no + // plate UI, so nothing is materialised and stale ids are dropped. Called at the top of snapshot(). + void materialize_plate_actions(); + // Loader callbacks (marshalled to the UI thread) land here. refresh_source rebuilds // one plugin's whole action set; refresh_capability touches a single capability. void refresh_source(const std::string& plugin_key, ActionChange change); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2f992872e4..c525896d0b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -12610,17 +12610,8 @@ void Plater::priv::on_action_add(SimpleEvent&) //BBS: add plate from toolbar void Plater::priv::on_action_add_plate(SimpleEvent&) { - if (q != nullptr) { - take_snapshot("add partplate"); - this->partplate_list.create_plate(); - int new_plate = this->partplate_list.get_plate_count() - 1; - this->partplate_list.select_plate(new_plate); - update(); - - // BBS set default view - //q->get_camera().select_view("topfront"); - q->get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; - } + if (q != nullptr) + q->add_plate(); } //BBS: remove plate from toolbar @@ -21492,6 +21483,24 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi return ret; } +//BBS: add an empty plate and switch to it (mirrors the toolbar's Add Plate). +int Plater::add_plate() +{ + if (!p->can_add_plate()) + return -1; + take_snapshot("add partplate"); + int new_plate = p->partplate_list.create_plate(); + if (new_plate < 0) + return new_plate; + p->partplate_list.select_plate(new_plate); + update(); + + // BBS set default view + //get_camera().select_view("topfront"); + p->camera.requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; + return new_plate; +} + int Plater::duplicate_plate(int plate_index) { int index = plate_index, ret; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 5308deec61..fd190d3832 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -775,6 +775,9 @@ public: void apply_background_progress(); //BBS: select the plate by hover_id int select_plate_by_hover_id(int hover_id, bool right_click = false, bool isModidyPlateName = false); + //BBS: add an empty plate and switch to it (the toolbar's Add Plate). Returns the new + // plate index, or -1 when the plate cap is reached. + int add_plate(); //BBS: delete the plate, index= -1 means the current plate int delete_plate(int plate_index = -1); int duplicate_plate(int plate_index = -1); diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index 8687115998..e5255f61fe 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -53,3 +53,11 @@ TEST_CASE("ActionRegistry takes exclusive ownership of published actions", "[spe STATIC_CHECK(std::is_same_v<decltype(&ActionRegistry::upsert), ExpectedUpsert>); } + +// A dynamic "Go to Plate N" action is keyed by plate index (not the display title), so renaming +// a plate never re-keys it - the same contract as a setting action. +TEST_CASE("Go-to-plate actions are keyed by index, not title", "[speeddial][actions]") +{ + CHECK(AppAction::compose_id("orca_plate_goto", "0", "orca") == "orca_plate_goto:0:orca"); + CHECK(AppAction::compose_id("orca_plate_goto", "2", "orca") == "orca_plate_goto:2:orca"); +} From c3a21fa0d416e1d8c90b16059d1ab7a09212bf47 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Wed, 9 Sep 2026 12:26:09 +0800 Subject: [PATCH 05/29] Switched back to jump to setting instead --- resources/web/dialog/SpeedDial/speeddial.js | 505 +----------------- .../web/dialog/SpeedDial/speeddial.test.js | 69 --- resources/web/dialog/SpeedDial/style.css | 142 ----- src/slic3r/GUI/ActionRegistry.cpp | 369 +------------ src/slic3r/GUI/ActionRegistry.hpp | 10 - src/slic3r/GUI/SpeedDialDialog.cpp | 25 - 6 files changed, 11 insertions(+), 1109 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 65d2e24430..78ba8a138d 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -37,18 +37,6 @@ var tabOptions = []; // [{id,title}] - notebook pages, fetched on entering // element handles, assigned in OnInit (kept null so load-time touches no DOM) var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, countEl = null, headEl = null; -// ---- inline setting editor state --------------------------------------------- -// The "setting" phase (opened by activating a setting action) replaces the list with an editor card -// for one option. phase transitions: commands -> setting -> (apply / open-in-sidebar) -> closed, or -// Esc back to commands. settingDesc is the C++ descriptor; settingRows are the per-index control -// descriptors (1 row for scalars, one per index for vectors); settingFieldEls hold the live controls. -var settingId = ""; // the setting action id being edited -var settingDesc = null; // {id,opt_key,type,title,breadcrumb,category,unit,tooltip,editable,control,cardinality,value|values,index_labels,enum_options,min,max,is_int} -var settingRows = []; // [{index,kind,value,label,enum_options,min,max,unit,is_int}] -var settingFieldEls = []; // [Element...] parallel to settingRows -var settingPreviewIcon = null; // <img> beside the editor title, updated live on dropdown pick -var openDropDownEl = null; // the custom dropdown toggle button whose option list is expanded - // ---- pure helpers (no DOM; unit-tested) ------------------------------------- // Pre-normalized haystacks, cached on the action object. The fold is length-preserving (1:1 per // char) so the ranges FuzzyRangesNorm returns slice the ORIGINAL title/source text correctly. The @@ -176,26 +164,6 @@ function shouldRenderActionList(query) { return !!((query || "").trim()); } -// Label for a closed-enum entry, looked up from its enum_options by value; falls back to the value. -// Pure so the node-vm test can exercise the dropdown label mapping. `value` is the current int value. -function dropdownLabel(options, value) { - var want = String(value == null ? "" : value); - for (var i = 0; i < (options || []).length; i++) - if (String(options[i].value) === want) - return options[i].label != null && options[i].label !== "" ? String(options[i].label) : String(options[i].key != null ? options[i].key : options[i].value); - return want; -} - -// Pure: the data:URI pictogram for a dropdown's selected value, or "" when none of the options has -// one (most settings have no pattern icon). Mirrors dropdownLabel so tests can drive it without DOM. -function dropdownIcon(options, value) { - var want = String(value == null ? "" : value); - for (var i = 0; i < (options || []).length; i++) - if (String(options[i].value) === want && options[i].icon) - return options[i].icon; - return ""; -} - // Put an action's pattern pictogram into a tile (search row or favourites tile) when it has one, // otherwise fall back to the monogram. Toggles the has-icon class so CSS neutralises the hue. function fillTile(tile, a) { @@ -213,83 +181,6 @@ function fillTile(tile, a) { } } -// Per-index control descriptors for the inline setting editor, derived from the C++ descriptor. -// Pure so the node-vm test can exercise the scalar/vector + control mapping without a DOM. -// Values are the current config value(s); vector options get one row per index, each labelled. -function settingControlRows(desc) { - if (!desc || !desc.editable) return []; - var rows = []; - var values = desc.cardinality === "vector" ? (desc.values || []) : [desc.value]; - var labels = desc.cardinality === "vector" ? (desc.index_labels || []) : []; - for (var i = 0; i < values.length; i++) { - rows.push({ - index: i, - kind: desc.control, - value: values[i], - label: labels[i] != null ? String(labels[i]) : (desc.cardinality === "vector" ? String(i + 1) : null), - enum_options: desc.enum_options || [], - min: typeof desc.min === "number" ? desc.min : null, - max: typeof desc.max === "number" ? desc.max : null, - unit: desc.unit || "", - is_int: !!desc.is_int - }); - } - return rows; -} - -// Pure: read the value a control would submit back for a setting. `el` is a DOM element (never -// passed in tests). Returns undefined for an unusable value (empty/invalid number, out of range), -// boolean for toggles, number for numeric, string otherwise. -function settingControlValue(row, el) { - if (!row || !el) return undefined; - switch (row.kind) { - case "toggle": return !!el.checked; - case "number": { - var raw = String(el.value || "").trim(); - if (raw === "") return undefined; - var n = row.is_int ? parseInt(raw, 10) : parseFloat(raw); - if (!isFinite(n)) return undefined; - if (row.min != null && n < row.min) return undefined; - if (row.max != null && n > row.max) return undefined; - return n; - } - case "dropdown": { - // value is stored on the toggle button's dataset (set when an option is picked). - var v = parseInt(el.dataset ? el.dataset.value : "", 10); - return isFinite(v) ? v : undefined; - } - case "combo": { - // Open enum: free text field (never a select), so read it as the seeded integer value. - var v = parseInt(el.value, 10); - return isFinite(v) ? v : undefined; - } - case "color": - case "text": - case "percent": { - // percent submission is a string ("10%", "0.5"); C++ parses + clamps it. Empty is invalid. - var raw = String(el.value || "").trim(); - return raw === "" ? undefined : raw; - } - default: return undefined; - } -} - -// Pure: assemble the value payload for a setting from its edited control rows. Returns the scalar -// for scalar settings, an array for vector settings, or undefined when any control is invalid. -function settingCollectedValue(desc, rows, values) { - if (!desc || !desc.editable) return undefined; - if (desc.cardinality === "vector") { - var out = []; - for (var i = 0; i < rows.length; i++) { - var v = settingControlValue(rows[i], values[i]); - if (v === undefined) return undefined; - out.push(v); - } - return out; - } - return settingControlValue(rows[0], values[0]); -} - // The active list for the main phase. A typed query ranks every action (commands/plugins/settings) // by relevance; an empty query shows the recent list (recents are a mixed bag - no discrimination). function commandList(actions, recents, query) { @@ -439,13 +330,9 @@ window.HandleStudio = function (payload) { lastResizeHeight = next.lastResizeHeight; phase = next.phase; tabOptions = next.tabOptions; - // Reset any half-open setting editor (the dialog was closed/reopened), restoring the search. - settingId = ""; settingDesc = null; settingRows = []; settingFieldEls = []; - settingPreviewIcon = null; - openDropDownEl = null; - // why: builtKey caches phase|query so renderCommandsList can skip a rebuild on arrow-nav. It - // survives an apply-then-reopen (which never goes through exitPhase), so without a reset the - // leftover editor card would be mistaken for the empty-query commands list and never rebuilt. + // why: builtKey caches phase|query so renderCommandsList can skip a rebuild on arrow-nav. + // It survives a re-open (which never goes through exitPhase), so without a reset the cached + // empty-query key would skip the rebuild and leave stale list content. builtKey = ""; if (headEl) headEl.hidden = false; if (qEl) { @@ -455,16 +342,6 @@ window.HandleStudio = function (payload) { } render({ resize: true, resetScroll: true }); focusInput(); - } else if (payload.command === "setting_descriptor") { - // Inline editor loaded: render the card. Guard against a stale response for a different id. - if (payload.descriptor && payload.descriptor.id === settingId) - settingDesc = payload.descriptor; - render({ resize: true }); - // why: keyboard focus must land on the field after the card is built, not stay on the hidden - // search input. fire on a timeout so the element is attached and its content selectable. - focusSettingEditor(); - } else if (payload.command === "apply_failed") { - flashHint("Couldn't apply that value"); } else if (payload.command === "tab_results") { tabOptions = payload.tabs || []; if (sel.zone === "list") @@ -649,7 +526,6 @@ function updateFavEyebrow(favs) { eyeEl.hidden = !a; } -// One settings row; has no star/tile because settings aren't pinnable. // A command/action row - used for search results, recents, and (because settings are actions now) // the setting options too. All rows are pinnable, so every row carries a star. function renderActionRow(a, i) { @@ -864,349 +740,11 @@ function renderPercentList() { listEl.appendChild(ph); } -// ---- inline setting editor (DOM stage) --------------------------------------- - -// Collapse every open custom dropdown except `keep` (null collapses all). The menu list elements -// are the .ed-dropdown-menu siblings of the toggle buttons we track via openDropDownEl. -function closeOtherDropDowns(keep) { - if (openDropDownEl && openDropDownEl !== keep && openDropDownEl.parentNode) { - var m = openDropDownEl.parentNode.querySelector(".ed-dropdown-menu"); - if (m) m.hidden = true; - openDropDownEl.classList.remove("open"); - } - if (!keep) - openDropDownEl = null; -} - -// Collapse the currently open dropdown, if any (kept for the editor's export/import-adjacent helpers). -function closeEditorDropDown() { closeOtherDropDowns(null); } - -// Place an open dropdown menu as a fixed overlay just under its toggle, so the menu floats over the -// card (never resizing it) and is clamped to the popup's bottom edge with an internal scrollbar for -// long option lists. position:fixed escapes the card/launcher overflow clipping that an absolute -// menu would otherwise hit, keeping every option reachable within the window. -function positionDropDownMenu(btn, menu) { - var lrect = (document.querySelector(".launcher") || { getBoundingClientRect: function () { return { top: 0, bottom: window.innerHeight }; } }).getBoundingClientRect(); - var rect = btn.getBoundingClientRect(); - // Available room above and below the toggle, within the popup. Opening the menu must not push it - // past the window edge (that's the unreachable-overflow bug) - pick whichever side has more room - // and clamp the box to it. Overflow-y:auto scrolls any long list inside the menu itself. - var spaceBelow = lrect.bottom - (rect.bottom + 8); - var spaceAbove = (rect.top - 8) - lrect.top; - var openUp = spaceBelow < spaceAbove; - var maxH = Math.max(0, Math.min(openUp ? spaceAbove : spaceBelow, 200)); - menu.style.position = "fixed"; - menu.style.width = rect.width + "px"; - menu.style.left = rect.left + "px"; - menu.style.maxHeight = maxH + "px"; - if (openUp) { - // bottom edge sits just above the toggle; the box grows upward to content height. - menu.style.top = "auto"; - menu.style.bottom = (lrect.bottom - rect.top + 4) + "px"; - } else { - menu.style.top = (rect.bottom + 4) + "px"; - menu.style.bottom = "auto"; - } -} - -// Build the control element for one row (toggle/number/dropdown/combo/text/color) and seed it with -// the current value. Returns {el, node, extra} - node is what is appended, extra carries a datalist. -function settingInputFor(row) { - var el; - if (row.kind === "toggle") { - el = document.createElement("input"); - el.type = "checkbox"; - el.checked = !!row.value; - var sw = document.createElement("label"); - sw.className = "ed-switch"; - sw.appendChild(el); - var slider = document.createElement("span"); - slider.className = "ed-slider"; - sw.appendChild(slider); - return { el: el, node: sw }; - } - if (row.kind === "number") { - el = document.createElement("input"); - el.type = "number"; - el.step = row.is_int ? 1 : "any"; - if (row.min != null) el.min = row.min; - if (row.max != null) el.max = row.max; - if (row.value != null && row.value !== "") el.value = row.value; - return { el: el, node: el }; - } - if (row.kind === "dropdown") { - // Native <select> popups are unreliable inside this wxWebView (a click synthesizes a - // keydown that can reach the global Enter handler and apply+close). Build a custom - // dropdown: a toggle button that expands an in-flow option list. Selection only updates - // local state; nothing applies until Enter/Apply. The value lives on the toggle button's - // dataset so settingControlValue can read it back without the DOM copy. - var wrap = document.createElement("div"); - wrap.className = "ed-dropdown"; - var btn = document.createElement("button"); - btn.type = "button"; - btn.className = "ed-dropdown-toggle"; - btn.dataset.value = row.value != null ? String(row.value) : ""; - // The selected value's pattern pictogram (hidden when the value has none). - var toggleIcon = document.createElement("img"); - toggleIcon.className = "ed-dropdown-icon"; - toggleIcon.setAttribute("aria-hidden", "true"); - toggleIcon.alt = ""; - var tIcon = dropdownIcon(row.enum_options || [], row.value); - toggleIcon.src = tIcon || ""; - toggleIcon.hidden = !tIcon; - btn.appendChild(toggleIcon); - var label = document.createElement("span"); - label.className = "ed-dropdown-label"; - label.textContent = dropdownLabel(row.enum_options || [], row.value); - btn.appendChild(label); - var caret = document.createElement("span"); - caret.className = "ed-dropdown-caret"; - caret.textContent = "▾"; - btn.appendChild(caret); - var listEl = document.createElement("div"); - listEl.className = "ed-dropdown-menu"; - listEl.hidden = true; - (row.enum_options || []).forEach(function (o, oi) { - var opt = document.createElement("button"); - opt.type = "button"; - opt.className = "ed-dropdown-option"; - if (o.icon) { - var img = document.createElement("img"); - img.className = "ed-option-icon"; - img.src = o.icon; - img.alt = ""; - img.setAttribute("aria-hidden", "true"); - opt.appendChild(img); - } - var optLabel = document.createElement("span"); - optLabel.className = "ed-option-label"; - optLabel.textContent = o.label; - opt.appendChild(optLabel); - if (String(o.value) === String(row.value)) - opt.classList.add("sel"); - opt.onclick = function (ev) { - ev.stopPropagation(); - btn.dataset.value = String(o.value); - label.textContent = o.label; - toggleIcon.src = o.icon || ""; - toggleIcon.hidden = !o.icon; - listEl.hidden = true; - btn.classList.remove("open"); - openDropDownEl = null; - onSettingValueChanged(settingDesc, o); - }; - listEl.appendChild(opt); - }); - btn.onclick = function (ev) { - ev.stopPropagation(); - if (openDropDownEl === btn) { - // clicking the open toggle closes it - listEl.hidden = true; - btn.classList.remove("open"); - openDropDownEl = null; - return; - } - closeOtherDropDowns(null); // collapse any other open dropdown - positionDropDownMenu(btn, listEl); - listEl.hidden = false; - btn.classList.add("open"); - openDropDownEl = btn; - }; - wrap.appendChild(btn); - wrap.appendChild(listEl); - return { el: btn, node: wrap }; - } - if (row.kind === "combo") { - el = document.createElement("input"); - el.type = "text"; - var dl = document.createElement("datalist"); - el.setAttribute("list", dl.id = "ed-combo-" + row.index); - (row.enum_options || []).forEach(function (o) { - var op = document.createElement("option"); - op.value = o.value; - op.textContent = o.label; - dl.appendChild(op); - }); - el.value = row.value != null ? String(row.value) : ""; - return { el: el, node: el, extra: dl }; - } - if (row.kind === "color") { - el = document.createElement("input"); - el.type = "color"; - el.value = row.value && /^#[0-9a-fA-F]{6}$/.test(row.value) ? row.value : "#000000"; - return { el: el, node: el }; - } - if (row.kind === "percent") { - // "mm or %" (coFloatOrPercent/coFloatsOrPercents): a free-text field showing the serialized - // value (e.g. "10%" or "0.5"). The unit hints at the sidebar semantics (mm or %), so it's - // not shown here - the value itself carries the % when applicable. - el = document.createElement("input"); - el.type = "text"; - el.value = row.value != null ? String(row.value) : ""; - return { el: el, node: el }; - } - // text - el = document.createElement("input"); - el.type = "text"; - el.value = row.value != null ? String(row.value) : ""; - return { el: el, node: el }; -} - -// One labeled control row in the editor card. -function renderControlRow(row, i) { - var wrap = document.createElement("div"); - wrap.className = "editor-row"; - if (row.label != null) { - var lab = document.createElement("label"); - lab.className = "editor-label"; - lab.textContent = row.label; - wrap.appendChild(lab); - } - var ctrl = settingInputFor(row); - if (ctrl.extra) - wrap.appendChild(ctrl.extra); // datalist for open-enum combos - wrap.appendChild(ctrl.node); - if (row.unit) { - var unit = document.createElement("span"); - unit.className = "editor-unit"; - unit.textContent = row.unit; - wrap.appendChild(unit); - } - settingFieldEls[i] = ctrl.el; - return wrap; -} - -// Render the editor card into listEl (phase === "setting"). Keeps the search head hidden so the -// card owns the layout. -function renderSettingStage() { - listEl.innerHTML = ""; - listEl.className = "dial-list setting"; - if (countEl) countEl.hidden = true; - if (!settingDesc || !settingDesc.opt_key) { - var ph = document.createElement("div"); - ph.className = "dial-empty"; - ph.textContent = "Loading…"; - listEl.appendChild(ph); - return; - } - var card = document.createElement("div"); - card.className = "dial-editor"; - if (settingDesc.breadcrumb) { - var crumb = document.createElement("div"); - crumb.className = "row-eyebrow"; - crumb.textContent = settingDesc.breadcrumb; - card.appendChild(crumb); - } - var titleRow = document.createElement("div"); - titleRow.className = "editor-title-row"; - var titleIcon = document.createElement("img"); - titleIcon.className = "editor-preview-icon"; - titleIcon.setAttribute("aria-hidden", "true"); - titleIcon.alt = ""; - var pIcon = dropdownIcon(settingDesc.enum_options || [], settingDesc.value); - titleIcon.src = pIcon || ""; - titleIcon.hidden = !pIcon; - titleRow.appendChild(titleIcon); - settingPreviewIcon = titleIcon; - var title = document.createElement("div"); - title.className = "editor-title"; - title.textContent = settingDesc.title || ""; - titleRow.appendChild(title); - card.appendChild(titleRow); - if (settingDesc.tooltip) { - var tt = document.createElement("div"); - tt.className = "editor-tooltip"; - tt.textContent = settingDesc.tooltip; - card.appendChild(tt); - } - - var actions = document.createElement("div"); - actions.className = "editor-actions"; - if (settingDesc.editable) { - settingRows = settingControlRows(settingDesc); - settingFieldEls = []; - if (settingRows.length) { - settingRows.forEach(function (row, i) { card.appendChild(renderControlRow(row, i)); }); - } else { - var empty = document.createElement("div"); - empty.className = "dial-empty"; - empty.textContent = "Nothing editable here"; - card.appendChild(empty); - } - var apply = document.createElement("button"); - apply.className = "ed-btn ed-btn-primary"; - apply.textContent = "Apply"; - apply.onclick = applySetting; - actions.appendChild(apply); - } else { - var ro = document.createElement("div"); - ro.className = "editor-readonly"; - ro.textContent = "This setting can't be edited here"; - card.appendChild(ro); - var open = document.createElement("button"); - open.className = "ed-btn"; - open.textContent = "Open in sidebar"; - open.onclick = openSettingInSidebar; - actions.appendChild(open); - } - var cancel = document.createElement("button"); - cancel.className = "ed-btn"; - cancel.textContent = "Cancel"; - cancel.onclick = exitPhase; - actions.appendChild(cancel); - card.appendChild(actions); - var hint = document.createElement("div"); - hint.className = "editor-hint"; - hint.textContent = "Enter to apply · Esc to cancel"; - card.appendChild(hint); - listEl.appendChild(card); -} - -function applySetting() { - if (!settingDesc || !settingDesc.editable) return; - var value = settingCollectedValue(settingDesc, settingRows, settingFieldEls); - if (value === undefined) { - flashHint("Enter a valid value"); - return; - } - SendMessage({ command: "set_setting", id: settingId, value: value }); -} - -function openSettingInSidebar() { - if (!settingDesc) return; - SendMessage({ command: "open_setting_in_sidebar", opt_key: settingDesc.opt_key, type: settingDesc.type, category: settingDesc.category || "" }); -} - -// When a dropdown option is picked, mirror its pattern pictogram onto the editor title's preview so -// the current selection is visible without opening the menu. Non-enum / icon-less rows no-op. -function onSettingValueChanged(desc, option) { - if (!settingPreviewIcon) return; - var icon = (option && option.icon) || ""; - settingPreviewIcon.src = icon; - settingPreviewIcon.hidden = !icon; -} - -function enterSettingPhase(a) { - if (!a) return; - phase = "setting"; - query = ""; qEl.value = ""; syncClearButton(); - sel = { zone: "list", i: 0 }; - settingId = a.id; - settingDesc = null; - settingRows = []; - settingFieldEls = []; - if (headEl) headEl.hidden = true; - render({ resetScroll: true }); - SendMessage({ command: "setting_descriptor", id: a.id }); -} - function renderList() { if (phase === "tab") renderTabList(); else if (phase === "percent") renderPercentList(); - else if (phase === "setting") - renderSettingStage(); else renderCommandsList(); } @@ -1284,7 +822,6 @@ function activateEntry(a) { if (!a) return; if (a.input === "percent") { enterPercentPhase(); return; } if (a.input === "tab") { enterTabsPhase(); return; } - if (a.input === "setting") { enterSettingPhase(a); return; } run(a); } @@ -1337,14 +874,11 @@ function enterTabsPhase() { function exitPhase() { phase = "commands"; tabOptions = []; query = ""; qEl.value = ""; - settingId = ""; settingDesc = null; settingRows = []; settingFieldEls = []; - settingPreviewIcon = null; - closeOtherDropDowns(null); if (headEl) headEl.hidden = false; sel = { zone: "list", i: 0 }; // why: builtKey caches phase|query so renderCommandsList can skip a rebuild on arrow-nav/click. - // Leftover from the setting phase it matches the (empty-query) commands key, which would skip - // the rebuild and leave the editor card in the list. Reset it so the commands view is rebuilt. + // It survives a second-phase exit (which never goes through exitPhase from the commands view), + // so without a reset the cached empty-query key would skip the rebuild and leave stale content. builtKey = ""; qEl.placeholder = "Search " + ACTIONS.length + " actions"; syncClearButton(); @@ -1354,19 +888,6 @@ function exitPhase() { function focusInput() { setTimeout(function () { if (qEl) qEl.focus(); }, 0); } -// Move keyboard focus onto the first editable field of the setting editor card. Deferred so the -// element is attached and its text is selectable by the time we focus it. For text-editable inputs -// also select the existing value so the user can type straight over it. -function focusSettingEditor() { - setTimeout(function () { - var el = settingFieldEls && settingFieldEls[0]; - if (!el) return; - if (el.focus) el.focus(); - if ((el.tagName === "INPUT") && el.select) - el.select(); - }, 0); -} - // ---- init -------------------------------------------------------------------- function OnInit() { qEl = $("q"); listEl = $("list"); favEl = $("favBar"); clearEl = $("clear"); eyeEl = $("favEyebrow"); countEl = $("count"); @@ -1397,21 +918,10 @@ function OnInit() { // why: dismiss the fav context menu on any click/scroll away from it (capture scroll to catch nested scrollers). document.addEventListener("click", hideFavMenu); - // Dismiss an open editor dropdown on any outside click. Toggle/option clicks stopPropagation - // so they don't immediately close the menu they just opened/picked from. - document.addEventListener("click", function () { - if (openDropDownEl) closeEditorDropDown(); - }); document.addEventListener("scroll", hideFavMenu, true); document.addEventListener("keydown", function (e) { if (favMenuEl && !favMenuEl.hidden && e.key === "Escape") { e.preventDefault(); hideFavMenu(); return; } - // While an editor dropdown menu is open it owns the keys: Escape closes the menu (a second - // Esc exits the phase), Enter/arrows select options natively, and we must not apply/exit. - if (phase === "setting" && openDropDownEl && openDropDownEl.parentNode) { - if (e.key === "Escape") { e.preventDefault(); closeEditorDropDown(); } - return; - } // Quick-launch a numbered favourite: Alt/Option + digit (0 = the 10th). Only in the // commands phase, where the pinned bar is shown. if (phase === "commands" && e.altKey && !e.ctrlKey && !e.metaKey) { @@ -1432,8 +942,8 @@ function OnInit() { // let Left/Right fall through so they move the caret in the focused search field. var lr = e.key === "ArrowLeft" || e.key === "ArrowRight"; if (e.key === "ArrowDown" || e.key === "ArrowUp" || (lr && sel.zone === "fav")) { - // In the percent/setting phases the input controls own the caret - arrows edit text, not rows. - if (phase === "percent" || phase === "setting") return; + // In the percent phase the input control owns the caret - arrows edit text, not rows. + if (phase === "percent") return; e.preventDefault(); sel = nextSel(sel, e.key, list.length, favs.length); // why: entering/leaving the fav zone toggles the eyebrow line, changing launcher height; @@ -1442,7 +952,6 @@ function OnInit() { } else if (e.key === "Enter") { e.preventDefault(); if (phase === "percent") runJumpToLayer(query.trim()); - else if (phase === "setting") applySetting(); else runSelected(); } else if (e.key === "Escape") { e.preventDefault(); diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index cb7ef0c179..da306255b1 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -159,73 +159,4 @@ assert.equal(ctx.revealTarget(100, -5, 50), 50, "negative start is clamped to th assert.equal(ctx.revealTarget(200, 50, 100), 150, "a scroll viewpoint reveals a window past the current rows"); assert.equal(ctx.revealTarget(10, 0, 50), 10, "a list shorter than one window stays fully materialized"); -// ---- inline setting editor helpers (settingControlRows / Value / CollectedValue) ---- -const scalarBoolDesc = { - editable: true, control: "toggle", cardinality: "scalar", value: true, - min: undefined, max: undefined, is_int: false, unit: "" -}; -const scalarRows = ctx.settingControlRows(scalarBoolDesc); -assert.equal(scalarRows.length, 1, "a scalar setting yields exactly one control row"); -assert.equal(scalarRows[0].kind, "toggle", "the control kind is carried through"); -assert.equal(scalarRows[0].index, 0, "the single row is indexed 0"); -assert.deepEqual(ctx.settingControlRows({ editable: false }), [], "a non-editable setting yields no control rows"); - -const vectorDesc = { - editable: true, control: "number", cardinality: "vector", values: [0.2, 0.4], - index_labels: ["1", "2"], min: 0, max: 1, is_int: false, unit: "mm" -}; -const vectorRows = ctx.settingControlRows(vectorDesc); -assert.equal(vectorRows.length, 2, "a vector setting yields one row per value"); -assert.deepEqual(vectorRows.map(function (r) { return r.value; }), [0.2, 0.4], "each row carries its current value"); -assert.deepEqual(vectorRows.map(function (r) { return r.label; }), ["1", "2"], "vector rows use the per-index labels"); -assert.equal(vectorRows[0].unit, "mm", "the unit is carried to each row"); - -// settingControlValue reads a control element (checked/value) as the shipped value. -assert.equal(ctx.settingControlValue({ kind: "toggle" }, { checked: true }), true, "a toggle submits its checked state"); -assert.equal(ctx.settingControlValue({ kind: "toggle" }, { checked: false }), false, "an off toggle submits false"); -assert.equal(ctx.settingControlValue({ kind: "number", is_int: false, min: 0, max: 1 }, { value: "0.5" }), 0.5, "a float number parses"); -assert.equal(ctx.settingControlValue({ kind: "number", is_int: true, min: 0, max: 10 }, { value: "5" }), 5, "an int number parses"); -assert.equal(ctx.settingControlValue({ kind: "number", is_int: false, min: 0, max: 1 }, { value: "2" }), undefined, "an out-of-range number is rejected"); -assert.equal(ctx.settingControlValue({ kind: "number", is_int: false, min: 0, max: 1 }, { value: "" }), undefined, "an empty number is rejected"); -assert.equal(ctx.settingControlValue({ kind: "dropdown" }, { dataset: { value: "3" } }), 3, "an enum dropdown submits its stored int value"); -assert.equal(ctx.settingControlValue({ kind: "dropdown" }, { dataset: { value: "nope" } }), undefined, "a non-numeric dropdown value is rejected"); -assert.equal(ctx.settingControlValue({ kind: "dropdown" }, {}), undefined, "a dropdown with no value is rejected"); -// dropdownLabel maps the current int value to its human label, falling back to the value. -const seamOptions = [ - { value: 0, key: "nearest", label: "Nearest" }, - { value: 1, key: "aligned", label: "Aligned" }, - { value: 2, key: "random", label: "Random" } -]; -assert.equal(ctx.dropdownLabel(seamOptions, 1), "Aligned", "the dropdown label for a known value is its label"); -assert.equal(ctx.dropdownLabel(seamOptions, 9), "9", "an unknown value falls back to the raw value"); -assert.equal(ctx.dropdownLabel([], 3), "3", "empty options fall back to the raw value"); -assert.equal(ctx.dropdownLabel( - [{ value: 1, key: "aligned", label: "" }], 1), "aligned", "a blank label falls back to the key"); -// dropdownIcon maps the current int value to its pattern pictogram, empty when there is none. -const patternOptions = [ - { value: 0, key: "rectilinear", label: "Rectilinear" }, - { value: 3, key: "gyroid", label: "Gyroid", icon: "data:image/svg+xml;base64,AAA" }, - { value: 5, key: "grid", label: "Grid", icon: "data:image/svg+xml;base64,BBB" } -]; -assert.equal(ctx.dropdownIcon(patternOptions, 3), "data:image/svg+xml;base64,AAA", "a known value returns its icon"); -assert.equal(ctx.dropdownIcon(patternOptions, 0), "", "a value with no icon returns empty"); -assert.equal(ctx.dropdownIcon(patternOptions, 9), "", "an unknown value returns empty"); -assert.equal(ctx.dropdownIcon([], 3), "", "empty options return empty"); -assert.equal(ctx.settingControlValue({ kind: "text" }, { value: "hello" }), "hello", "text submits as a string"); -// percent (coFloatOrPercent / coFloatsOrPercents): submits the raw typed string; empty is invalid. -assert.equal(ctx.settingControlValue({ kind: "percent" }, { value: "10%" }), "10%", "a percent value submits as its string"); -assert.equal(ctx.settingControlValue({ kind: "percent" }, { value: "0.5" }), "0.5", "an mm value submits as a plain string"); -assert.equal(ctx.settingControlValue({ kind: "percent" }, { value: " " }), undefined, "a blank percent value is rejected"); -assert.equal(ctx.settingControlValue({ kind: "percent" }, {}), undefined, "a percent field with no value is rejected"); -// A percent scalar-to-scalar payload carries the raw string through unchanged. -assert.equal(ctx.settingCollectedValue( - { editable: true, control: "percent", cardinality: "scalar", value: "10%" }, - [{ kind: "percent", value: "10%" }], [{ value: "10%" }]), "10%", "a percent scalar assembles its string"); - -// settingCollectedValue assembles the payload (scalar vs vector) for the set_setting message. -assert.equal(ctx.settingCollectedValue({ editable: false }, [], []), undefined, "a non-editable setting yields no payload"); -assert.equal(ctx.settingCollectedValue(scalarBoolDesc, scalarRows, [{ checked: true }]), true, "a scalar assembles a single value"); -assert.deepEqual(ctx.settingCollectedValue(vectorDesc, vectorRows, [{ value: "0.3" }, { value: "0.7" }]), [0.3, 0.7], "a vector assembles an array"); -assert.equal(ctx.settingCollectedValue(vectorDesc, vectorRows, [{ value: "0.3" }, { value: "2" }]), undefined, "an invalid vector element aborts the whole payload"); - console.log("ok"); diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index b5108462dd..2467e88749 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -294,145 +294,3 @@ kbd { color: var(--muted, var(--orca-muted, #6b7280)); text-align: center; } - -/* ---- inline setting editor card (phase "setting") --------------------------- */ -.dial-list.setting { overflow-y: auto; } -.dial-editor { - display: flex; - flex-direction: column; - gap: 6px; - padding: 8px; -} -.editor-title { font-size: 14px; font-weight: 600; line-height: 1.3; } -.editor-title-row { display: flex; align-items: center; gap: 8px; } -.editor-preview-icon { - flex: 0 0 auto; - width: 24px; - height: 24px; -} -.editor-preview-icon[hidden] { display: none; } -.editor-tooltip { font-size: 11px; line-height: 1.4; color: var(--muted, var(--orca-muted, #6b7280)); } -.editor-row { display: flex; align-items: center; gap: 8px; min-height: 32px; } -.editor-label { - flex: 0 0 auto; - min-width: 64px; - font-size: 12px; - color: var(--muted, var(--orca-muted, #6b7280)); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.editor-unit { font-size: 12px; color: var(--muted, var(--orca-muted, #6b7280)); } -.editor-row input[type="number"], -.editor-row input[type="text"], -.editor-row select, -.editor-row input[type="color"] { - flex: 1 1 auto; - min-width: 0; - height: 30px; - padding: 0 8px; - border: 1px solid var(--border, var(--orca-border, #ddd)); - border-radius: 6px; - background: var(--panel, var(--orca-bg, #fff)); - color: var(--text, var(--orca-fg, #1b1c1e)); - font: inherit; -} -.editor-row input[type="color"] { padding: 2px 4px; cursor: pointer; } -.editor-row input:focus { outline: none; border-color: var(--main-color, var(--orca-accent, #009688)); box-shadow: 0 0 0 2px rgba(0,150,136,.22); } -/* custom dropdown (native <select> popups are flaky in the embedded webview) */ -.ed-dropdown { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; } -.ed-dropdown-toggle { - display: flex; - align-items: center; - gap: 6px; - width: 100%; - height: 30px; - padding: 0 8px; - border: 1px solid var(--border, var(--orca-border, #ddd)); - border-radius: 6px; - background: var(--panel, var(--orca-bg, #fff)); - color: var(--text, var(--orca-fg, #1b1c1e)); - font: inherit; - text-align: left; - cursor: pointer; -} -.ed-dropdown-toggle.open { border-color: var(--main-color, var(--orca-accent, #009688)); } -.ed-dropdown-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.ed-dropdown-caret { flex: 0 0 auto; font-size: 10px; color: var(--muted, var(--orca-muted, #6b7280)); transition: transform .12s; } -.ed-dropdown-toggle.open .ed-dropdown-caret { transform: rotate(180deg); } -.ed-dropdown-menu { - /* Overlay popover, not in-flow: opening it must NOT grow the editor card or the popup window. - position/left/top/width/max-height are set from JS on open (position:fixed escapes the - scrollable card and launcher overflow, so the menu clips to the popup and scrolls itself). - These are only the visual defaults. */ - position: absolute; - z-index: 30; - border: 1px solid var(--border, var(--orca-border, #ddd)); - border-radius: 6px; - background: var(--panel, var(--orca-bg, #fff)); - box-shadow: 0 8px 24px rgba(0,0,0,.16); - max-height: 200px; - overflow-y: auto; -} -.ed-dropdown-option { - display: flex; - align-items: center; - gap: 8px; - width: 100%; - padding: 6px 8px; - border: 0; - background: none; - color: var(--text, var(--orca-fg, #1b1c1e)); - font: inherit; - text-align: left; - cursor: pointer; -} -.ed-dropdown-option:hover { background: var(--row-hover, rgba(127,127,127,.16)); } -.ed-dropdown-option.sel { font-weight: 600; color: var(--main-color, var(--orca-accent, #009688)); } -.ed-option-label { flex: 1 1 auto; min-width: 0; } -/* Pattern pictograms: filled with the option's icon, and (for values with none) absent. */ -.ed-option-icon, -.ed-dropdown-icon { - flex: 0 0 auto; - width: 16px; - height: 16px; -} -.ed-dropdown-icon[hidden] { display: none; } -/* toggle switch */ -.ed-switch { position: relative; flex: 0 0 auto; width: 36px; height: 20px; } -.ed-switch input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; } -.ed-slider { - position: absolute; - inset: 0; - background: #c4c4c4; - border-radius: 10px; - transition: background .15s; -} -.ed-slider::before { - content: ""; - position: absolute; - width: 16px; - height: 16px; - left: 2px; - top: 2px; - background: #fff; - border-radius: 50%; - transition: transform .15s; -} -.ed-switch input:checked + .ed-slider { background: var(--main-color, var(--orca-accent, #009688)); } -.ed-switch input:checked + .ed-slider::before { transform: translateX(16px); } -.editor-readonly { padding: 4px 0; font-size: 12px; color: var(--muted, var(--orca-muted, #6b7280)); } -.editor-actions { display: flex; gap: 8px; padding-top: 4px; } -.ed-btn { - padding: 6px 12px; - border: 1px solid var(--border, var(--orca-border, #ddd)); - border-radius: 6px; - background: var(--panel, var(--orca-bg, #fff)); - color: var(--text, var(--orca-fg, #1b1c1e)); - font: inherit; - cursor: pointer; -} -.ed-btn:hover { background: var(--row-hover, rgba(127,127,127,.16)); } -.ed-btn-primary { background: var(--main-color, var(--orca-accent, #009688)); border-color: var(--main-color, var(--orca-accent, #009688)); color: #fff; } -.ed-btn-primary:hover { filter: brightness(1.05); } -.editor-hint { font-size: 10px; text-align: center; color: var(--muted, var(--orca-muted, #6b7280)); } diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index ef31401909..b55455f45d 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -180,17 +180,15 @@ struct SettingAction : AppAction , type(type_in) , category(std::move(category_in)) { - // A setting is a two-phase command: activating it opens the inline editor (the "setting" - // phase) instead of running. Native run() is a no-op fallback; the editor applies through - // apply_setting(). + // A setting is a single-phase command: activating it jumps the sidebar to the option + // (like the sidebar's own settings search), then the dial closes. run() performs the jump. this->kind = AppActionKind::Command; this->group = std::move(group); - this->input = "setting"; } AppActionRunResult run(const std::string& /*param*/) const override { - // Two-phase: the palette collects the edit; native run() is a no-op fallback. + wxGetApp().sidebar().jump_to_option(opt_key, type, category); return {AppActionRunResult::Level::Success}; } @@ -693,45 +691,7 @@ std::vector<std::unique_ptr<AppAction>> native_commands() return out; } -// ---- inline setting editor helpers ------------------------------------------ - -// The inline editor "control" kind for an option, or "" when it can't be edited inline -// (plugin-backed values, points, serialized strings, etc.). readonly/legend options are -// filtered out of the search entirely in materialize_setting_actions(), so they never -// reach here. -std::string setting_control(const ConfigOptionDef& def) -{ - if (def.readonly || def.gui_type == ConfigOptionDef::GUIType::legend || - def.gui_type == ConfigOptionDef::GUIType::one_string || def.is_plugin_backed()) - return ""; - // Serialized vectors are entered as ONE semicolon-separated field (e.g. post_process), which the - // per-index editor doesn't model - keep them in the open-in-sidebar bucket. - if (def.gui_flags.find("serialized") != std::string::npos) - return ""; - switch (def.gui_type) { - case ConfigOptionDef::GUIType::color: return "color"; - case ConfigOptionDef::GUIType::i_enum_open: - case ConfigOptionDef::GUIType::f_enum_open: return "combo"; - default: break; - } - switch (def.type) { - case coBool: - case coBools: return "toggle"; - case coEnum: - case coEnums: return def.enum_values.empty() ? "combo" : "dropdown"; - case coInt: - case coInts: - case coFloat: - case coFloats: - case coPercent: - case coPercents: return "number"; - case coFloatOrPercent: - case coFloatsOrPercents: return "percent"; - case coString: - case coStrings: return "text"; - default: return ""; - } -} +// ---- setting action helpers -------------------------------------------------- // Self-contained base64 encoder (for the tiny pictogram SVGs), avoiding a dependency on the exact // wxBase64Encode overload/return type across wx versions. @@ -790,41 +750,6 @@ std::string setting_icon_for_key(const std::string& key) return "data:image/svg+xml;base64," + base64_encode(data); } -// [{value,key,label,icon}...] for an enum/combo. Ordered by enum_values when present, else by the -// keys_map iteration. label falls back to the key when enum_labels doesn't provide one. `icon` is -// the value's pattern pictogram when one exists, empty otherwise. -nlohmann::json setting_enum_options(const ConfigOptionDef& def) -{ - nlohmann::json out = nlohmann::json::array(); - auto label_at = [&](size_t i, const std::string& key) -> std::string { - return (i < def.enum_labels.size() && !def.enum_labels[i].empty()) ? def.enum_labels[i] : key; - }; - if (def.enum_keys_map != nullptr) { - std::vector<std::string> ordered; - if (!def.enum_values.empty()) - ordered = def.enum_values; - else - for (const auto& kv : *def.enum_keys_map) - ordered.push_back(kv.first); - for (size_t i = 0; i < ordered.size(); ++i) { - auto it = def.enum_keys_map->find(ordered[i]); - if (it == def.enum_keys_map->end()) - continue; - out.push_back({{"value", it->second}, - {"key", ordered[i]}, - {"label", label_at(i, ordered[i])}, - {"icon", setting_icon_for_key(ordered[i])}}); - } - } else { - for (size_t i = 0; i < def.enum_values.size(); ++i) - out.push_back({{"value", (long long) i}, - {"key", def.enum_values[i]}, - {"label", label_at(i, def.enum_values[i])}, - {"icon", setting_icon_for_key(def.enum_values[i])}}); - } - return out; -} - // The pattern pictogram for a setting's CURRENT value (its enum int), empty when it isn't a // pattern-style enum or the value has no icon. Used for the search-result tile. std::string setting_action_icon(const SettingAction& a) @@ -854,146 +779,6 @@ std::string setting_action_icon(const SettingAction& a) std::string SettingAction::icon() const { return setting_action_icon(*this); } -// Current value of the option at vector index `idx` as JSON (bool/number/string), or null for a -// type the inline editor doesn't render. `config` is the tab's live config; when an option is -// absent the def's default is shown. -nlohmann::json setting_value_json(const DynamicPrintConfig& config, const ConfigOptionDef& def, size_t idx) -{ - const ConfigOption* opt = config.option(def.opt_key); - const ConfigOption* root = opt ? opt : def.default_value.get(); - if (!root) - return nullptr; - switch (def.type) { - case coBool: return root->getBool(); - case coInt: return root->getInt(); - case coFloat: return root->getFloat(); - case coPercent: return root->getFloat(); - case coString: return static_cast<const ConfigOptionString*>(root)->value; - case coEnum: return root->getInt(); - case coBools: { - if (auto v = dynamic_cast<const ConfigOptionBools*>(root)) - return bool(v->get_at(idx)); - if (auto v = dynamic_cast<const ConfigOptionBoolsNullable*>(root)) - return bool(v->get_at(idx) != 0); - return nullptr; - } - case coInts: { - if (auto v = dynamic_cast<const ConfigOptionInts*>(root)) - return v->get_at(idx); - if (auto v = dynamic_cast<const ConfigOptionIntsNullable*>(root)) { - const int nil = ConfigOptionIntsNullable::nil_value(); - int val = v->get_at(idx); - return val == nil ? nlohmann::json(nullptr) : nlohmann::json(val); - } - return nullptr; - } - case coFloats: { - if (auto v = dynamic_cast<const ConfigOptionFloats*>(root)) - return v->get_at(idx); - if (auto v = dynamic_cast<const ConfigOptionFloatsNullable*>(root)) { - double val = v->get_at(idx); - return std::isnan(val) ? nlohmann::json(nullptr) : nlohmann::json(val); - } - return nullptr; - } - case coPercents: { - if (auto v = dynamic_cast<const ConfigOptionPercents*>(root)) - return v->get_at(idx); - if (auto v = dynamic_cast<const ConfigOptionPercentsNullable*>(root)) { - double val = v->get_at(idx); - return std::isnan(val) ? nlohmann::json(nullptr) : nlohmann::json(val); - } - return nullptr; - } - case coStrings: return static_cast<const ConfigOptionStrings*>(root)->get_at(idx); - case coEnums: { - if (auto v = dynamic_cast<const ConfigOptionEnumsGeneric*>(root)) - return v->get_at(idx); - if (auto v = dynamic_cast<const ConfigOptionEnumsGenericNullable*>(root)) { - const int nil = ConfigOptionEnumsGenericNullable::nil_value(); - int val = v->get_at(idx); - return val == nil ? nlohmann::json(nullptr) : nlohmann::json(val); - } - return nullptr; - } - case coFloatOrPercent: return root->serialize(); - case coFloatsOrPercents: { - if (auto v = dynamic_cast<const ConfigOptionFloatsOrPercents*>(root)) { - auto ss = v->vserialize(); - return idx < ss.size() ? ss[idx] : nullptr; - } - if (auto v = dynamic_cast<const ConfigOptionFloatsOrPercentsNullable*>(root)) { - auto ss = v->vserialize(); - return idx < ss.size() ? ss[idx] : nullptr; - } - return nullptr; - } - default: return nullptr; - } -} - -// How many scalar values the option currently has (1 for scalars, the array length for vectors). -size_t setting_value_count(const DynamicPrintConfig& config, const ConfigOptionDef& def) -{ - if ((int(def.type) & int(coVectorType)) == 0) - return 1; - // size() lives on ConfigOptionVectorBase, not ConfigOption - dynamic_cast to it covers every - // vector type (and their nullable variants) polymorphically. - const ConfigOption* opt = config.option(def.opt_key); - if (opt) - if (auto v = dynamic_cast<const ConfigOptionVectorBase*>(opt)) - return v->size(); - if (def.default_value) - if (auto v = dynamic_cast<const ConfigOptionVectorBase*>(def.default_value.get())) - return v->size(); - return 1; -} - -// boost::any for a single element, matching what Slic3r::GUI::change_opt_value expects. -boost::any setting_any_from_json(const ConfigOptionDef& def, const nlohmann::json& v) -{ - if (!v.is_null()) { - switch (def.type) { - case coBool: return boost::any(v.is_boolean() ? v.get<bool>() : v.get<int>() != 0); - case coInt: return boost::any(v.get<int>()); - case coFloat: - case coPercent: return boost::any(v.get<double>()); - case coString: return boost::any(v.get<std::string>()); - case coEnum: return boost::any(v.get<int>()); - case coBools: return boost::any(static_cast<unsigned char>(v.get<bool>() ? 1 : 0)); - case coInts: return boost::any(v.get<int>()); - case coFloats: - case coPercents: return boost::any(v.get<double>()); - case coStrings: return boost::any(v.get<std::string>()); - case coFloatOrPercent: - case coFloatsOrPercents: { - // change_opt_value detects "percent" via a trailing '%', so trim whitespace first or a - // stray space (e.g. "10% ") would be misread as mm. - std::string s = v.is_string() ? v.get<std::string>() : std::to_string(v.get<double>()); - boost::trim(s); - // An empty string would make change_opt_value's str.back() UB - bail out to a rejected apply. - return s.empty() ? boost::any() : boost::any(s); - } - case coEnums: return boost::any(v.get<int>()); - default: break; - } - } - // Coerce numeric types that may arrive as a different JSON numeric type. - if (v.is_number()) { - switch (def.type) { - case coInt: - case coEnums: return boost::any(v.get<int>()); - case coFloat: - case coPercent: - case coInts: - case coFloats: - case coPercents: return boost::any(v.get<double>()); - default: break; - } - } - return boost::any(); -} - } // namespace ActionRegistry::~ActionRegistry() = default; @@ -1257,16 +1042,6 @@ void ActionRegistry::materialize_setting_actions() std::unordered_set<std::string> seen; for (const Search::Option& opt : options) { - // Omit rows the inline editor can't represent and that aren't useful as a jump target: - // readonly (e.g. the detected thread count) and legend (static text) GUI rows. They remain - // in the sidebar's own search; only the Speed Dial pool drops them. - Tab* tab = wxGetApp().get_tab(opt.type); - if (tab && tab->get_config()) { - const ConfigOptionDef* def = tab->get_config()->def()->get(opt.opt_key()); - if (!def || def->readonly || def->gui_type == ConfigOptionDef::GUIType::legend) - continue; - } - const std::string id = SettingAction::id_for(opt.opt_key(), opt.type); seen.insert(id); @@ -1468,140 +1243,4 @@ nlohmann::json ActionRegistry::tab_options() const return out; } -// ---- inline setting editor (read the current value) -------------------------- - -nlohmann::json ActionRegistry::setting_descriptor(const std::string& id) const -{ - assert(wxThread::IsMain()); - const AppAction* a = by_id(id); - const SettingAction* sa = dynamic_cast<const SettingAction*>(a); - if (!sa) - return nlohmann::json::object(); - Tab* tab = wxGetApp().get_tab(sa->type); - if (!tab) - return nlohmann::json::object(); - DynamicPrintConfig* config = tab->get_config(); - if (!config) - return nlohmann::json::object(); - const ConfigOptionDef* def = config->def()->get(sa->opt_key); - if (!def) - return nlohmann::json::object(); - - const std::string control = setting_control(*def); - const bool vector = (int(def->type) & int(coVectorType)) != 0; - - nlohmann::json d = {{"id", sa->id()}, - {"opt_key", sa->opt_key}, - {"type", int(sa->type)}, - {"title", a->title()}, - {"breadcrumb", a->source_name()}, - {"category", boost::nowide::narrow(sa->category)}, - {"unit", def->sidetext}, - {"tooltip", def->tooltip}, - {"editable", !control.empty()}, - {"control", control}, - {"cardinality", vector ? "vector" : "scalar"}}; - - if (!control.empty()) { - // Hide unbounded min/max so the page doesn't clamp a sane value to ±FLT_MAX. - if (def->min > -FLT_MAX) - d["min"] = def->min; - if (def->max < FLT_MAX) - d["max"] = def->max; - if (control == "number") - d["is_int"] = (def->type == coInt || def->type == coInts); - if (control == "dropdown" || control == "combo") - d["enum_options"] = setting_enum_options(*def); - if (vector) { - nlohmann::json values = nlohmann::json::array(); - nlohmann::json labels = nlohmann::json::array(); - const size_t n = setting_value_count(*config, *def); - for (size_t i = 0; i < n; ++i) { - values.push_back(setting_value_json(*config, *def, i)); - labels.push_back(std::to_string(i + 1)); - } - d["values"] = std::move(values); - d["index_labels"] = std::move(labels); - } else { - d["value"] = setting_value_json(*config, *def, 0); - } - } - return d; -} - -// ---- inline setting editor (write the edited value back) --------------------- - -bool ActionRegistry::apply_setting(const std::string& id, const nlohmann::json& value) -{ - assert(wxThread::IsMain()); - const AppAction* a = by_id(id); - const SettingAction* sa = dynamic_cast<const SettingAction*>(a); - if (!sa) - return false; - Tab* tab = wxGetApp().get_tab(sa->type); - if (!tab) - return false; - DynamicPrintConfig* config = tab->get_config(); - if (!config) - return false; - const ConfigOptionDef* def = config->def()->get(sa->opt_key); - if (!def) - return false; - const std::string control = setting_control(*def); - if (control.empty()) - return false; - - const bool vector = (int(def->type) & int(coVectorType)) != 0; - const size_t n = vector ? (value.is_array() ? value.size() : 0) : 1; - if (vector && n == 0) - return false; - - for (size_t i = 0; i < n; ++i) { - const nlohmann::json& elem = vector ? value[i] : value; - boost::any any = setting_any_from_json(*def, elem); - if (any.empty()) - return false; - if (control == "number" && elem.is_number()) { - const double d = elem.get<double>(); - if (d < def->min || d > def->max) - return false; - } - if (control == "percent" && elem.is_string()) { - // "mm or %" value: strip a trailing %/whitespace, clamp the numeric part to [min,max]. - // Reject anything that isn't a well-formed number (which change_opt_value would throw on). - std::string s = elem.get<std::string>(); - boost::trim(s); - if (!s.empty() && s.back() == '%') - s.pop_back(); - boost::trim(s); - if (s.empty()) - return false; - char* end = nullptr; - const double d = std::strtod(s.c_str(), &end); - if (end == s.c_str() || *end != '\0') - return false; - if (d < def->min || d > def->max) - return false; - } - Slic3r::GUI::change_opt_value(*config, sa->opt_key, any, int(i)); - } - - // Mark the preset modified like a sidebar edit. Scalar options also get the standard - // post-change hook so dependent settings refresh; vector options have no unambiguous scalar - // value to pass, so on_value_change is skipped (the config write + dirty flag is still correct). - tab->update_dirty(); - if (!vector) { - boost::any any = setting_any_from_json(*def, value); - if (!any.empty()) - tab->on_value_change(sa->opt_key, any); - } - - // The config write is separate from the on-screen Field, so repaint the field(s) that display - // this option (on whatever page they live, not just the active page) - otherwise the sidebar - // shows the "modified" arrow but keeps the stale value pushed to the last edit/reload. - if (Page* page = nullptr; tab->get_field(sa->opt_key, &page) && page) - page->reload_config(); - return true; -} - }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index d7bd1fa583..6daafdde56 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -173,16 +173,6 @@ public: // plugins) aren't separate pages and are not listed. Call on the UI thread; null-safe. nlohmann::json tab_options() const; - // Inline setting editor descriptor for a SettingAction id. Returns the JSON the palette - // renders: {id, opt_key, type, title, breadcrumb, category, group, unit, tooltip, editable, - // control ("toggle|number|dropdown|combo|text|color"), cardinality ("scalar"|"vector"), - // value|values, index_labels[], enum_options[], min|max}. Empty object for a non-setting id. - nlohmann::json setting_descriptor(const std::string& id) const; - // Apply an edit submitted by the palette. `value` is the control's JSON payload (scalar, or an - // array for vector settings). Writes the value(s) into the global preset config and marks the - // preset dirty, exactly like a sidebar edit. Returns false on a bad id/type/value. - bool apply_setting(const std::string& id, const nlohmann::json& value); - private: void seed_state(AppAction& a) const; // favourite/stats from config AppAction* find(const std::string& id); diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index 593d8a7344..b199d0b5ae 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -9,10 +9,6 @@ #include "Plater.hpp" #include "Widgets/WebViewHostDialog.hpp" -#include <libslic3r/Preset.hpp> - -#include <boost/nowide/convert.hpp> - #include <algorithm> #include <wx/display.h> @@ -155,27 +151,6 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload) if (wxGetApp().mainframe) wxGetApp().mainframe->select_tab(from_u8(tab_id)); } - } else if (command == "setting_descriptor") { - // Inline editor: hand the page the descriptor for the setting it's editing. - const std::string id = payload.value("id", ""); - call_web_handler( - {{"command", "setting_descriptor"}, {"descriptor", wxGetApp().action_registry().setting_descriptor(id)}}); - } else if (command == "set_setting") { - // Inline editor submit. Apply the value; on success close the dialog. - const std::string id = payload.value("id", ""); - const nlohmann::json value = payload.contains("value") ? payload["value"] : nlohmann::json(nullptr); - if (id.empty() || !wxGetApp().action_registry().apply_setting(id, value)) { - call_web_handler({{"command", "apply_failed"}, {"id", id}}); - return; - } - Hide(); - } else if (command == "open_setting_in_sidebar") { - // Non-inline-editable setting (points, plugin-backed, float-or-percent): jump the sidebar. - Hide(); - const std::string opt_key = payload.value("opt_key", ""); - const std::string category = payload.value("category", ""); - if (!opt_key.empty()) - wxGetApp().sidebar().jump_to_option(opt_key, Preset::Type(payload.value("type", int(Preset::TYPE_INVALID))), boost::nowide::widen(category)); } else if (command == "resize") resize_to_content(json_int_or(payload, "height", 0)); } From 3985200672b509db272306f3a9d916e536dfb4a6 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Wed, 9 Sep 2026 16:53:58 +0800 Subject: [PATCH 06/29] Search improvements. Code refactoring so its easier to maintain. Experimental features for speed dial: connect/disconnect from printer. --- resources/web/dialog/SpeedDial/speeddial.js | 185 +++-- .../web/dialog/SpeedDial/speeddial.test.js | 29 +- resources/web/dialog/SpeedDial/style.css | 15 +- resources/web/js/fuzzy-search.js | 57 +- resources/web/js/fuzzy-search.test.js | 13 +- src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/ActionRegistry.cpp | 666 +++--------------- src/slic3r/GUI/ActionRegistry.hpp | 10 +- src/slic3r/GUI/MainFrame.cpp | 178 +++-- src/slic3r/GUI/MainFrame.hpp | 20 + src/slic3r/GUI/NativeCommands.cpp | 500 +++++++++++++ src/slic3r/GUI/NativeCommands.hpp | 31 + src/slic3r/GUI/Plater.cpp | 19 + src/slic3r/GUI/Plater.hpp | 3 + tests/slic3rutils/test_action_source.cpp | 10 + 15 files changed, 968 insertions(+), 770 deletions(-) create mode 100644 src/slic3r/GUI/NativeCommands.cpp create mode 100644 src/slic3r/GUI/NativeCommands.hpp diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 78ba8a138d..04844aae43 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -39,37 +39,60 @@ var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, count // ---- pure helpers (no DOM; unit-tested) ------------------------------------- // Pre-normalized haystacks, cached on the action object. The fold is length-preserving (1:1 per -// char) so the ranges FuzzyRangesNorm returns slice the ORIGINAL title/source text correctly. The -// action objects arrive from C++ and are stable for the dialog's lifetime, so we compute these once. +// char) so the ranges FuzzyRangesNorm returns slice the ORIGINAL title/group/source text correctly. +// The action objects arrive from C++ and are stable for the dialog's lifetime, so we compute these once. function titleNorm(a) { if (a._tn === undefined) a._tn = NormText(a.title, false); return a._tn; } -function otherNorm(a) { - if (a._on === undefined) - a._on = NormText((a.source || "") + " " + (a.group || ""), false); - return a._on; +// The eyebrow (header) line shows group when present, else source. Settings keep an empty group so +// their source path is the eyebrow; commands / plates / recents carry a non-empty group. Splitting the +// two lets a match range stay aligned to whichever string the eyebrow actually renders. +function groupNorm(a) { + if (a._gn === undefined) + a._gn = NormText(a.group || "", false); + return a._gn; +} +function sourceNorm(a) { + if (a._sn === undefined) + a._sn = NormText(a.source || "", false); + return a._sn; } -// Relevance score for a single field vs the current query needle, or -1 when there's no match. -// Higher is better: an earlier start and a more contiguous (fewer gaps) match beat a scattered late one. -function matchScoreNorm(haystackNorm) { - if (!searchNeedle) return -1; - var r = FuzzyRangesNorm(haystackNorm || "", searchNeedle); - if (!r) return -1; - var gaps = 0; - for (var i = 1; i < r.length; i++) - gaps += r[i][0] - r[i - 1][1]; - return 1000 - r[0][0] * 10 - gaps * 10; +// Match one pre-normalized field vs the current needle. Returns {score, ranges, contiguous} when the +// needle is present, else null. wwRe is a compiled whole-word (\b-bounded) regex for the current needle. +// A whole-word hit is preferred - it highlights the full word (e.g. "orient" in "Auto-Orient", not the +// stray "o" of "Auto") and marks a perfect match. Otherwise FuzzyRangesNorm (which now prefers the +// most-contiguous run) is used. score is higher for an earlier start and fewer gaps; contiguous marks +// a perfect match - the whole needle landed as one unbroken run. +function fieldMatchScore(norm, wwRe) { + if (!searchNeedle) return null; + if (wwRe) { + var m = wwRe.exec(norm || ""); + if (m) + return {score: 1000 - m.index * 10, ranges: [[m.index, m.index + m[0].length]], contiguous: true}; + } + var r = FuzzyRangesNorm(norm || "", searchNeedle); + if (!r) return null; + var gaps = 0, len = 0; + for (var i = 0; i < r.length; i++) { + if (i > 0) + gaps += r[i][0] - r[i - 1][1]; + len += r[i][1] - r[i][0]; + } + return {score: 1000 - r[0][0] * 10 - gaps * 10, ranges: r, contiguous: r.length === 1 && len === searchNeedle.length}; } -// Per-action score: title matches rank above a source/group-only match of equal quality. -function actionSearchScore(a) { - var title = matchScoreNorm(titleNorm(a)); - var other = matchScoreNorm(otherNorm(a)); - if (title < 0 && other < 0) return -1; - return Math.max(title < 0 ? -1e9 : title + 10000, other < 0 ? -1e9 : other); +// Combine the per-field match scores into one comparable value. Ranking tiers, strongest first: +// tier (contiguous/perfect vs fuzzy) > field (title > group > source) > start/gaps. +// The additive weights keep every contiguous match above every fuzzy one regardless of field. +function scoreFields(t, g, s) { + var best = -1; + if (t) best = Math.max(best, (t.contiguous ? 100000 : 0) + 2000 + t.score); + if (g) best = Math.max(best, (g.contiguous ? 100000 : 0) + 1000 + g.score); + if (s) best = Math.max(best, (s.contiguous ? 100000 : 0) + s.score); + return best; } // The unified main-phase search: every action (command/plugin/setting) matching the query, ranked @@ -82,15 +105,27 @@ function searchActions(actions, query) { matchIndex = {}; if (!q) { searchNeedle = ""; return list.slice(0); } searchNeedle = NormText(q, false); + // Compiled once per pass, reused over every field: non-global so no exec()/lastIndex state leaks + // between fields, and EscapeRegExp keeps regex metachars in the query literal. + var wwRe = new RegExp("\\b" + EscapeRegExp(searchNeedle) + "\\b"); var scored = []; for (var i = 0; i < list.length; i++) { var a = list[i]; - var s = actionSearchScore(a); - if (s < 0) continue; - var titleMatch = FuzzyRangesNorm(titleNorm(a), searchNeedle); - matchIndex[a.id] = { title: titleMatch, source: FuzzyRangesNorm(otherNorm(a), searchNeedle), useTitle: !!titleMatch }; - scored.push({ a: a, s: s }); + var t = fieldMatchScore(titleNorm(a), wwRe); + var g = fieldMatchScore(groupNorm(a), wwRe); + var s = fieldMatchScore(sourceNorm(a), wwRe); + var score = scoreFields(t, g, s); + if (score < 0) continue; + // Ranges are per-field against the ACTUAL text drawn: title for the row-name, and group (or + // source when group is empty) for the eyebrow - so highlight offsets stay aligned to the label. + matchIndex[a.id] = { + title: t ? t.ranges : null, + group: g ? g.ranges : null, + source: s ? s.ranges : null, + useEyebrowGroup: !!(a.group) + }; + scored.push({ a: a, s: score }); } scored.sort(function (x, y) { if (x.s !== y.s) return y.s - x.s; @@ -112,7 +147,7 @@ function buildKey() { return phase + "|" + (query || "").trim(); } function visibleFavourites(favourites, actions) { // why: a fav whose id has no live action (plugin unloaded/disabled) renders a dead - // monogram tile whose click run()s to a silent no-op; drop it from the quick-bar. + // placeholder tile whose click run()s to a silent no-op; drop it from the quick-bar. var seen = {}; (actions || []).forEach(function (a) { seen[a.id] = true; }); return (favourites || []).filter(function (id, i, arr) { @@ -164,21 +199,42 @@ function shouldRenderActionList(query) { return !!((query || "").trim()); } -// Put an action's pattern pictogram into a tile (search row or favourites tile) when it has one, -// otherwise fall back to the monogram. Toggles the has-icon class so CSS neutralises the hue. +// 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 items stay distinct. +// 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. +function monogramFor(item, list, titleOf, sourceOf, idOf) { + var items = list || []; + var title = titleOf(item) || " "; + var ti = title.charAt(0).toUpperCase(); + var sameTitle = items.filter(function (o) { return (titleOf(o) || " ").charAt(0).toUpperCase() === ti; }); + if (sameTitle.length <= 1) + return ti; + var source = sourceOf(item) || " "; + var pi = source.charAt(0).toUpperCase(); + var sameSource = sameTitle.filter(function (o) { return (sourceOf(o) || " ").charAt(0).toUpperCase() === pi; }); + if (sameSource.length <= 1) + return pi + ti; + 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++) + if (sameSource[i] === item || idOf(sameSource[i]) === idOf(item)) + return pi + ti + (i + 1); + return pi + ti; +} + +// Action tile code - see monogramFor for the escalation ladder. Settings are actions now, so +// they share this ladder (title initial, then source, then a stable ordinal). +function tileCode(action, actions) { + return monogramFor(action, actions, + function (o) { return o.title; }, + function (o) { return o.source; }, + function (o) { return o.id; }); +} + +// Put an action's monogram into a tile (search row or favourites tile). A null action (a tab row +// with no backing action) renders an empty tile. function fillTile(tile, a) { - tile.classList.remove("has-icon"); - if (a && a.icon) { - tile.textContent = ""; - var img = document.createElement("img"); - img.className = "tile-icon"; - img.src = a.icon; - img.alt = ""; - tile.appendChild(img); - tile.classList.add("has-icon"); - } else { - tile.textContent = a ? tileCode(a, ACTIONS) : ""; - } + tile.textContent = a ? tileCode(a, ACTIONS) : ""; } // The active list for the main phase. A typed query ranks every action (commands/plugins/settings) @@ -224,38 +280,6 @@ function actionLabel(action, actions) { return label; } -// 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 items stay distinct. -// 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. -function monogramFor(item, list, titleOf, sourceOf, idOf) { - var items = list || []; - var title = titleOf(item) || " "; - var ti = title.charAt(0).toUpperCase(); - var sameTitle = items.filter(function (o) { return (titleOf(o) || " ").charAt(0).toUpperCase() === ti; }); - if (sameTitle.length <= 1) - return ti; - var source = sourceOf(item) || " "; - var pi = source.charAt(0).toUpperCase(); - var sameSource = sameTitle.filter(function (o) { return (sourceOf(o) || " ").charAt(0).toUpperCase() === pi; }); - if (sameSource.length <= 1) - return pi + ti; - 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++) - if (sameSource[i] === item || idOf(sameSource[i]) === idOf(item)) - return pi + ti + (i + 1); - return pi + ti; -} - -// Action tile code - see monogramFor for the escalation ladder. Settings are actions now, so -// they share this ladder (title initial, then source, then a stable ordinal). -function tileCode(action, actions) { - return monogramFor(action, actions, - function (o) { return o.title; }, - function (o) { return o.source; }, - function (o) { return o.id; }); -} - function syncClearButton() { if (clearEl) clearEl.hidden = !query; @@ -542,7 +566,12 @@ function renderActionRow(a, i) { var left = document.createElement("div"); left.className = "row-left"; var mi = matchIndex[a.id]; - var sourceEl = markedText("row-eyebrow", a.group || a.source, mi ? mi.source : null); + // The eyebrow shows group when present, else source. Highlight with the ranges of whichever of the + // two the eyebrow actually renders (so a "Recent Projects"/"Object" header match lights up like a + // setting path does - the offsets are computed against the same string we are marking). + var eyebrow = a.group || a.source; + var eyebrowMatch = mi ? (mi.useEyebrowGroup ? mi.group : mi.source) : null; + var sourceEl = markedText("row-eyebrow", eyebrow, eyebrowMatch); var line = document.createElement("div"); line.className = "row-line"; var name = markedText("row-name", a.title, mi ? mi.title : null); @@ -675,8 +704,8 @@ function renderCommandsList() { updateSelection(); } -// 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. +// A tab row: no star/unpin (tabs aren't pinnable), placeholder tile (tabs have no pictogram). Uses +// tabTitle so pages added with an empty text (e.g. Home) still show a label. function renderTabRow(t, i) { var label = tabTitle(t); var row = document.createElement("div"); @@ -686,7 +715,7 @@ function renderTabRow(t, i) { var tile = document.createElement("div"); tile.className = "tile"; tile.style.setProperty("--h", hue(t.id)); - tile.textContent = label.charAt(0).toUpperCase(); + fillTile(tile, null); var left = document.createElement("div"); left.className = "row-left"; diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index da306255b1..6a0a42bc2e 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -59,7 +59,7 @@ assert.equal(ctx.tabTitle({ id: "home" }), "Home", 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"); + "a leading space from the Notebook button label is trimmed so the label shows cleanly"); 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, @@ -82,6 +82,33 @@ assert.equal(ctx.searchActions(pool, "layer").length >= 2, true, assert.equal(ctx.searchActions(pool, "surface")[0].id, "c2", "a later-but-precise match still ranks by relevance, not by pool type"); +// A perfect match (the needle as one contiguous run) outranks a fuzzy match of the same field - and a +// contiguous GROUP/header hit ("Recent Projects") beats a scattered fuzzy TITLE hit ("Retraction Length"), +// which is what the old flat title-bonus ranking got backwards. +const perfectPool = [ + { id: "set", title: "Retraction Length", source: "Process : Quality : Retraction", group: "", input: "" }, + { id: "recent", title: "myproject.3mf", source: "/home/me/projects/myproject.3mf", group: "Recent Projects", input: "" } +]; +assert.equal(ctx.searchActions(perfectPool, "recent")[0].id, "recent", + "a contiguous header/group match ranks above a scattered fuzzy title match"); +// Within a perfect match, the row-name (title) outranks the header (group): the action whose TITLE +// contains the needle perfectly beats the action whose GROUP does, both being contiguous matches. +const titleFirstPool = [ + { id: "grp", title: "Delete Selected", source: "OrcaSlicer", group: "Object", input: "" }, + { id: "t", title: "Object Preview", source: "OrcaSlicer", group: "View", input: "" } +]; +assert.equal(ctx.searchActions(titleFirstPool, "object")[0].id, "t", + "a perfect title match ranks above an equally-perfect group match"); + +// Highlighting: the needle is matched as a whole word / most-contiguous run, so "orient" lights up the +// whole word in "Auto-Orient" instead of the stray "o" of "Auto" plus "rient" (greedy-leftmost). +const orientPool = [ + { id: "ao", title: "Auto-Orient", source: "OrcaSlicer", group: "Object", input: "" } +]; +ctx.searchActions(orientPool, "orient"); +assert.deepEqual(ctx.matchIndex.ao.title, [[5, 11]], + "a whole-word match highlights the full word, not a scattered fuzzy pick"); + // commandList (the main-phase list) delegates to the ranked search for a typed query and returns // the mixed recents (no discrimination) for an empty query. const mixed = [ diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index 2467e88749..aa8bdc7c4b 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -43,8 +43,8 @@ body { cursor: pointer; color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%)); font-weight: 700; - /* why: mirror .tile centering - tileCode can be 2-3 chars (e.g. "EA1") on collision, and a - bare <button> inherits 13px + UA padding, clipping the code (e.g. "AE1"). inline-flex + 12px + pad:0 fits it. */ + /* why: mirror .tile centering - icons/placeholder are 18px glyphs, and a bare <button> inherits + 13px + UA padding, which would clip them. inline-flex + pad:0 + overflow:hidden fits them. */ font-size: 12px; padding: 0; display: inline-flex; @@ -227,17 +227,6 @@ body { background: var(--speed-tile-bg, #f0f0f0); border: 1px solid var(--speed-tile-border, #d8d8d8); } -/* A tile holding a pattern pictogram: drop the hue fill so the stroked SVG reads cleanly. */ -.tile.has-icon, -.fav-tile.has-icon { - background: none; - border-color: transparent; -} -.tile-icon { - width: 18px; - height: 18px; - display: block; -} .row-left { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; } .row-eyebrow { font-size: 10px; diff --git a/resources/web/js/fuzzy-search.js b/resources/web/js/fuzzy-search.js index 36dd9edb10..66b2c7b520 100644 --- a/resources/web/js/fuzzy-search.js +++ b/resources/web/js/fuzzy-search.js @@ -34,24 +34,44 @@ function NormText(text, caseSensitive) { // Match a PRE-normalized haystack against a PRE-normalized needle (both produced by NormText with the // same caseSensitive flag). Skipping the per-character fold makes repeated matching (per keystroke over a // cached pool) cheap. Returns ranges in original coordinates, or null on no match. +// Prefers the most-contiguous (smallest-span) occurrence over greedy-leftmost: a scattered match that +// spans a stray earlier character is worse than a tight run later, so "orient" against a normalized +// "auto-orient" returns [[5,11]] (the word) not [[3,4],[6,11]]. A fully contiguous run is the optimum +// and short-circuits early. function FuzzyRangesNorm(haystackNorm, needleNorm) { const t = haystackNorm || ""; const needle = needleNorm || ""; if (!needle) return null; - const ranges = []; - let qi = 0; - for (let i = 0; i < t.length && qi < needle.length; i++) { - if (t[i] === needle[qi]) { - const last = ranges[ranges.length - 1]; - if (last && last[1] === i) - last[1] = i + 1; - else - ranges.push([i, i + 1]); - qi++; + const n = t.length, nl = needle.length; + let best = null; // {ranges, span, start} + for (let start = 0; start < n; start++) { + if (t[start] !== needle[0]) + continue; + let qi = 0; + const ranges = []; + let lastEnd = start; + for (let i = start; i < n && qi < nl; i++) { + if (t[i] === needle[qi]) { + const last = ranges[ranges.length - 1]; + if (last && last[1] === i) + last[1] = i + 1; + else + ranges.push([i, i + 1]); + lastEnd = i + 1; + qi++; + } + } + if (qi !== nl) + continue; + const span = lastEnd - start; + if (!best || span < best.span || (span === best.span && start < best.start)) { + best = { ranges, span, start }; + if (span === nl) + return ranges; // can't beat a fully contiguous run } } - return qi === needle.length ? ranges : null; + return best ? best.ranges : null; } function EscapeRegExp(value) { @@ -95,3 +115,18 @@ function WholeWordRanges(text, query, caseSensitive) { ranges.push([match.index, match.index + match[0].length]); return ranges.length > 0 ? ranges : null; } + +// Same whole-word (\b-bounded) match as WholeWordRanges, but against PRE-normalized haystack/needle +// (NormText output, so offsets stay length-aligned to the original text). Returns the first match as +// [[i, i+len]] in original coordinates, or null. Non-global so the caller can reuse one compiled regex +// across many fields without re-setting lastIndex. Skipping the per-char fold keeps the Speed Dial's +// per-keystroke scan over thousands of cached settings cheap. +function WholeWordRangesNorm(haystackNorm, needleNorm) { + const t = haystackNorm || ""; + const needle = needleNorm || ""; + if (!needle) + return null; + const re = new RegExp(`\\b${EscapeRegExp(needle)}\\b`); + const match = re.exec(t); + return match ? [[match.index, match.index + match[0].length]] : null; +} diff --git a/resources/web/js/fuzzy-search.test.js b/resources/web/js/fuzzy-search.test.js index 209045098e..48d2352907 100644 --- a/resources/web/js/fuzzy-search.test.js +++ b/resources/web/js/fuzzy-search.test.js @@ -5,7 +5,7 @@ const vm = require("vm"), assert = require("assert"), fs = require("fs"); const ctx = {}; vm.createContext(ctx); vm.runInContext(fs.readFileSync(__dirname + "/fuzzy-search.js", "utf8"), ctx); -const { FoldChar, Norm, EscapeRegExp, FuzzyRanges, WholeWordRanges } = ctx; +const { FoldChar, Norm, NormText, EscapeRegExp, FuzzyRanges, WholeWordRanges, FuzzyRangesNorm, WholeWordRangesNorm } = ctx; // FoldChar / Norm: accents fold, case only folds when case-insensitive. assert.equal(FoldChar("é"), "e"); @@ -34,4 +34,15 @@ assert.deepEqual(WholeWordRanges("a.b", "a", false), [[0, 1]]); // '.' is a assert.equal(EscapeRegExp("a.b*"), "a\\.b\\*"); assert.deepEqual(WholeWordRanges("c++ tool", "c", false), [[0, 1]]); // '+' would be a regex error unescaped +// FuzzyRangesNorm: prefers the most-contiguous (smallest-span) occurrence over greedy-leftmost, so a +// stray earlier character does not steal the highlight from a later tight word. +assert.deepEqual(FuzzyRangesNorm(NormText("Auto-Orient", false), NormText("orient", false)), [[5, 11]]); +// A contiguous run inside a larger word is also preferred over a scattered greedy pick. +assert.deepEqual(FuzzyRangesNorm(NormText("AutoOriented", false), NormText("orient", false)), [[4, 10]]); +assert.equal(FuzzyRangesNorm(NormText("Measure", false), NormText("xyz", false)), null); // no subsequence + +// WholeWordRangesNorm: \b-bounded literal against a pre-normalized haystack, offsets in original coords. +assert.deepEqual(WholeWordRangesNorm(NormText("Auto-Orient", false), NormText("orient", false)), [[5, 11]]); +assert.equal(WholeWordRangesNorm(NormText("AutoOriented", false), NormText("orient", false)), null); // inside a word + console.log("ok"); diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 7c25792bb3..e6bb2fa10a 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -127,6 +127,8 @@ set(SLIC3R_GUI_SOURCES GUI/SpeedDialDialog.hpp GUI/ActionRegistry.cpp GUI/ActionRegistry.hpp + GUI/NativeCommands.cpp + GUI/NativeCommands.hpp GUI/PluginsConfigDialog.cpp GUI/PluginsConfigDialog.hpp GUI/ProcessRunner.cpp diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index b55455f45d..92916eb538 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -1,41 +1,29 @@ #include "ActionRegistry.hpp" -#include "calib_dlg.hpp" -#include "GCodeViewer.hpp" -#include "GLCanvas3D.hpp" #include "GUI.hpp" #include "GUI_App.hpp" #include "I18N.hpp" -#include "IMSlider.hpp" #include "MainFrame.hpp" +#include "NativeCommands.hpp" #include "Notebook.hpp" #include "Plater.hpp" -#include "PlateSettingsDialog.hpp" #include "Search.hpp" #include "Tab.hpp" #include "slic3r/plugin/PluginManager.hpp" #include <libslic3r/AppConfig.hpp> #include <libslic3r/Config.hpp> -#include <libslic3r/PresetBundle.hpp> -#include <libslic3r/Utils.hpp> #include <slic3r/plugin/PythonPluginInterface.hpp> #include <wx/thread.h> -#include <boost/algorithm/string/predicate.hpp> -#include <boost/any.hpp> -#include <boost/algorithm/string/trim.hpp> #include <boost/filesystem.hpp> #include <boost/nowide/convert.hpp> #include <algorithm> -#include <cfloat> #include <cmath> -#include <cstdlib> #include <ctime> #include <exception> -#include <fstream> #include <iterator> #include <string> #include <unordered_map> @@ -145,6 +133,7 @@ constexpr const char* kOrcaSourceKey = "orca"; constexpr const char* kOrcaSourceName = "OrcaSlicer"; constexpr const char* kSettingPrefix = "orca_setting"; constexpr const char* kPlateGotoPrefix = "orca_plate_goto"; +constexpr const char* kRecentProjectPrefix = "orca_recent_project"; // Display context for a setting action's eyebrow, e.g. the "Process" in "Process : Quality : Layers". // Keyed by the option's preset type so the palette reads like the settings sidebar tabs. @@ -168,7 +157,7 @@ struct SettingAction : AppAction { std::string opt_key; Preset::Type type; - std::wstring category; // localized category, forwarded to jump_to_option + std::wstring category; // localized category, forwarded to jump_to_option static std::string id_for(const std::string& opt_key, Preset::Type type) { return std::string(kSettingPrefix) + ":" + opt_key + ":" + std::to_string(int(type)); } @@ -191,405 +180,32 @@ struct SettingAction : AppAction wxGetApp().sidebar().jump_to_option(opt_key, type, category); return {AppActionRunResult::Level::Success}; } - - // The current value's pattern pictogram (e.g. the selected infill pattern), for the search-result - // tile. Defined below after the icon helper it delegates to. - std::string icon() const override; }; -// 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(); - } -} - -// Select a named camera view ("top"/"front"/...); Plater::select_view dispatches to the current -// panel. Shared by the view_* speed-dial commands. -AppActionRunResult view_command(Plater* plater, const std::string& dir) -{ - if (plater) - plater->select_view(dir); - return {AppActionRunResult::Level::Success}; -} - -// 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) { - // Actually re-slice (respects the toolbar's current plate/all selection), then show the result. - plater->reslice(); - 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_tab" is two-phase: the palette collects the tab after activating it, so native - // dispatch here is a no-op (the jump goes through the go_to_tab web command). - if (command_key == "go_to_tab") - return {AppActionRunResult::Level::Success}; - - // ---- Slice -> Export pipeline. Each Plater method self-guards (empty model / error / - // background-invalid) and then opens its own save dialog / show_error, mirroring the File menu. - if (command_key == "export_gcode") { - if (plater) - plater->export_gcode(false); - return {AppActionRunResult::Level::Success}; - } - if (command_key == "export_stl") { - if (plater) - plater->export_stl(); - return {AppActionRunResult::Level::Success}; - } - if (command_key == "export_3mf") { - if (plater) - plater->export_core_3mf(); - return {AppActionRunResult::Level::Success}; - } - if (command_key == "export_sliced_file") { - if (plater) - plater->export_gcode_3mf(); - return {AppActionRunResult::Level::Success}; - } - if (command_key == "export_all_sliced_file") { - if (plater) - plater->export_gcode_3mf(true); - return {AppActionRunResult::Level::Success}; - } - - // ---- Calibration wizards. Each mirrors the menu handler (MainFrame.cpp): recreate the dialog - // fresh per launch. The palette hides itself and defers dispatch off the webview callback, so a - // ShowModal() here is safe (same path as open_preferences). The 3D panel is ensured below. - auto calib = [&](auto&& open) -> AppActionRunResult { - if (!plater) - return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")}; - // Auto-switch to the Prepare (3D) view instead of prompting: set the 3D panel - // synchronously (the wizard's new_project also re-establishes it) and select the - // Prepare notebook page so the tab label matches. The palette is hidden and this - // dispatch is deferred off the webview callback, so a modal on a switched tab is safe. - if (!plater->is_view3D_shown()) { - plater->select_view_3D("3D"); - if (MainFrame* mf = wxGetApp().mainframe; mf) - mf->select_tab(TAB_ID_PREPARE); - } - open(plater); - return {AppActionRunResult::Level::Success}; - }; - if (command_key == "calib_temperature") - return calib([](Plater* p) { - Temp_Calibration_Dlg* dlg = new Temp_Calibration_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); - dlg->ShowModal(); - dlg->Destroy(); - }); - if (command_key == "calib_max_volumetric") - return calib([](Plater* p) { - MaxVolumetricSpeed_Test_Dlg* dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); - dlg->ShowModal(); - dlg->Destroy(); - }); - if (command_key == "calib_pressure_advance") - return calib([](Plater* p) { - PA_Calibration_Dlg* dlg = new PA_Calibration_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); - dlg->ShowModal(); - dlg->Destroy(); - }); - if (command_key == "calib_flow_ratio") - return calib([](Plater* p) { - FlowRateCalibrationDialog* dlg = new FlowRateCalibrationDialog((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); - dlg->ShowModal(); - dlg->Destroy(); - }); - if (command_key == "calib_retraction") - return calib([](Plater* p) { - Retraction_Test_Dlg* dlg = new Retraction_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); - dlg->ShowModal(); - dlg->Destroy(); - }); - if (command_key == "calib_cornering") - return calib([](Plater* p) { - Cornering_Test_Dlg* dlg = new Cornering_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); - dlg->ShowModal(); - dlg->Destroy(); - }); - if (command_key == "calib_input_shaping_freq") - return calib([](Plater* p) { - Input_Shaping_Freq_Test_Dlg* dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); - dlg->ShowModal(); - dlg->Destroy(); - }); - if (command_key == "calib_input_shaping_damp") - return calib([](Plater* p) { - Input_Shaping_Damp_Test_Dlg* dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); - dlg->ShowModal(); - dlg->Destroy(); - }); - if (command_key == "calib_vfa") - return calib([](Plater* p) { - VFA_Test_Dlg* dlg = new VFA_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p); - dlg->ShowModal(); - dlg->Destroy(); - }); - - // ---- View controls. select_view dispatches to the current panel; named views + perspective - // toggle + fit-to-bed mirror the View menu items (MainFrame.cpp). reset_window_layout is direct. - if (command_key == "view_top") - return view_command(plater, "top"); - if (command_key == "view_bottom") - return view_command(plater, "bottom"); - if (command_key == "view_front") - return view_command(plater, "front"); - if (command_key == "view_rear") - return view_command(plater, "rear"); - if (command_key == "view_left") - return view_command(plater, "left"); - if (command_key == "view_right") - return view_command(plater, "right"); - if (command_key == "view_iso") - return view_command(plater, "iso"); - if (command_key == "view_default") { - if (plater) { - plater->select_view("plate"); - if (GLCanvas3D* canvas = plater->get_current_canvas3D()) - canvas->zoom_to_bed(); - } - return {AppActionRunResult::Level::Success}; - } - if (command_key == "view_fit_bed") { - if (plater) - if (GLCanvas3D* canvas = plater->get_current_canvas3D()) - canvas->zoom_to_bed(); - return {AppActionRunResult::Level::Success}; - } - if (command_key == "view_toggle_perspective") { - if (plater) - plater->get_camera().select_next_type(); - return {AppActionRunResult::Level::Success}; - } - if (command_key == "reset_window_layout") { - if (plater) - plater->reset_window_layout(); - return {AppActionRunResult::Level::Success}; - } - - // ---- Object / interaction operations (single-phase). Each mirrors a toolbar/menu action and is - // guarded by an existing can_* / selection check so nothing crashes on empty selection or a busy - // background worker, and returns a friendly Info instead. Structural ops self-update()/schedule a - // re-slice; transform ops (mirror/center/drop) post their own schedule-background event. We only - // need the underlying object (not a specific object index), so a non-capturing lambda is used as - // the guard/op pair below. Rotate/scale by angle/factor, duplicate (modal count dialog) and - // cut/segment/merge (unimplemented on Plater) are deliberately left out of this MVP. - auto obj = [&](bool (*ok)(Plater*), void (*op)(Plater*)) -> AppActionRunResult { - if (!plater) - return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")}; - // Object ops read the Prepare (3D) canvas selection, so ensure that view before guarding so - // a launch from the Preview/other tab doesn't report a spuriously empty selection. - if (!plater->is_view3D_shown()) { - plater->select_view_3D("3D"); - if (MainFrame* mf = wxGetApp().mainframe; mf) - mf->select_tab(TAB_ID_PREPARE); - } - if (!ok(plater)) - return {AppActionRunResult::Level::Info, _L("Select an object first.")}; - op(plater); - return {AppActionRunResult::Level::Success}; - }; - if (command_key == "obj_delete") - return obj([](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->remove_selected(); }); - if (command_key == "obj_delete_all") - return obj([](Plater* p) { return p->can_delete_all(); }, [](Plater* p) { p->delete_all_objects_from_model(); }); - if (command_key == "obj_mirror_x") - return obj([](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::X); }); - if (command_key == "obj_mirror_y") - return obj([](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Y); }); - if (command_key == "obj_mirror_z") - return obj([](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Z); }); - if (command_key == "obj_split_objects") - return obj([](Plater* p) { return p->can_split_to_objects(); }, [](Plater* p) { p->split_object(true); }); - if (command_key == "obj_split_parts") - return obj([](Plater* p) { return p->can_split_to_volumes(); }, [](Plater* p) { p->split_volume(); }); - if (command_key == "obj_center") - return obj([](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); }); - if (command_key == "obj_drop") - return obj([](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); }); - if (command_key == "obj_fit_volume") - return obj([](Plater* p) { return p->can_scale_to_print_volume(); }, [](Plater* p) { p->scale_selection_to_fit_print_volume(); }); - if (command_key == "obj_instances_up") - return obj([](Plater* p) { return p->can_increase_instances(); }, [](Plater* p) { p->increase_instances(); }); - if (command_key == "obj_instances_down") - return obj([](Plater* p) { return p->can_decrease_instances(); }, [](Plater* p) { p->decrease_instances(); }); - if (command_key == "obj_arrange") - return obj([](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->arrange(); }); - // Auto-orient has no dedicated can_*; can_arrange covers "objects exist + UI worker idle". - if (command_key == "obj_orient") - return obj([](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); }); - - // ---- Plate management. Plates are a filament (FFF) feature: SLA builds a single plate with - // no plate UI, and gcode-only mode has no editable project - so gate every plate op on FFF + - // the normal editor (mirroring where the plate toolbar/menu live). These act on the CURRENT - // plate (delete/duplicate take -1) except plate_goto, which jumps to the index in `param`. - auto plate_plater = [&]() -> Plater* { - return (plater && plater->printer_technology() == ptFFF && !plater->only_gcode_mode()) ? plater : nullptr; - }; - const AppActionRunResult plate_unavailable{AppActionRunResult::Level::Info, _L("Plates are a filament (FFF) feature.")}; - - if (command_key == "plate_add") { - if (Plater* p = plate_plater(); p) { - if (!p->can_add_plate()) - return {AppActionRunResult::Level::Info, _L("Cannot add another plate (maximum reached).")}; - p->add_plate(); - return {AppActionRunResult::Level::Success}; - } - return plate_unavailable; - } - if (command_key == "plate_duplicate") { - if (Plater* p = plate_plater(); p) { - if (!p->can_add_plate()) - return {AppActionRunResult::Level::Info, _L("Cannot duplicate a plate (maximum reached).")}; - p->duplicate_plate(); - return {AppActionRunResult::Level::Success}; - } - return plate_unavailable; - } - if (command_key == "plate_delete") { - if (Plater* p = plate_plater(); p) { - if (!p->can_delete_plate()) - return {AppActionRunResult::Level::Info, _L("Cannot delete the only plate.")}; - p->delete_plate(); - return {AppActionRunResult::Level::Success}; - } - return plate_unavailable; - } - if (command_key == "plate_rename") { - if (Plater* p = plate_plater(); p) { - PartPlate* curr = p->get_partplate_list().get_curr_plate(); - PlateNameEditDialog dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, _L("Edit Plate Name")); - dlg.set_plate_name(from_u8(curr->get_plate_name())); - if (dlg.ShowModal() == wxID_YES) - curr->set_plate_name(dlg.get_plate_name().ToUTF8().data()); - return {AppActionRunResult::Level::Success}; - } - return plate_unavailable; - } - if (command_key == "plate_toggle_lock") { - if (Plater* p = plate_plater(); p) { - PartPlateList& plates = p->get_partplate_list(); - const int index = plates.get_curr_plate_index(); - p->take_snapshot("lock partplate"); - plates.lock_plate(index, !plates.is_locked(index)); - return {AppActionRunResult::Level::Success}; - } - return plate_unavailable; - } - if (command_key == "plate_goto") { - if (Plater* p = plate_plater(); p) { - PartPlateList& plates = p->get_partplate_list(); - const int count = plates.get_plate_count(); - if (count <= 0) - return {AppActionRunResult::Level::Info, _L("No plates available.")}; - int index = 0; - try { - index = std::stoi(param); - } catch (const std::exception&) {} - index = std::clamp(index, 0, count - 1); - p->select_plate(index, false); - return {AppActionRunResult::Level::Success}; - } - return plate_unavailable; - } - 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). +// A built-in command action. Thin value: identity + presentation come from the NativeCommands +// catalog, and run() routes back to it - the catalog is the single source of truth for its +// behaviour. source_key is the constant "orca" so a renamed title never re-keys the action +// (matches the plugin source-key contract). struct CommandAction : AppAction { + static std::unique_ptr<CommandAction> make(const NativeCommand& c) + { return std::unique_ptr<CommandAction>(new CommandAction(c)); } + 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)) + AppActionRunResult run(const std::string& param) const override { return NativeCommands::run(command_key, param); } + +private: + explicit CommandAction(const NativeCommand& c) + : AppAction(kCommandPrefix, c.title, kOrcaSourceKey, kOrcaSourceName), command_key(c.key) { this->kind = AppActionKind::Command; - this->group = std::move(group); - this->input = std::move(input); + this->group = c.group; + this->input = c.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)); } - // A dynamic "Go to Plate N" action, one per live plate, rebuilt on every snapshot() (so a // rename/move immediately shows up). id is keyed by plate index, NOT the display title, so // renaming a plate never re-keys it - the same contract as SettingAction. A pinned "Go to @@ -610,174 +226,37 @@ struct PlateAction : AppAction } AppActionRunResult run(const std::string& /*param*/) const override - { return run_native_command("plate_goto", std::to_string(plate_index)); } + { return NativeCommands::run("plate_goto", std::to_string(plate_index)); } }; -// The built-in palette commands, registered once at init(). -std::vector<std::unique_ptr<AppAction>> native_commands() +// A dynamic "Open recent project <name>" action, one per recent project file, rebuilt on every +// snapshot() (like PlateAction) so the list always reflects the current recents. The id is keyed +// by the file PATH, NOT the display title - the same contract as SettingAction/PlateAction, so a +// rename of a project (or a reordered recents list) never re-keys the action. A pinned recent whose +// file is deleted simply stops resolving (visibleFavourites drops dead pins). run() loads the +// project through MainFrame::open_recent_project so the existing missing-file handling is reused. +struct RecentProjectAction : AppAction { - 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("Slice & Export"))); - // Two-phase commands: activating them collects input in the palette, then runs. Settings are - // not a command here - they're materialised as first-class SettingActions (see materialize_). - out.push_back(make_command("go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"), "percent")); - 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"))); + std::string file_path; - // Slice -> Export pipeline. Each runs a public Plater method; the methods self-guard (empty - // model / error / background-invalid) and open their own save dialog / show_error. - out.push_back(make_command("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"))); - out.push_back(make_command("export_stl", _u8L("Export STL"), _u8L("Slice & Export"))); - out.push_back(make_command("export_3mf", _u8L("Export 3MF"), _u8L("Slice & Export"))); - out.push_back(make_command("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"))); - out.push_back(make_command("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"))); + static std::string id_for(const std::string& path) + { return AppAction::compose_id(kRecentProjectPrefix, path, kOrcaSourceKey); } - // Calibration wizards (one command per dialog mirroring the Calibration menu, MainFrame.cpp). - out.push_back(make_command("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"))); - out.push_back(make_command("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"))); - out.push_back(make_command("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"))); - out.push_back(make_command("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"))); - out.push_back(make_command("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"))); - out.push_back(make_command("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"))); - out.push_back(make_command("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"))); - out.push_back(make_command("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"))); - out.push_back(make_command("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"))); + RecentProjectAction(std::string path, std::string title, std::string source) + : AppAction(AppActionId{id_for(path)}, std::move(title), kOrcaSourceKey, std::move(source)) + , file_path(std::move(path)) + { + this->kind = AppActionKind::Command; + this->group = _u8L("Recent Projects"); + } - // View controls (mirror the View menu; most duplicate the Ctrl+0..6 shortcuts). - out.push_back(make_command("view_top", _u8L("View: Top"), _u8L("View"))); - out.push_back(make_command("view_bottom", _u8L("View: Bottom"), _u8L("View"))); - out.push_back(make_command("view_front", _u8L("View: Front"), _u8L("View"))); - out.push_back(make_command("view_rear", _u8L("View: Rear"), _u8L("View"))); - out.push_back(make_command("view_left", _u8L("View: Left"), _u8L("View"))); - out.push_back(make_command("view_right", _u8L("View: Right"), _u8L("View"))); - out.push_back(make_command("view_iso", _u8L("View: Isometric"), _u8L("View"))); - out.push_back(make_command("view_default", _u8L("View: Default"), _u8L("View"))); - out.push_back(make_command("view_fit_bed", _u8L("Fit Bed to View"), _u8L("View"))); - out.push_back(make_command("view_toggle_perspective", _u8L("Toggle Perspective"), _u8L("View"))); - out.push_back(make_command("reset_window_layout", _u8L("Reset Window Layout"), _u8L("View"))); - - // Object operations (single-phase). Each maps to a public Plater method guarded by a can_* / - // selection check in run_native_command; structural ops self-update()/schedule re-slice. - out.push_back(make_command("obj_delete", _u8L("Delete Selected"), _u8L("Object"))); - out.push_back(make_command("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"))); - out.push_back(make_command("obj_mirror_x", _u8L("Mirror X"), _u8L("Object"))); - out.push_back(make_command("obj_mirror_y", _u8L("Mirror Y"), _u8L("Object"))); - out.push_back(make_command("obj_mirror_z", _u8L("Mirror Z"), _u8L("Object"))); - out.push_back(make_command("obj_split_objects", _u8L("Split to Objects"), _u8L("Object"))); - out.push_back(make_command("obj_split_parts", _u8L("Split to Parts"), _u8L("Object"))); - out.push_back(make_command("obj_center", _u8L("Center Selected on Plate"), _u8L("Object"))); - out.push_back(make_command("obj_drop", _u8L("Drop to Bed"), _u8L("Object"))); - out.push_back(make_command("obj_fit_volume", _u8L("Scale to Fit Print Volume"), _u8L("Object"))); - out.push_back(make_command("obj_instances_up", _u8L("Increase Instances"), _u8L("Object"))); - out.push_back(make_command("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object"))); - out.push_back(make_command("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object"))); - out.push_back(make_command("obj_orient", _u8L("Auto-Orient"), _u8L("Object"))); - - // Plate management. These act on the CURRENT plate (like Plater::delete_plate(-1)); the - // per-plate "Go to Plate N" actions are dynamic and materialised in materialize_plate_actions(). - out.push_back(make_command("plate_add", _u8L("Add Plate"), _u8L("Plate"))); - out.push_back(make_command("plate_duplicate", _u8L("Duplicate Plate"), _u8L("Plate"))); - out.push_back(make_command("plate_delete", _u8L("Delete Plate"), _u8L("Plate"))); - out.push_back(make_command("plate_rename", _u8L("Rename Plate"), _u8L("Plate"))); - out.push_back(make_command("plate_toggle_lock", _u8L("Toggle Plate Lock"), _u8L("Plate"))); - return out; -} - -// ---- setting action helpers -------------------------------------------------- - -// Self-contained base64 encoder (for the tiny pictogram SVGs), avoiding a dependency on the exact -// wxBase64Encode overload/return type across wx versions. -std::string base64_encode(const std::string& data) -{ - static const char* tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - auto enc = [&](unsigned n, int pad) { - // pad = number of extraneous bytes in the final group (0, 1 or 2): - // 0 leftover -> 4 chars from all 24 bits - // 2 leftover (pad=1) -> 3 chars then '=' - // 1 leftover (pad=2) -> 2 chars then "==" - // The '=' padding always comes LAST; a misplaced '=' decodes as garbage in the webview. - std::string out; - out.push_back(tbl[(n >> 18) & 63]); - out.push_back(tbl[(n >> 12) & 63]); - out.push_back(pad >= 2 ? '=' : tbl[(n >> 6) & 63]); - out.push_back(pad >= 1 ? '=' : tbl[n & 63]); - return out; - }; - std::string out; - out.reserve(((data.size() + 2) / 3) * 4); - size_t i = 0; - for (; i + 3 <= data.size(); i += 3) - out += enc(((unsigned char) data[i]) << 16 | ((unsigned char) data[i + 1]) << 8 | ((unsigned char) data[i + 2]), 0); - if (i + 1 == data.size()) - out += enc(((unsigned char) data[i]) << 16, 2); - else if (i + 2 == data.size()) - out += enc(((unsigned char) data[i]) << 16 | ((unsigned char) data[i + 1]) << 8, 1); - return out; -} - -// data:URI for the pattern pictogram icons/param_<key>.svg, or "" when there is no such icon. -// This mirrors the sidebar Choice field (Field.cpp add_item_bitmaps), which loads param_<value>.svg -// per enum value - most settings have no icon, only pattern-style enums (infill/support patterns). -// Base64 data URIs are used so the embedded webview renders them identically on every backend -// (no file:// subresource / CORS restrictions). -std::string setting_icon_for_key(const std::string& key) -{ - if (key.empty()) - return {}; - - const std::string path = (boost::filesystem::path(resources_dir()) / "images" / ("param_" + key + ".svg")).string(); - // Non-throwing stat: a throwing filesystem_error here would propagate out of snapshot() and - // abort the app (the palette opener). exists(fs ::error_code) never throws. - boost::system::error_code ec; - if (!boost::filesystem::exists(path, ec)) - return {}; - - std::ifstream in(path, std::ios::binary); - if (!in) - return {}; - std::string data((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>()); - if (data.empty()) - return {}; - - return "data:image/svg+xml;base64," + base64_encode(data); -} - -// The pattern pictogram for a setting's CURRENT value (its enum int), empty when it isn't a -// pattern-style enum or the value has no icon. Used for the search-result tile. -std::string setting_action_icon(const SettingAction& a) -{ - Tab* tab = wxGetApp().get_tab(a.type); - if (!tab || !tab->get_config()) - return {}; - DynamicPrintConfig* config = tab->get_config(); - const ConfigOptionDef* def = config->def()->get(a.opt_key); - if (!def || def->type != coEnum || (int(def->type) & int(coVectorType)) != 0) - return {}; - // Read the value WITHOUT config->opt_int(): the non-const overload routes through a type-checked - // option<ConfigOptionInt>() that returns null for enum values (type() is coEnum, not coInt) and - // would deref null. Pull the ConfigOption* and dynamic_cast instead (succeeds: enums derive from - // ConfigOptionInt), falling back to the def default when the option is absent. - const ConfigOption* opt = (config->has(a.opt_key) ? config->option(a.opt_key) : def->default_value.get()); - const ConfigOptionInt* int_opt = dynamic_cast<const ConfigOptionInt*>(opt); - if (!int_opt) - return {}; - const int value = int_opt->getInt(); - if (def->enum_keys_map) - for (const auto& kv : *def->enum_keys_map) - if (kv.second == value) - return setting_icon_for_key(kv.first); - return {}; -} - -std::string SettingAction::icon() const { return setting_action_icon(*this); } + AppActionRunResult run(const std::string& /*param*/) const override + { + if (MainFrame* mf = wxGetApp().mainframe; mf) + mf->open_recent_project(size_t(-1), wxString::FromUTF8(file_path)); + return {AppActionRunResult::Level::Success}; + } +}; } // namespace @@ -838,9 +317,10 @@ void ActionRegistry::init() // 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)); + // by prefix, so this is order-independent. The catalog lives in NativeCommands - the registry + // only materialises thin CommandAction values from it. + for (const NativeCommand& c : NativeCommands::catalog()) + upsert(CommandAction::make(c)); } void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange change) @@ -1060,6 +540,7 @@ void ActionRegistry::materialize_setting_actions() // (above) is the single display/search breadcrumb rather than being duplicated. auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, boost::nowide::narrow(label_w), std::string(), opt.category_local, boost::nowide::narrow(path)); + action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); if (auto it = stats.find(id); it != stats.end() && it->is_object()) { action->count = it->value("count", 0); @@ -1137,6 +618,55 @@ void ActionRegistry::materialize_plate_actions() } } +void ActionRegistry::materialize_recent_project_actions() +{ + assert(wxThread::IsMain()); + + // Persisted per-action state, read ONCE (mirrors materialize_plate_actions) so a relisted recent + // project keeps its recency/favourite when the recents list reorders - the id is path-keyed. + nlohmann::json stats = read_section("stats", nlohmann::json::object()); + if (!stats.is_object()) + stats = nlohmann::json::object(); + const std::vector<std::string> favs = favourite_ids(); + + // app_config stores recents oldest-first; the palette shows newest-first. + std::vector<std::string> recents = wxGetApp().app_config->get_recent_projects(); + std::reverse(recents.begin(), recents.end()); + + std::unordered_set<std::string> seen; + for (const std::string& path : recents) { + // Skip projects whose file is gone; the stale id is dropped below. + boost::system::error_code ec; + if (path.empty() || !boost::filesystem::exists(boost::filesystem::path(path), ec)) + continue; + + const std::string id = RecentProjectAction::id_for(path); + seen.insert(id); + + // Title = file basename; source/eyebrow = the full path so search can match either. + boost::filesystem::path p(path); + std::string title = p.filename().string(); + if (title.empty()) + title = path; + + auto action = std::make_unique<RecentProjectAction>(path, std::move(title), path); + action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); + if (auto it = stats.find(id); it != stats.end() && it->is_object()) { + action->count = it->value("count", 0); + action->last = it->value("last", 0LL); + } + m_actions.insert_or_assign(action->id(), std::shared_ptr<AppAction>(std::move(action))); + } + + // Drop recent-project actions whose file no longer exists / was removed from the recents list. + for (auto it = m_actions.begin(); it != m_actions.end();) { + if (it->first.rfind(kRecentProjectPrefix, 0) == 0 && !seen.count(it->first)) + it = m_actions.erase(it); + else + ++it; + } +} + bool ActionRegistry::should_ask(const std::string& id) const { assert(wxThread::IsMain()); @@ -1163,6 +693,7 @@ nlohmann::json ActionRegistry::snapshot() // the palette opens). materialize_setting_actions(); materialize_plate_actions(); + materialize_recent_project_actions(); std::vector<const AppAction*> sorted; sorted.reserve(m_actions.size()); @@ -1188,8 +719,7 @@ nlohmann::json ActionRegistry::snapshot() {"source", a->source_name()}, {"group", a->group}, {"input", a->input}, - {"shortcut", ""}, - {"icon", a->icon()}}); + {"shortcut", ""}}); }; nlohmann::json actions = nlohmann::json::array(); diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index 6daafdde56..678de6986d 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -81,11 +81,6 @@ struct AppAction // commands (e.g. a layer percentage); plugins ignore it. virtual AppActionRunResult run(const std::string& param = {}) const = 0; - // Optional data:URI for a small pictogram to show in the palette row/tile/the editor - // (e.g. the current infill/pattern). Empty string = fall back to the monogram. Only - // SettingAction overrides this; the base returns an empty string. - virtual std::string icon() const { return {}; } - protected: // The definition is constructor-set and immutable. Refreshes replace an action // instead of mutating identity after the registry has indexed it by id. @@ -187,6 +182,11 @@ private: // plate UI, so nothing is materialised and stale ids are dropped. Called at the top of snapshot(). void materialize_plate_actions(); + // (Re)materialise one "Open recent project <name>" action per recent project file, so the + // palette lists every recent project and can load it by clicking. Keyed by file path (stable); + // files that no longer exist are skipped and their stale ids dropped. Called at the top of snapshot(). + void materialize_recent_project_actions(); + // Loader callbacks (marshalled to the UI thread) land here. refresh_source rebuilds // one plugin's whole action set; refresh_capability touches a single capability. void refresh_source(const std::string& plugin_key, ActionChange change); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index f607f39fb9..4ba82fd402 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -3440,88 +3440,51 @@ void MainFrame::init_menubar_as_editor() // Temperature append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Temperature"), _L("Temperature Calibration"), - [this](wxCommandEvent&) { - if (!m_temp_calib_dlg) - m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater); - m_temp_calib_dlg->ShowModal(); - }, "", nullptr, + [this](wxCommandEvent&) { run_calibration(CalibKind::Temperature); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); // Max Volumetric Speed append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Max flowrate"), _L("Max flowrate"), - [this](wxCommandEvent&) { - if (!m_vol_test_dlg) - m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater); - m_vol_test_dlg->ShowModal(); - }, "", nullptr, + [this](wxCommandEvent&) { run_calibration(CalibKind::MaxVolumetric); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); // Pressure Advance append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Pressure advance"), _L("Pressure advance"), - [this](wxCommandEvent&) { - if (!m_pa_calib_dlg) - m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater); - m_pa_calib_dlg->ShowModal(); - }, "", nullptr, + [this](wxCommandEvent&) { run_calibration(CalibKind::PressureAdvance); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); // Flow rate (Wizard Dialog) append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"), - [this](wxCommandEvent&) { - if (!m_plater) return; - if (!m_flow_rate_calib_dlg) - m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*)this, wxID_ANY, m_plater); - m_flow_rate_calib_dlg->ShowModal(); - }, "", nullptr, + [this](wxCommandEvent&) { run_calibration(CalibKind::FlowRatio); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); // Retraction append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Retraction"), _L("Retraction"), - [this](wxCommandEvent&) { - if (!m_retraction_calib_dlg) - m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater); - m_retraction_calib_dlg->ShowModal(); - }, "", nullptr, + [this](wxCommandEvent&) { run_calibration(CalibKind::Retraction); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); // Cornering append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Cornering"), _L("Cornering calibration"), - [this](wxCommandEvent&) { - auto dlg = new Cornering_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater); - dlg->ShowModal(); - dlg->Destroy(); - }, "", nullptr, + [this](wxCommandEvent&) { run_calibration(CalibKind::Cornering); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); // Input Shaping (with submenu) auto input_shaping_menu = new wxMenu(); append_menu_item( input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"), - [this](wxCommandEvent&) { - auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater); - dlg->ShowModal(); - dlg->Destroy(); - }, + [this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingFreq); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); append_menu_item( input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"), - [this](wxCommandEvent&) { - auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater); - dlg->ShowModal(); - dlg->Destroy(); - }, + [this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingDamp); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); m_topbar->GetCalibMenu()->AppendSubMenu(input_shaping_menu, _L("Input Shaping")); // VFA append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("VFA"), _L("VFA"), - [this](wxCommandEvent&) { - if (!m_vfa_test_dlg) - m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater); - m_vfa_test_dlg->ShowModal(); - }, "", nullptr, + [this](wxCommandEvent&) { run_calibration(CalibKind::VFA); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); // help @@ -3582,89 +3545,52 @@ void MainFrame::init_menubar_as_editor() // Temperature append_menu_item(calib_menu, wxID_ANY, _L("Temperature"), _L("Temperature"), - [this](wxCommandEvent&) { - if (!m_temp_calib_dlg) - m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater); - m_temp_calib_dlg->ShowModal(); - }, "", nullptr, + [this](wxCommandEvent&) { run_calibration(CalibKind::Temperature); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); // Max Volumetric Speed append_menu_item(calib_menu, wxID_ANY, _L("Max flowrate"), _L("Max flowrate"), - [this](wxCommandEvent&) { - if (!m_vol_test_dlg) - m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater); - m_vol_test_dlg->ShowModal(); - }, "", nullptr, + [this](wxCommandEvent&) { run_calibration(CalibKind::MaxVolumetric); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); // Pressure Advance append_menu_item(calib_menu, wxID_ANY, _L("Pressure advance"), _L("Pressure advance"), - [this](wxCommandEvent&) { - if (!m_pa_calib_dlg) - m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater); - m_pa_calib_dlg->ShowModal(); - }, "", nullptr, + [this](wxCommandEvent&) { run_calibration(CalibKind::PressureAdvance); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); // Flowrate (with submenu) // ORCA: Flow rate (Wizard Dialog) append_menu_item(calib_menu, wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"), - [this](wxCommandEvent&) { - if (!m_plater) return; - if (!m_flow_rate_calib_dlg) - m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*)this, wxID_ANY, m_plater); - m_flow_rate_calib_dlg->ShowModal(); - }, "", nullptr, + [this](wxCommandEvent&) { run_calibration(CalibKind::FlowRatio); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); // Retraction append_menu_item(calib_menu, wxID_ANY, _L("Retraction"), _L("Retraction"), - [this](wxCommandEvent&) { - if (!m_retraction_calib_dlg) - m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater); - m_retraction_calib_dlg->ShowModal(); - }, "", nullptr, + [this](wxCommandEvent&) { run_calibration(CalibKind::Retraction); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); // Cornering append_menu_item(calib_menu, wxID_ANY, _L("Cornering"), _L("Cornering calibration"), - [this](wxCommandEvent&) { - auto dlg = new Cornering_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater); - dlg->ShowModal(); - dlg->Destroy(); - }, "", nullptr, + [this](wxCommandEvent&) { run_calibration(CalibKind::Cornering); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); // Input Shaping (with submenu) auto input_shaping_menu = new wxMenu(); append_menu_item( input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"), - [this](wxCommandEvent&) { - auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater); - dlg->ShowModal(); - dlg->Destroy(); - }, + [this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingFreq); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); append_menu_item( input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"), - [this](wxCommandEvent&) { - auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater); - dlg->ShowModal(); - dlg->Destroy(); - }, + [this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingDamp); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); calib_menu->AppendSubMenu(input_shaping_menu, _L("Input Shaping")); // VFA append_menu_item(calib_menu, wxID_ANY, _L("VFA"), _L("VFA"), - [this](wxCommandEvent&) { - if (!m_vfa_test_dlg) - m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater); - m_vfa_test_dlg->ShowModal(); - }, "", nullptr, + [this](wxCommandEvent&) { run_calibration(CalibKind::VFA); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); // help append_menu_item(calib_menu, wxID_ANY, _L("Calibration Guide"), _L("Calibration Guide"), @@ -4424,7 +4350,73 @@ void MainFrame::technology_changed() // update menu titles PrinterTechnology pt = plater()->printer_technology(); if (int id = m_menubar->FindMenu(pt == ptFFF ? _omitL("Material Settings") : _L("Filament settings")); id != wxNOT_FOUND) - m_menubar->SetMenuLabel(id, pt == ptSLA ? _omitL("Material Settings") : _L("Filament settings")); + m_menubar->SetMenuLabel(id, pt == ptFFF ? _omitL("Material Settings") : _L("Filament settings")); +} + +// Opens the calibration wizard for `calib_kind`, reusing the cached member dialogs the Calibration +// menu builds. This is the single source of truth for the wizard lifecycle: the Calibration menu +// handlers and the Speed Dial native commands both call it, so they share the same per-wizard +// member (fresh on first launch, reused thereafter). Call while the Prepare (3D) panel is shown. +void MainFrame::run_calibration(CalibKind calib_kind) +{ + switch (calib_kind) { + case CalibKind::Temperature: { + if (!m_temp_calib_dlg) + m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*) this, wxID_ANY, m_plater); + m_temp_calib_dlg->ShowModal(); + break; + } + case CalibKind::MaxVolumetric: { + if (!m_vol_test_dlg) + m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater); + m_vol_test_dlg->ShowModal(); + break; + } + case CalibKind::PressureAdvance: { + if (!m_pa_calib_dlg) + m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*) this, wxID_ANY, m_plater); + m_pa_calib_dlg->ShowModal(); + break; + } + case CalibKind::FlowRatio: { + if (!m_plater) + break; + if (!m_flow_rate_calib_dlg) + m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*) this, wxID_ANY, m_plater); + m_flow_rate_calib_dlg->ShowModal(); + break; + } + case CalibKind::Retraction: { + if (!m_retraction_calib_dlg) + m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater); + m_retraction_calib_dlg->ShowModal(); + break; + } + case CalibKind::Cornering: { + auto dlg = new Cornering_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater); + dlg->ShowModal(); + dlg->Destroy(); + break; + } + case CalibKind::InputShapingFreq: { + auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater); + dlg->ShowModal(); + dlg->Destroy(); + break; + } + case CalibKind::InputShapingDamp: { + auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater); + dlg->ShowModal(); + dlg->Destroy(); + break; + } + case CalibKind::VFA: { + if (!m_vfa_test_dlg) + m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater); + m_vfa_test_dlg->ShowModal(); + break; + } + } } diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index b8b7ef2e7d..fc23e1dad8 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -106,6 +106,21 @@ protected: void on_dpi_changed(const wxRect& suggested_rect) override; }; +// Calibration wizard identity, shared by MainFrame::run_calibration and the Speed Dial command runners. +// Kept in order with the wizard list below. +enum class CalibKind : int +{ + Temperature, + MaxVolumetric, + PressureAdvance, + FlowRatio, + Retraction, + Cornering, + InputShapingFreq, + InputShapingDamp, + VFA +}; + class MainFrame : public DPIFrame { #ifdef __APPLE__ @@ -349,6 +364,11 @@ public: void technology_changed(); + // Opens the calibration wizard for `kind`, reusing the cached member dialogs the Calibration + // menu builds (m_*_calib_dlg). Single source of truth for the wizard lifecycle: the Calibration + // menu handlers and the Speed Dial native commands both call this. Call while the Prepare (3D) + // panel is shown (menu items are gated on is_view3D_shown; the speed dial ensures it first). + void run_calibration(CalibKind calib_kind); //BBS void load_url(wxString url); diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp new file mode 100644 index 0000000000..e4c4449cf6 --- /dev/null +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -0,0 +1,500 @@ +#include "NativeCommands.hpp" + +#include "calib_dlg.hpp" +#include "Camera.hpp" +#include "GCodeViewer.hpp" +#include "GLCanvas3D.hpp" +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "I18N.hpp" +#include "IMSlider.hpp" +#include "MainFrame.hpp" +#include "Plater.hpp" +#include "PlateSettingsDialog.hpp" +#include "DeviceCore/DevManager.h" + +#include <libslic3r/Utils.hpp> + +#include <algorithm> +#include <cmath> +#include <cstdlib> +#include <exception> +#include <string> +#include <tuple> +#include <utility> + +namespace Slic3r { namespace GUI { + +namespace { + +// Plate ops are an FFF feature: SLA has a single plate and no plate UI, gcode-only mode has no +// editable project - so gate every plate op on FFF + the normal editor. +bool is_fff_plater(Plater* plater) { return plater && plater->printer_technology() == ptFFF && !plater->only_gcode_mode(); } + +AppActionRunResult plate_unavailable() { return {AppActionRunResult::Level::Info, _L("Plates are a filament (FFF) feature.")}; } + +// Switch to the Prepare (3D) panel so object/calibration ops have a live canvas + selection, and +// the notebook page label matches. A no-op when the 3D panel is already shown. +void ensure_3d_view(Plater* plater) +{ + if (plater && !plater->is_view3D_shown()) { + plater->select_view_3D("3D"); + if (MainFrame* mf = wxGetApp().mainframe; mf) + mf->select_tab(TAB_ID_PREPARE); + } +} + +// Object op guard + run: object ops read the Prepare canvas selection, so ensure that view first so +// a launch from another tab doesn't report a spuriously empty selection. +AppActionRunResult object_op(Plater* plater, bool (*ok)(Plater*), void (*op)(Plater*)) +{ + if (!plater) + return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")}; + ensure_3d_view(plater); + if (!ok(plater)) + return {AppActionRunResult::Level::Info, _L("Select an object first.")}; + op(plater); + return {AppActionRunResult::Level::Success}; +} + +// 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; 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; + + const double max = double(layers->GetMaxValue()); + const int target = int(std::lround(pct / 100.0 * max)); + layers->SetHigherValue(target); + if (layers->is_one_layer()) + layers->SetLowerValue(target); + layers->set_as_dirty(); + if (moves) { + moves->SetHigherValue(moves->GetMaxValue()); + moves->set_as_dirty(); + } +} + +// Select a named camera view. Plater::select_view dispatches to the current panel. +AppActionRunResult view_command(Plater* plater, const std::string& dir) +{ + if (plater) + plater->select_view(dir); + return {AppActionRunResult::Level::Success}; +} + +// Calibration wizards. Reuses MainFrame::run_calibration so the speed dial shows the same cached +// member dialogs as the Calibration menu (the menu handlers call run_calibration too). +AppActionRunResult calib_command(CalibKind kind) +{ + MainFrame* mf = wxGetApp().mainframe; + if (!mf) + return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")}; + ensure_3d_view(wxGetApp().plater()); + mf->run_calibration(kind); + return {AppActionRunResult::Level::Success}; +} + +std::vector<NativeCommand> build_command_catalog() +{ + std::vector<NativeCommand> out; + auto add = [&](std::string key, std::string title, std::string group, + std::function<AppActionRunResult(const std::string&)> runner, std::string input = {}) { + out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(runner)}); + }; + + // ---- Slice & Export ---- + add("slice_and_preview", _u8L("Slice and Preview"), _u8L("Slice & Export"), [](const std::string&) { + Plater* plater = wxGetApp().plater(); + if (plater) { + plater->reslice(); + plater->select_view_3D("Preview", false); + if (MainFrame* mf = wxGetApp().mainframe; mf) + mf->select_tab(TAB_ID_PREVIEW); + } + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + + add( + "go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"), + [](const std::string& param) { + Plater* plater = wxGetApp().plater(); + if (plater) { + plater->select_view_3D("Preview", false); + if (MainFrame* mf = wxGetApp().mainframe; mf) + mf->select_tab(TAB_ID_PREVIEW); + go_to_layer(plater, param); + } + return AppActionRunResult{AppActionRunResult::Level::Success}; + }, + "percent"); + + // "go_to_tab" is two-phase: the palette collects the tab after activating it, so dispatch here + // is a no-op (the jump goes through the go_to_tab web command). + add( + "go_to_tab", _u8L("Go to tab..."), _u8L("Commands"), + [](const std::string&) { return AppActionRunResult{AppActionRunResult::Level::Success}; }, "tab"); + + add("load_project", _u8L("Load Project"), _u8L("Commands"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->load_project(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("save_project", _u8L("Save Project"), _u8L("Commands"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->save_project(false); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("save_project_as", _u8L("Save Project As"), _u8L("Commands"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->save_project(true); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("open_preferences", _u8L("Preferences"), _u8L("Commands"), [](const std::string&) { + wxGetApp().open_preferences(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + + // ---- Mode ---- + add("mode_simple", _u8L("Mode: Simple"), _u8L("Mode"), [](const std::string&) { + wxGetApp().save_mode(comSimple); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode"), [](const std::string&) { + wxGetApp().save_mode(comAdvanced); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("mode_expert", _u8L("Mode: Expert"), _u8L("Mode"), [](const std::string&) { + wxGetApp().save_mode(comExpert); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + + // ---- Export pipeline ---- + add("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->export_gcode(false); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("export_stl", _u8L("Export STL"), _u8L("Slice & Export"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->export_stl(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("export_3mf", _u8L("Export 3MF"), _u8L("Slice & Export"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->export_core_3mf(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->export_gcode_3mf(false); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), + [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->export_gcode_3mf(true); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + + // ---- Calibration ---- + add("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), + [](const std::string&) { return calib_command(CalibKind::Temperature); }); + add("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"), + [](const std::string&) { return calib_command(CalibKind::MaxVolumetric); }); + add("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"), + [](const std::string&) { return calib_command(CalibKind::PressureAdvance); }); + add("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"), + [](const std::string&) { return calib_command(CalibKind::FlowRatio); }); + add("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"), + [](const std::string&) { return calib_command(CalibKind::Retraction); }); + add("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"), + [](const std::string&) { return calib_command(CalibKind::Cornering); }); + add("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"), + [](const std::string&) { return calib_command(CalibKind::InputShapingFreq); }); + add("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), + [](const std::string&) { return calib_command(CalibKind::InputShapingDamp); }); + add("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), + [](const std::string&) { return calib_command(CalibKind::VFA); }); + + // ---- View ---- + for (auto [key, dir, title] : + std::initializer_list<std::tuple<const char*, const char*, const char*>>{{"view_top", "top", "View: Top"}, + {"view_bottom", "bottom", "View: Bottom"}, + {"view_front", "front", "View: Front"}, + {"view_rear", "rear", "View: Rear"}, + {"view_left", "left", "View: Left"}, + {"view_right", "right", "View: Right"}, + {"view_iso", "iso", "View: Isometric"}}) { + std::string k = key, d = dir; + add(k, Slic3r::GUI::I18N::translate_utf8(title), _u8L("View"), + [d](const std::string&) { return view_command(wxGetApp().plater(), d); }); + } + add("view_default", _u8L("View: Default"), _u8L("View"), [](const std::string&) { + Plater* plater = wxGetApp().plater(); + if (plater) { + plater->select_view("plate"); + if (GLCanvas3D* canvas = plater->get_current_canvas3D()) + canvas->zoom_to_bed(); + } + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("view_fit_bed", _u8L("Fit Bed to View"), _u8L("View"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + if (GLCanvas3D* canvas = plater->get_current_canvas3D()) + canvas->zoom_to_bed(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("view_toggle_perspective", _u8L("Toggle Perspective"), _u8L("View"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->get_camera().select_next_type(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("reset_window_layout", _u8L("Reset Window Layout"), _u8L("View"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->reset_window_layout(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + + // ---- Object ---- + add("obj_delete", _u8L("Delete Selected"), _u8L("Object"), [](const std::string&) { + return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->remove_selected(); }); + }); + add("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"), [](const std::string&) { + return object_op( + wxGetApp().plater(), [](Plater* p) { return p->can_delete_all(); }, [](Plater* p) { p->delete_all_objects_from_model(); }); + }); + add("obj_mirror_x", _u8L("Mirror X"), _u8L("Object"), [](const std::string&) { + return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::X); }); + }); + add("obj_mirror_y", _u8L("Mirror Y"), _u8L("Object"), [](const std::string&) { + return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Y); }); + }); + add("obj_mirror_z", _u8L("Mirror Z"), _u8L("Object"), [](const std::string&) { + return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Z); }); + }); + add("obj_split_objects", _u8L("Split to Objects"), _u8L("Object"), [](const std::string&) { + return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_objects(); }, [](Plater* p) { p->split_object(true); }); + }); + add("obj_split_parts", _u8L("Split to Parts"), _u8L("Object"), [](const std::string&) { + return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_volumes(); }, [](Plater* p) { p->split_volume(); }); + }); + add("obj_center", _u8L("Center Selected on Plate"), _u8L("Object"), [](const std::string&) { + return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); }); + }); + add("obj_drop", _u8L("Drop to Bed"), _u8L("Object"), [](const std::string&) { + return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); }); + }); + add("obj_fit_volume", _u8L("Scale to Fit Print Volume"), _u8L("Object"), [](const std::string&) { + return object_op( + wxGetApp().plater(), [](Plater* p) { return p->can_scale_to_print_volume(); }, + [](Plater* p) { p->scale_selection_to_fit_print_volume(); }); + }); + add("obj_instances_up", _u8L("Increase Instances"), _u8L("Object"), [](const std::string&) { + return object_op( + wxGetApp().plater(), [](Plater* p) { return p->can_increase_instances(); }, [](Plater* p) { p->increase_instances(); }); + }); + add("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object"), [](const std::string&) { + return object_op( + wxGetApp().plater(), [](Plater* p) { return p->can_decrease_instances(); }, [](Plater* p) { p->decrease_instances(); }); + }); + add("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object"), [](const std::string&) { + return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->arrange(); }); + }); + add("obj_orient", _u8L("Auto-Orient"), _u8L("Object"), [](const std::string&) { + return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); }); + }); + + // ---- Plate ---- + add("plate_add", _u8L("Add Plate"), _u8L("Plate"), [](const std::string&) { + Plater* plater = wxGetApp().plater(); + if (!is_fff_plater(plater)) + return plate_unavailable(); + if (!plater->can_add_plate()) + return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot add another plate (maximum reached).")}; + plater->add_plate(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("plate_duplicate", _u8L("Duplicate Plate"), _u8L("Plate"), [](const std::string&) { + Plater* plater = wxGetApp().plater(); + if (!is_fff_plater(plater)) + return plate_unavailable(); + if (!plater->can_add_plate()) + return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot duplicate a plate (maximum reached).")}; + plater->duplicate_plate(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("plate_delete", _u8L("Delete Plate"), _u8L("Plate"), [](const std::string&) { + Plater* plater = wxGetApp().plater(); + if (!is_fff_plater(plater)) + return plate_unavailable(); + if (!plater->can_delete_plate()) + return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot delete the only plate.")}; + plater->delete_plate(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("plate_rename", _u8L("Rename Plate"), _u8L("Plate"), [](const std::string&) { + Plater* plater = wxGetApp().plater(); + if (!is_fff_plater(plater)) + return plate_unavailable(); + PartPlate* curr = plater->get_partplate_list().get_curr_plate(); + PlateNameEditDialog dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, _L("Edit Plate Name")); + dlg.set_plate_name(from_u8(curr->get_plate_name())); + if (dlg.ShowModal() == wxID_YES) + curr->set_plate_name(dlg.get_plate_name().ToUTF8().data()); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("plate_toggle_lock", _u8L("Toggle Plate Lock"), _u8L("Plate"), [](const std::string&) { + Plater* plater = wxGetApp().plater(); + if (!is_fff_plater(plater)) + return plate_unavailable(); + PartPlateList& plates = plater->get_partplate_list(); + const int index = plates.get_curr_plate_index(); + plater->take_snapshot("lock partplate"); + plates.lock_plate(index, !plates.is_locked(index)); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("plate_goto", _u8L("Go to Plate"), _u8L("Plate"), [](const std::string& param) { + Plater* plater = wxGetApp().plater(); + if (!is_fff_plater(plater)) + return plate_unavailable(); + PartPlateList& plates = plater->get_partplate_list(); + const int count = plates.get_plate_count(); + if (count <= 0) + return AppActionRunResult{AppActionRunResult::Level::Info, _L("No plates available.")}; + int index = 0; + try { + index = std::stoi(param); + } catch (const std::exception&) {} + index = std::clamp(index, 0, count - 1); + plater->select_plate(index, false); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + + // ---- Printer / device connection ---- + add("connect_printer", _u8L("Connect Printer"), _u8L("Printer"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->connect_to_printer(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("disconnect_printer", _u8L("Disconnect Printer"), _u8L("Printer"), [](const std::string&) { + DeviceManager* dev = wxGetApp().getDeviceManager(); + if (!dev) + return AppActionRunResult{AppActionRunResult::Level::Info, _L("Printer connection is unavailable.")}; + if (MachineObject* machine = dev->get_selected_machine()) { + machine->disconnect(); + dev->set_selected_machine(""); + return AppActionRunResult{AppActionRunResult::Level::Success}; + } + return AppActionRunResult{AppActionRunResult::Level::Info, _L("Printer is not connected.")}; + }); + add("sync_ams", _u8L("Synchronize Filament List from AMS"), _u8L("Printer"), [](const std::string&) { + Plater* plater = wxGetApp().plater(); + DeviceManager* dev = wxGetApp().getDeviceManager(); + if (dev && dev->get_selected_machine() && plater) { + plater->sidebar().sync_ams_list(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + } + return AppActionRunResult{AppActionRunResult::Level::Info, _L("Connect a printer to synchronize the AMS filament list.")}; + }); + + // ---- Presets / cloud ---- + add("preset_bundle", _u8L("Open Preset Bundle"), _u8L("Presets"), [](const std::string&) { + wxGetApp().open_presetbundledialog(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("sync_presets", _u8L("Sync Presets"), _u8L("Presets"), [](const std::string&) { + if (!wxGetApp().is_user_login()) + return AppActionRunResult{AppActionRunResult::Level::Info, _L("Sign in to sync presets.")}; + wxGetApp().restart_sync_user_preset(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + + // ---- Import ---- + add("import_file", _u8L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), _u8L("Import"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) { +#ifdef __APPLE__ + plater->add_model(); +#else + plater->add_file(); +#endif + } + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("import_zip_archive", _u8L("Import ZIP Archive"), _u8L("Import"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->import_zip_archive(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("import_configs", _u8L("Import Configs"), _u8L("Import"), [](const std::string&) { + if (MainFrame* mf = wxGetApp().mainframe) + mf->load_config_file(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + + // ---- Export extras ---- + add("export_stl_multi", _u8L("Export All Objects as STLs"), _u8L("Export"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->export_stl(false, false, true); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("export_drc_single", _u8L("Export All Objects as DRC (one file)"), _u8L("Export"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->export_stl(false, false, false, FT_DRC); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("export_drc_multi", _u8L("Export All Objects as DRCs"), _u8L("Export"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->export_stl(false, false, true, FT_DRC); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("export_toolpaths_obj", _u8L("Export Toolpaths as OBJ"), _u8L("Export"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->export_toolpaths_to_obj(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("export_config", _u8L("Export Preset Bundle"), _u8L("Export"), [](const std::string&) { + if (MainFrame* mf = wxGetApp().mainframe) + mf->export_config(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + + return out; +} + +} // namespace + +const std::vector<NativeCommand>& NativeCommands::catalog() +{ + static const std::vector<NativeCommand> commands = build_command_catalog(); + return commands; +} + +AppActionRunResult NativeCommands::run(const std::string& key, const std::string& param) +{ + GUI_App& app = wxGetApp(); + if (app.is_closing()) + return {}; + for (const NativeCommand& c : catalog()) + if (c.key == key) + return c.runner(param); + return {AppActionRunResult::Level::Info, _L("Unknown command.")}; +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/NativeCommands.hpp b/src/slic3r/GUI/NativeCommands.hpp new file mode 100644 index 0000000000..80c431364d --- /dev/null +++ b/src/slic3r/GUI/NativeCommands.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include <functional> +#include <string> +#include <vector> + +#include "ActionRegistry.hpp" // for AppActionRunResult + +namespace Slic3r { namespace GUI { + +// A built-in speed-dial command: identity + how to run it. The registry keeps commands as thin +// values (CommandAction) and routes run() here, so this catalog is the single source of truth for +// the behaviour (runner => an owner method) and the presentation (title/group/input). +struct NativeCommand +{ + std::string key; + std::string title; + std::string group; + std::string input; // "settings"/"percent"/"tab" or "" for immediate run + std::function<AppActionRunResult(const std::string& param)> runner; +}; + +namespace NativeCommands { +// The full built-in command catalog, built once (init()). UI thread only. +const std::vector<NativeCommand>& catalog(); + +// Dispatches `key` to its runner (unknown keys return a quiet Info). UI thread only. +AppActionRunResult run(const std::string& key, const std::string& param = {}); +} // namespace NativeCommands + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index c525896d0b..a6478097f7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7138,6 +7138,7 @@ struct Plater::priv void on_action_publish(wxCommandEvent &evt); void on_action_print_plate(SimpleEvent&); void open_machine_select_dialog(int plate_idx, PrintFromType print_type = PrintFromType::FROM_NORMAL); + void connect_to_printer(); void on_action_print_all(SimpleEvent&); void on_action_export_gcode(SimpleEvent&); void on_action_send_gcode(SimpleEvent&); @@ -12728,6 +12729,22 @@ void Plater::priv::open_machine_select_dialog(int plate_idx, PrintFromType print m_select_machine_dlg->ShowModal(); } +void Plater::priv::connect_to_printer() +{ + // BBL network (vendor BBL, not print-host) and printer-agents mode surface the real + // machine picker (lists discovered printers, connects on selection). Everything else + // is a print-host printer: the Connection button's dialog (host/API-key config). + PresetBundle* pb = wxGetApp().preset_bundle; + const bool bbl_or_agent = (pb && pb->use_bbl_network()) || + wxGetApp().app_config->get_bool("use_printer_agents"); + if (bbl_or_agent) + open_machine_select_dialog(q->get_partplate_list().get_curr_plate_index()); + else { + PhysicalPrinterDialog dlg(q->GetParent()); + dlg.ShowModal(); + } +} + void Plater::priv::on_action_send_to_multi_machine(SimpleEvent&) { if (!m_send_multi_dlg) @@ -17511,6 +17528,8 @@ Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_ void Plater::reset_window_layout() { p->reset_window_layout(); } +void Plater::connect_to_printer() { p->connect_to_printer(); } + //BBS void Plater::select_curr_plate_all() { p->select_curr_plate_all(); } void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index fd190d3832..e8264cf417 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -354,6 +354,9 @@ public: void add_file(); // Returns false when no object was added (e.g. the user cancelled the load dialog). bool add_model(bool imperial_units = false, std::string fname = ""); + // Opens the connection/management dialog appropriate for the current printer's + // technology: the machine picker for BBL/printer-agents, the print-host dialog otherwise. + void connect_to_printer(); void import_zip_archive(); void import_sl1_archive(); void extract_config_from_project(); diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index e5255f61fe..0536925865 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -61,3 +61,13 @@ TEST_CASE("Go-to-plate actions are keyed by index, not title", "[speeddial][acti CHECK(AppAction::compose_id("orca_plate_goto", "0", "orca") == "orca_plate_goto:0:orca"); CHECK(AppAction::compose_id("orca_plate_goto", "2", "orca") == "orca_plate_goto:2:orca"); } + +// A dynamic "Open recent project" action is keyed by file path (not the display name), so renaming +// a project or reordering the recents list never re-keys it - the same contract as a setting action. +TEST_CASE("Recent-project actions are keyed by path, not title", "[speeddial][actions]") +{ + CHECK(AppAction::compose_id("orca_recent_project", "/a/b/project.3mf", "orca") == + "orca_recent_project:/a/b/project.3mf:orca"); + CHECK(AppAction::compose_id("orca_recent_project", "C:/Data/cube.3mf", "orca") == + "orca_recent_project:C:/Data/cube.3mf:orca"); +} From 0709c9931e3fb1bcf02567687278b02638425e34 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Wed, 9 Sep 2026 17:36:21 +0800 Subject: [PATCH 07/29] Slight refactor for opening Plugins in speed dial --- src/slic3r/GUI/GUI_App.cpp | 44 ++++ src/slic3r/GUI/GUI_App.hpp | 3 + src/slic3r/GUI/NativeCommands.cpp | 19 ++ src/slic3r/GUI/PluginsDialog.cpp | 328 +++++++++++++++++++----------- src/slic3r/GUI/PluginsDialog.hpp | 18 ++ 5 files changed, 290 insertions(+), 122 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 3500493329..c5d59d9185 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -8532,6 +8532,50 @@ void GUI_App::open_plugins_dialog(size_t open_on_tab, const std::string& highlig } } +void GUI_App::refresh_plugins() +{ + // The metadata refresh blocks on disc discovery and a cloud round-trip, so run it on a worker + // and report completion through the notification manager -- the speed dial needs no dialog. + std::thread([]() { + refresh_plugin_metadata_blocking(/*fetch_cloud=*/true); + wxTheApp->CallAfter([]() { + if (wxGetApp().is_closing()) + return; + Plater* plater = wxGetApp().plater(); + if (plater == nullptr) + return; + plater->get_notification_manager()->push_notification( + NotificationType::CustomNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, + into_u8(_L("Plugins refreshed."))); + }); + }).detach(); +} + +void GUI_App::install_local_plugin() +{ + if (mainframe == nullptr) + return; + + wxFileDialog dialog(mainframe, _L("Select plugin package"), wxEmptyString, wxEmptyString, _L("Plugin files (*.py;*.whl)|*.py;*.whl"), + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return; + + wxString message; + const bool ok = install_local_plugin_package(boost::filesystem::path(dialog.GetPath().ToUTF8().data()), mainframe, message); + if (message.IsEmpty()) + return; // user cancelled the overwrite prompt + + Plater* plater = this->plater(); + if (plater == nullptr) + return; + plater->get_notification_manager()->push_notification( + NotificationType::CustomNotification, + ok ? NotificationManager::NotificationLevel::RegularNotificationLevel : NotificationManager::NotificationLevel::ErrorNotificationLevel, + into_u8(message)); +} + void GUI_App::open_terminal_dialog() { // Reached from the plugins dialog's webview ("open_terminal" command), i.e. from diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 8bf32df64c..b1ee96d468 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -633,6 +633,9 @@ public: void open_preferences(size_t open_on_tab = 0, const std::string& highlight_option = std::string()); void open_presetbundledialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string()); void open_plugins_dialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string()); + // Dialog-free plugin actions used by the speed dial: they never require the Plugins dialog to be open. + void refresh_plugins(); + void install_local_plugin(); void open_terminal_dialog(); void open_speed_dial(); ActionRegistry& action_registry() { return m_action_registry; } diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp index e4c4449cf6..94edd4898e 100644 --- a/src/slic3r/GUI/NativeCommands.cpp +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -10,6 +10,7 @@ #include "IMSlider.hpp" #include "MainFrame.hpp" #include "Plater.hpp" +#include "PluginsDialog.hpp" #include "PlateSettingsDialog.hpp" #include "DeviceCore/DevManager.h" @@ -475,6 +476,24 @@ std::vector<NativeCommand> build_command_catalog() return AppActionRunResult{AppActionRunResult::Level::Success}; }); + // ---- Plugins ---- + add("open_plugins", _u8L("Open Plugins"), _u8L("Plugins"), [](const std::string&) { + wxGetApp().open_plugins_dialog(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("refresh_plugins", _u8L("Refresh Plugins"), _u8L("Plugins"), [](const std::string&) { + wxGetApp().refresh_plugins(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("install_plugin", _u8L("Install Plugin"), _u8L("Plugins"), [](const std::string&) { + open_plugin_hub(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("install_local_plugin", _u8L("Install Local Plugin"), _u8L("Plugins"), [](const std::string&) { + wxGetApp().install_local_plugin(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + return out; } diff --git a/src/slic3r/GUI/PluginsDialog.cpp b/src/slic3r/GUI/PluginsDialog.cpp index a39fcbc535..4f97a7d089 100644 --- a/src/slic3r/GUI/PluginsDialog.cpp +++ b/src/slic3r/GUI/PluginsDialog.cpp @@ -125,15 +125,6 @@ PluginCapabilityType primary_capability_type_of(PluginManager& manager, const st return capabilities.empty() ? PluginCapabilityType::Unknown : capabilities.front()->type(); } -std::vector<PluginDescriptor> current_cloud_metadata_snapshot() -{ - std::vector<PluginDescriptor> cloud_entries; - for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true)) - if (entry.is_cloud_plugin()) - cloud_entries.push_back(entry); - return cloud_entries; -} - PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor) { descriptor.plugin_root.clear(); @@ -149,41 +140,6 @@ PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor) return descriptor; } -void refresh_plugin_metadata_blocking(bool fetch_cloud) -{ - PluginManager& manager = PluginManager::instance(); - - std::vector<std::string> not_found, unauthorized; - const std::vector<PluginDescriptor> current_cloud_metadata = fetch_cloud ? std::vector<PluginDescriptor>{} : - current_cloud_metadata_snapshot(); - - manager.rescan_plugins(); - - if (!fetch_cloud) { - manager.update_cloud_metadata(current_cloud_metadata); - return; - } - - manager.fetch_plugins_from_cloud(¬_found, &unauthorized); - - wxGetApp().CallAfter([not_found = std::move(not_found), unauthorized = std::move(unauthorized)]() { - if (wxGetApp().is_closing()) - return; - Plater* plater = wxGetApp().plater(); - if (plater == nullptr) - return; - - for (const auto& uuid : not_found) - plater->get_notification_manager()->push_notification(NotificationType::CustomNotification, - NotificationManager::NotificationLevel::RegularNotificationLevel, - format(_L("Plugin %s is no longer available."), uuid)); - for (const auto& uuid : unauthorized) - plater->get_notification_manager()->push_notification(NotificationType::CustomNotification, - NotificationManager::NotificationLevel::RegularNotificationLevel, - format(_L("Plugin %s access is unauthorized."), uuid)); - }); -} - std::string to_string(PluginUpdateStatus status); nlohmann::json build_context_actions_payload(const PluginAvailableActions& available_actions); @@ -447,6 +403,200 @@ bool take_plugin_operation_result(const std::shared_ptr<PluginOperationState>& s } } // namespace +// ── Dialog-independent plugin actions (also used by the speed dial) ─────────────────────────── + +namespace { + +// Snapshot of the currently-known cloud plugin descriptors, used to refresh metadata without a +// network round-trip (kUseCurrentCloudMeta). +std::vector<PluginDescriptor> current_cloud_metadata_snapshot() +{ + std::vector<PluginDescriptor> cloud_entries; + for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true)) + if (entry.is_cloud_plugin()) + cloud_entries.push_back(entry); + return cloud_entries; +} + +} // namespace + +void refresh_plugin_metadata_blocking(bool fetch_cloud) +{ + PluginManager& manager = PluginManager::instance(); + + std::vector<std::string> not_found, unauthorized; + const std::vector<PluginDescriptor> current_cloud_metadata = fetch_cloud ? std::vector<PluginDescriptor>{} : + current_cloud_metadata_snapshot(); + + manager.rescan_plugins(); + + if (!fetch_cloud) { + manager.update_cloud_metadata(current_cloud_metadata); + return; + } + + manager.fetch_plugins_from_cloud(¬_found, &unauthorized); + + wxGetApp().CallAfter([not_found = std::move(not_found), unauthorized = std::move(unauthorized)]() { + if (wxGetApp().is_closing()) + return; + Plater* plater = wxGetApp().plater(); + if (plater == nullptr) + return; + + for (const auto& uuid : not_found) + plater->get_notification_manager()->push_notification(NotificationType::CustomNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, + format(_L("Plugin %s is no longer available."), uuid)); + for (const auto& uuid : unauthorized) + plater->get_notification_manager()->push_notification(NotificationType::CustomNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, + format(_L("Plugin %s access is unauthorized."), uuid)); + }); +} + +void open_plugin_hub() +{ + std::string cloud_base_url = "https://cloud.orcaslicer.com"; + + if (wxGetApp().getAgent()) { + auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(wxGetApp().getAgent()->get_cloud_agent()); + if (orca_agent && !orca_agent->get_cloud_base_url().empty()) + cloud_base_url = orca_agent->get_cloud_base_url(); + } + + while (!cloud_base_url.empty() && cloud_base_url.back() == '/') + cloud_base_url.pop_back(); + if (cloud_base_url.empty()) + cloud_base_url = "https://cloud.orcaslicer.com"; + + wxLaunchDefaultBrowser(wxString::FromUTF8(cloud_base_url + "/app/plugins/plugin-hub")); +} + +bool install_local_plugin_package(const boost::filesystem::path& package_file, wxWindow* parent, wxString& message) +{ + message.clear(); + if (package_file.empty()) + return false; + + // ---- pre-flight (main thread): validate + inspect + overwrite prompt ---- + const wxString package_name = from_u8(package_file.filename().string()); + + std::string extension = package_file.extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), + [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); }); + if (extension != ".py" && extension != ".whl") { + message = _L("Select a .py or .whl plugin package."); + return false; + } + + PluginDescriptor plugin_descriptor; + bool existing_installation = false; + std::string error; + try { + if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error)) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file << " error=" << error; + message = _L("Failed to install plugin package. See the log for details."); + return false; + } + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file << " error=" << ex.what(); + message = _L("Failed to install plugin package. See the log for details."); + return false; + } catch (...) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file; + message = _L("Failed to install plugin package. See the log for details."); + return false; + } + + if (existing_installation) { + const wxString plugin_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name); + wxMessageDialog dialog(parent, + wxString::Format(_L("Plugin \"%s\" is already installed.\n\nInstalling this package will overwrite the existing plugin."), + plugin_name), + kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING); + dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel")); + if (dialog.ShowModal() != wxID_OK) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin package installation cancelled before overwrite. package=" << package_file + << " plugin=" << plugin_descriptor.name; + return false; // cancelled: message stays empty so callers stay silent + } + } + + // ---- install + refresh on a worker behind a modal progress dialog (keeps the UI live) ---- + bool installed = false; + { + struct Result + { + std::mutex mutex; + bool ok = false; + std::string error; + }; + auto state = std::make_shared<Result>(); + + wxProgressDialog* progress = new wxProgressDialog(_L("Installing plugin"), _L("Installing plugin") + ": " + package_name, + 100, parent, wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME); + wxTimer* timer = new wxTimer(); + timer->Bind(wxEVT_TIMER, [progress](wxTimerEvent&) { + if (progress) + progress->Pulse(); + }); + timer->Start(100); + + bool finished = false; + wxEventLoop loop; + auto on_finish = [&finished, &loop]() { + finished = true; + if (loop.IsRunning()) + loop.Exit(); + }; + + std::thread([state, package_file, on_finish]() mutable { + std::string error; + bool ok = false; + try { + ok = PluginManager::instance().install_plugin(package_file, error); + } catch (const std::exception& ex) { + error = ex.what(); + } catch (...) { + error = "Unknown error"; + } + if (ok) { + // Reflect the new package in discovery/cloud metadata without blocking the caller. + try { refresh_plugin_metadata_blocking(kUseCurrentCloudMeta); } catch (...) {} + } + { + std::lock_guard<std::mutex> lock(state->mutex); + state->ok = ok; + state->error = std::move(error); + } + wxTheApp->CallAfter(on_finish); + }).detach(); + + if (!finished) + loop.Run(); + + timer->Stop(); + delete timer; + progress->Destroy(); + + std::lock_guard<std::mutex> lock(state->mutex); + installed = state->ok; + error = std::move(state->error); + } + + if (!installed) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package installation failed for " << package_file << " error=" << error; + message = _L("Failed to install plugin package. See the log for details."); + return false; + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin package installed successfully from " << package_file; + const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name); + message = wxString::Format(_L("Installed \"%s\"."), installed_name); + return true; +} + PluginsDialog::PluginsDialog(wxWindow* parent, wxWindowID id, const wxString&, const wxPoint& pos, const wxSize& size, long style) : WebViewHostDialog(parent, id, _L("Plugins"), pos, size, style) { create_webview("web/dialog/PluginsDialog/index.html", _L("Plugins"), wxSize(900, 820), wxSize(760, 715)); } @@ -818,78 +968,28 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path) { if (package_path.empty()) return false; - BOOST_LOG_TRIVIAL(info) << "Installing local plugin package from path: " << package_path; - std::string error; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Installing local plugin package from path: " << package_path; + const boost::filesystem::path package_file(package_path); - const wxString package_name = from_u8(package_file.filename().string()); + wxString message; + const bool installed = install_local_plugin_package(package_file, this, message); - std::string extension = package_file.extension().string(); - std::transform(extension.begin(), extension.end(), extension.begin(), - [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); }); - if (extension != ".py" && extension != ".whl") { - show_status(_L("Select a .py or .whl plugin package."), "info"); - return false; - } - - PluginDescriptor plugin_descriptor; - bool existing_installation = false; - auto report_inspection_failure = [&]() { - BOOST_LOG_TRIVIAL(error) << "Plugin package inspection failed for " << package_path << " error=" << error; - show_status(_L("Failed to install plugin package. See the log for details."), "warn"); + // The shared helper reports a user-cancelled overwrite with an empty message: stay silent. + if (message.IsEmpty()) { send_plugins(); return false; - }; - - try { - if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error)) - return report_inspection_failure(); - } catch (const std::exception& ex) { - error = ex.what(); - return report_inspection_failure(); - } catch (...) { - error = "Unknown error"; - return report_inspection_failure(); - } - - if (existing_installation) { - const wxString plugin_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name); - wxMessageDialog dialog( - this, - wxString::Format(_L("Plugin \"%s\" is already installed.\n\nInstalling this package will overwrite the existing plugin."), - plugin_name), - kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING); - dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel")); - const int overwrite_rc = dialog.ShowModal(); - restore_z_order(); - if (overwrite_rc != wxID_OK) { - BOOST_LOG_TRIVIAL(info) << "Plugin package installation cancelled before overwrite. package=" << package_path - << " plugin=" << plugin_descriptor.name; - return false; - } - } - - bool installed = false; - try { - installed = run_with_dialog_wait([package_file, &error]() { return PluginManager::instance().install_plugin(package_file, error); }, - _L("Installing plugin"), _L("Installing plugin") + ": " + package_name, 100, - wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME); - } catch (const std::exception& ex) { - error = ex.what(); - } catch (...) { - error = "Unknown error"; } if (!installed) { - BOOST_LOG_TRIVIAL(error) << "Plugin package installation failed for " << package_path << " error=" << error; - show_status(_L("Failed to install plugin package. See the log for details."), "warn"); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to install plugin package."; + show_status(message, "warn"); send_plugins(); return false; } - BOOST_LOG_TRIVIAL(info) << "Plugin package installed successfully from " << package_path; - const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name); - show_status(wxString::Format(_L("Installed \"%s\"."), installed_name), "success"); - refresh_plugin_metadata_async(_L("Refreshing"), _L("Refreshing plugins data"), kUseCurrentCloudMeta); + show_status(message, "success"); + prompt_for_missing_plugins(); + send_plugins(); return true; } @@ -1085,23 +1185,7 @@ void PluginsDialog::open_plugin_on_cloud(const std::string& sharing_token) wxLaunchDefaultBrowser(wxString::FromUTF8(orca_agent->get_cloud_base_url() + "/p/" + sharing_token)); } -void PluginsDialog::open_plugin_hub() -{ - std::string cloud_base_url = "https://cloud.orcaslicer.com"; - - if (wxGetApp().getAgent()) { - auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(wxGetApp().getAgent()->get_cloud_agent()); - if (orca_agent && !orca_agent->get_cloud_base_url().empty()) - cloud_base_url = orca_agent->get_cloud_base_url(); - } - - while (!cloud_base_url.empty() && cloud_base_url.back() == '/') - cloud_base_url.pop_back(); - if (cloud_base_url.empty()) - cloud_base_url = "https://cloud.orcaslicer.com"; - - wxLaunchDefaultBrowser(wxString::FromUTF8(cloud_base_url + "/app/plugins/plugin-hub")); -} +void PluginsDialog::open_plugin_hub() { Slic3r::GUI::open_plugin_hub(); } void PluginsDialog::delete_local_plugin(const PluginDescriptor& plugin) { diff --git a/src/slic3r/GUI/PluginsDialog.hpp b/src/slic3r/GUI/PluginsDialog.hpp index e663de79e4..2c4a5925d9 100644 --- a/src/slic3r/GUI/PluginsDialog.hpp +++ b/src/slic3r/GUI/PluginsDialog.hpp @@ -24,6 +24,8 @@ #include <wx/string.h> #include <wx/timer.h> +#include <boost/filesystem.hpp> + class wxTimer; namespace Slic3r { @@ -34,6 +36,22 @@ enum class PluginCapabilityType; namespace GUI { +// Dialog-independent plugin-management actions, shared by the Plugins dialog and the speed dial: +// they never require the webview dialog to be open. + +// Rescans local plugins and (optionally) re-fetches cloud metadata. Blocking: run off the UI +// thread. Used by PluginsDialog (behind its progress dialog) and GUI_App::refresh_plugins(). +void refresh_plugin_metadata_blocking(bool fetch_cloud); + +// Opens the Cloud plugin hub in the default browser. No dialog needed. +void open_plugin_hub(); + +// Synchronously installs a local plugin package (.py/.whl). Runs on the UI thread but keeps it +// responsive by performing the install on a worker behind a modal progress dialog. `parent` owns +// the overwrite prompt and the progress dialog. On success `message` carries the localized +// confirmation; on a user-cancelled overwrite it is empty; on failure it carries the reason. +bool install_local_plugin_package(const boost::filesystem::path& package_file, wxWindow* parent, wxString& message); + class PluginsDialog : public Slic3r::GUI::WebViewHostDialog { public: From 0fb87f05b146aea4124c4b4941fc9960472625f7 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Wed, 9 Sep 2026 18:05:49 +0800 Subject: [PATCH 08/29] Remove printer connect/disconnect for now --- src/slic3r/GUI/NativeCommands.cpp | 16 ---------------- src/slic3r/GUI/Plater.cpp | 19 ------------------- src/slic3r/GUI/Plater.hpp | 3 --- 3 files changed, 38 deletions(-) diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp index 94edd4898e..d0dfc78086 100644 --- a/src/slic3r/GUI/NativeCommands.cpp +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -389,22 +389,6 @@ std::vector<NativeCommand> build_command_catalog() }); // ---- Printer / device connection ---- - add("connect_printer", _u8L("Connect Printer"), _u8L("Printer"), [](const std::string&) { - if (Plater* plater = wxGetApp().plater()) - plater->connect_to_printer(); - return AppActionRunResult{AppActionRunResult::Level::Success}; - }); - add("disconnect_printer", _u8L("Disconnect Printer"), _u8L("Printer"), [](const std::string&) { - DeviceManager* dev = wxGetApp().getDeviceManager(); - if (!dev) - return AppActionRunResult{AppActionRunResult::Level::Info, _L("Printer connection is unavailable.")}; - if (MachineObject* machine = dev->get_selected_machine()) { - machine->disconnect(); - dev->set_selected_machine(""); - return AppActionRunResult{AppActionRunResult::Level::Success}; - } - return AppActionRunResult{AppActionRunResult::Level::Info, _L("Printer is not connected.")}; - }); add("sync_ams", _u8L("Synchronize Filament List from AMS"), _u8L("Printer"), [](const std::string&) { Plater* plater = wxGetApp().plater(); DeviceManager* dev = wxGetApp().getDeviceManager(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index a6478097f7..c525896d0b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7138,7 +7138,6 @@ struct Plater::priv void on_action_publish(wxCommandEvent &evt); void on_action_print_plate(SimpleEvent&); void open_machine_select_dialog(int plate_idx, PrintFromType print_type = PrintFromType::FROM_NORMAL); - void connect_to_printer(); void on_action_print_all(SimpleEvent&); void on_action_export_gcode(SimpleEvent&); void on_action_send_gcode(SimpleEvent&); @@ -12729,22 +12728,6 @@ void Plater::priv::open_machine_select_dialog(int plate_idx, PrintFromType print m_select_machine_dlg->ShowModal(); } -void Plater::priv::connect_to_printer() -{ - // BBL network (vendor BBL, not print-host) and printer-agents mode surface the real - // machine picker (lists discovered printers, connects on selection). Everything else - // is a print-host printer: the Connection button's dialog (host/API-key config). - PresetBundle* pb = wxGetApp().preset_bundle; - const bool bbl_or_agent = (pb && pb->use_bbl_network()) || - wxGetApp().app_config->get_bool("use_printer_agents"); - if (bbl_or_agent) - open_machine_select_dialog(q->get_partplate_list().get_curr_plate_index()); - else { - PhysicalPrinterDialog dlg(q->GetParent()); - dlg.ShowModal(); - } -} - void Plater::priv::on_action_send_to_multi_machine(SimpleEvent&) { if (!m_send_multi_dlg) @@ -17528,8 +17511,6 @@ Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_ void Plater::reset_window_layout() { p->reset_window_layout(); } -void Plater::connect_to_printer() { p->connect_to_printer(); } - //BBS void Plater::select_curr_plate_all() { p->select_curr_plate_all(); } void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index e8264cf417..fd190d3832 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -354,9 +354,6 @@ public: void add_file(); // Returns false when no object was added (e.g. the user cancelled the load dialog). bool add_model(bool imperial_units = false, std::string fname = ""); - // Opens the connection/management dialog appropriate for the current printer's - // technology: the machine picker for BBL/printer-agents, the print-host dialog otherwise. - void connect_to_printer(); void import_zip_archive(); void import_sl1_archive(); void extract_config_from_project(); From a0ba8e12e5a31700f5fc9553db2d95bd35cbc15b Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Thu, 10 Sep 2026 12:07:36 +0800 Subject: [PATCH 09/29] Change to a pin/bookmark icon. Added shortcut (Ctrl/Cmd+B) to pin/unpin actions. Clear unresolved pinned actions when switching between modes. Fixed scroll-back issue when scrolling to pin action --- resources/web/dialog/SpeedDial/speeddial.js | 63 ++++++++++++++++----- resources/web/dialog/SpeedDial/style.css | 12 ++-- src/slic3r/GUI/ActionRegistry.cpp | 15 ++++- src/slic3r/GUI/SpeedDialDialog.cpp | 2 +- 4 files changed, 68 insertions(+), 24 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 04844aae43..b92261b351 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -375,7 +375,7 @@ window.HandleStudio = function (payload) { // Favourites are at the quick-launch cap - undo the optimistic pin and flash a hint. var fid = payload.id; if (fid && FAVS.indexOf(fid) !== -1) FAVS.splice(FAVS.indexOf(fid), 1); - render({ resize: true }); + render({ resize: true, keepScroll: true }); flashHint("Favourites are full (" + (payload.limit || K_FAV_LIMIT) + " max)"); } }; @@ -434,10 +434,19 @@ function markedText(className, text, match) { return node; } -function starSvg(on) { +// A bookmark glyph: outlined when unpinned, filled when saved to favourites. +function pinSvg(on) { return '<svg width="15" height="15" viewBox="0 0 24 24" fill="' + (on ? "currentColor" : "none") + '" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round">' + - '<path d="M12 3.5l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 17.9 6.8 20.6l1-5.8L3.5 9.7l5.9-.9z"/></svg>'; + '<path d="M6 3.5A1.5 1.5 0 0 1 7.5 2h9A1.5 1.5 0 0 1 18 3.5V21l-6-4.2L6 21z"/></svg>'; +} + +// Sync one pin button to its favourite state. Shared by row construction and the in-place +// updatePins pass so the two can't drift. +function setPinState(pin, on) { + pin.classList.toggle("on", on); + pin.innerHTML = pinSvg(on); + pin.title = on ? "Unpin from favourites (Ctrl+B)" : "Pin to favourites (Ctrl+B)"; } // ---- render ------------------------------------------------------------------ @@ -551,7 +560,7 @@ function updateFavEyebrow(favs) { } // A command/action row - used for search results, recents, and (because settings are actions now) -// the setting options too. All rows are pinnable, so every row carries a star. +// the setting options too. All rows are pinnable, so every row carries a bookmark. function renderActionRow(a, i) { var on = FAVS.indexOf(a.id) !== -1; var row = document.createElement("div"); @@ -591,14 +600,13 @@ function renderActionRow(a, i) { row.appendChild(tile); row.appendChild(left); - var star = document.createElement("button"); - star.className = "star" + (on ? " on" : ""); - star.innerHTML = starSvg(on); - star.title = on ? "Unpin from favourites" : "Pin to favourites"; - star.onclick = function (ev) { ev.stopPropagation(); toggleFav(a.id); }; + var pin = document.createElement("button"); + pin.className = "pin"; + setPinState(pin, on); + pin.onclick = function (ev) { ev.stopPropagation(); toggleFav(a.id); }; // why: two quick fav/unfav clicks must not dblclick-run the row - star.ondblclick = function (ev) { ev.stopPropagation(); }; - row.appendChild(star); + pin.ondblclick = function (ev) { ev.stopPropagation(); }; + row.appendChild(pin); row.onclick = function () { sel = { zone: "list", i: i }; render({ resize: true }); }; row.ondblclick = function () { sel = { zone: "list", i: i }; activateEntry(a); }; @@ -663,6 +671,21 @@ function updateSelection() { } } +// Sync the pin buttons in place when FAVS changes but the row set doesn't (fav toggle), so a +// bookmark fills/empties without a rebuild that would reset scroll. Rows carry data-idx into the +// active list. +function updatePins(list) { + var rows = listEl ? listEl.querySelectorAll(".row") : []; + for (var i = 0; i < rows.length; i++) { + var a = list[parseInt(rows[i].getAttribute("data-idx"), 10)]; + var pin = rows[i].querySelector(".pin"); + if (!a || !pin) continue; + var on = FAVS.indexOf(a.id) !== -1; + if (pin.classList.contains("on") !== on) + setPinState(pin, on); + } +} + function renderCommandsList() { var list = currentList(); var total = list.length; @@ -702,9 +725,10 @@ function renderCommandsList() { countEl.textContent = showList ? resultCountText(ACTIONS.length, total, query) : total + " recent"; } updateSelection(); + updatePins(list); } -// A tab row: no star/unpin (tabs aren't pinnable), placeholder tile (tabs have no pictogram). Uses +// A tab row: no pin/unpin (tabs aren't pinnable), placeholder tile (tabs have no pictogram). Uses // tabTitle so pages added with an empty text (e.g. Home) still show a label. function renderTabRow(t, i) { var label = tabTitle(t); @@ -781,7 +805,10 @@ function renderList() { function render(opts) { renderFav(); renderList(); - scrollSelectedIntoView(); + // Pin toggles don't move the selection, so they pass keepScroll to avoid snapping the list + // back to a row that is currently off-screen. + if (!(opts && opts.keepScroll)) + scrollSelectedIntoView(); if (opts && opts.resetScroll) resetScrollPositions(listEl, document); if (opts && opts.resize) @@ -835,7 +862,7 @@ function toggleFav(id) { var newState = k === -1; if (newState) FAVS.push(id); else FAVS.splice(k, 1); SendMessage({ command: "toggle_favourite", id: id, fav: newState }); - render({ resize: true }); + render({ resize: true, keepScroll: true }); } // Fire a command/plugin action; C++ owns the run-confirm (native dialog) + suppression, then @@ -951,6 +978,14 @@ function OnInit() { document.addEventListener("keydown", function (e) { if (favMenuEl && !favMenuEl.hidden && e.key === "Escape") { e.preventDefault(); hideFavMenu(); return; } + // Pin/unpin the highlighted action: Ctrl/Cmd+B. Commands phase only (tabs/percent aren't pinnable). + if (phase === "commands" && (e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && + e.key.toLowerCase() === "b") { + e.preventDefault(); + var id = selectedActionId(sel, currentList(), currentVisibleFavs(), query); + if (id) toggleFav(id); + return; + } // Quick-launch a numbered favourite: Alt/Option + digit (0 = the 10th). Only in the // commands phase, where the pinned bar is shown. if (phase === "commands" && e.altKey && !e.ctrlKey && !e.metaKey) { diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index aa8bdc7c4b..f7d045bc9a 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -255,7 +255,7 @@ kbd { border: 1px solid var(--border, var(--orca-border, #ddd)); border-radius: 4px; } -.star { +.pin { flex: 0 0 auto; width: 24px; height: 24px; @@ -269,11 +269,11 @@ kbd { justify-content: center; opacity: 0; } -.star svg { display: block; } -.row:hover .star:not(.on), -.row.sel .star:not(.on) { opacity: .5; } -.star.on { opacity: 1; color: var(--main-color, var(--orca-accent, #009688)); } -.star:hover { background: rgba(127,127,127,.18); } +.pin svg { display: block; } +.row:hover .pin:not(.on), +.row.sel .pin:not(.on) { opacity: .5; } +.pin.on { opacity: 1; color: var(--main-color, var(--orca-accent, #009688)); } +.pin:hover { background: rgba(127,127,127,.18); } .dial-empty { min-height: 64px; padding: 18px 10px; diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index 92916eb538..f6e124c118 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -728,9 +728,18 @@ nlohmann::json ActionRegistry::snapshot() // 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 - // reorder the favourites bar). The page (js) filters out ids with no live action itself. - // Cap on read so the bar cannot exceed the quick-launch slots (kFavLimit). - nlohmann::json favourites(favourite_ids()); + // reorder the favourites bar). Drop pins with no live action (an option hidden by the + // current mode, an unloaded plugin, a gone plate/project) and persist the pruned list, so + // invisible pins can't silently fill the quick-launch cap. Order is preserved. + std::vector<std::string> favs = favourite_ids(); + std::vector<std::string> live_favs; + live_favs.reserve(favs.size()); + for (const auto& id : favs) + if (m_actions.count(id)) + live_favs.push_back(id); + if (live_favs.size() != favs.size()) + write_section("favourite_actions", nlohmann::json(live_favs)); + nlohmann::json favourites(live_favs); // Recent = the last-N launched actions by recency (only actions with a run history). constexpr size_t kRecentLimit = 5; diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index b199d0b5ae..fbc446c5a0 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -127,7 +127,7 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload) send_actions(); } else if (command == "toggle_favourite") { // set_favourite() refuses once the bar hits kFavLimit; tell the page so it can undo the - // star and show a "favourites are full" hint instead of silently losing the pin. + // pin and show a "favourites are full" hint instead of silently losing the favourite. const std::string fav_id = payload.value("id", ""); const bool ok = wxGetApp().action_registry().set_favourite(fav_id, payload.value("fav", false)); if (!ok) From b4beae4be35e83314d01b8bee417d25b55b8615a Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Thu, 10 Sep 2026 12:47:43 +0800 Subject: [PATCH 10/29] Fixed potential UB --- src/slic3r/GUI/ActionRegistry.cpp | 73 +++++++++++++----------- tests/slic3rutils/test_action_source.cpp | 10 ++++ 2 files changed, 49 insertions(+), 34 deletions(-) diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index f6e124c118..370d808142 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -128,11 +128,11 @@ std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std: // ---- 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"; -constexpr const char* kSettingPrefix = "orca_setting"; -constexpr const char* kPlateGotoPrefix = "orca_plate_goto"; +constexpr const char* kCommandPrefix = "orca_command"; +constexpr const char* kOrcaSourceKey = "orca"; +constexpr const char* kOrcaSourceName = "OrcaSlicer"; +constexpr const char* kSettingPrefix = "orca_setting"; +constexpr const char* kPlateGotoPrefix = "orca_plate_goto"; constexpr const char* kRecentProjectPrefix = "orca_recent_project"; // Display context for a setting action's eyebrow, e.g. the "Process" in "Process : Quality : Layers". @@ -142,10 +142,10 @@ std::string setting_type_context(Preset::Type type) switch (type) { case Preset::TYPE_FILAMENT: case Preset::TYPE_SLA_MATERIAL: return _u8L("Filament"); - case Preset::TYPE_PRINTER: return _u8L("Printer"); + case Preset::TYPE_PRINTER: return _u8L("Printer"); case Preset::TYPE_PRINT: case Preset::TYPE_SLA_PRINT: - default: return _u8L("Process"); + default: return _u8L("Process"); } } @@ -155,15 +155,19 @@ std::string setting_type_context(Preset::Type type) // the generic registry run() bumps stats so a jump shows up in "recents" like any other action. struct SettingAction : AppAction { - std::string opt_key; + std::string opt_key; Preset::Type type; - std::wstring category; // localized category, forwarded to jump_to_option + std::wstring category; // localized category, forwarded to jump_to_option static std::string id_for(const std::string& opt_key, Preset::Type type) { return std::string(kSettingPrefix) + ":" + opt_key + ":" + std::to_string(int(type)); } - SettingAction(std::string opt_key_in, Preset::Type type_in, std::string title, std::string group, - std::wstring category_in, std::string source_name) + SettingAction(std::string opt_key_in, + Preset::Type type_in, + std::string title, + std::string group, + std::wstring category_in, + std::string source_name) : AppAction(AppActionId{id_for(opt_key_in, type_in)}, std::move(title), kOrcaSourceKey, std::move(source_name)) , opt_key(std::move(opt_key_in)) , type(type_in) @@ -182,15 +186,13 @@ struct SettingAction : AppAction } }; - // A built-in command action. Thin value: identity + presentation come from the NativeCommands // catalog, and run() routes back to it - the catalog is the single source of truth for its -// behaviour. source_key is the constant "orca" so a renamed title never re-keys the action -// (matches the plugin source-key contract). +// behaviour. The id is keyed by the stable catalog key (NOT the display title), so a rename or a +// UI-language switch never re-keys the action; the title is display-only. struct CommandAction : AppAction { - static std::unique_ptr<CommandAction> make(const NativeCommand& c) - { return std::unique_ptr<CommandAction>(new CommandAction(c)); } + static std::unique_ptr<CommandAction> make(const NativeCommand& c) { return std::unique_ptr<CommandAction>(new CommandAction(c)); } std::string command_key; @@ -198,7 +200,8 @@ struct CommandAction : AppAction private: explicit CommandAction(const NativeCommand& c) - : AppAction(kCommandPrefix, c.title, kOrcaSourceKey, kOrcaSourceName), command_key(c.key) + : AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, c.title, kOrcaSourceKey, kOrcaSourceName), + command_key(c.key) { this->kind = AppActionKind::Command; this->group = c.group; @@ -214,12 +217,10 @@ struct PlateAction : AppAction { int plate_index; - static std::string id_for(int index) - { return AppAction::compose_id(kPlateGotoPrefix, std::to_string(index), kOrcaSourceKey); } + static std::string id_for(int index) { return AppAction::compose_id(kPlateGotoPrefix, std::to_string(index), kOrcaSourceKey); } PlateAction(int index, std::string title, std::string source_name) - : AppAction(AppActionId{id_for(index)}, std::move(title), kOrcaSourceKey, std::move(source_name)) - , plate_index(index) + : AppAction(AppActionId{id_for(index)}, std::move(title), kOrcaSourceKey, std::move(source_name)), plate_index(index) { this->kind = AppActionKind::Command; this->group = _u8L("Plate"); @@ -239,12 +240,10 @@ struct RecentProjectAction : AppAction { std::string file_path; - static std::string id_for(const std::string& path) - { return AppAction::compose_id(kRecentProjectPrefix, path, kOrcaSourceKey); } + static std::string id_for(const std::string& path) { return AppAction::compose_id(kRecentProjectPrefix, path, kOrcaSourceKey); } RecentProjectAction(std::string path, std::string title, std::string source) - : AppAction(AppActionId{id_for(path)}, std::move(title), kOrcaSourceKey, std::move(source)) - , file_path(std::move(path)) + : AppAction(AppActionId{id_for(path)}, std::move(title), kOrcaSourceKey, std::move(source)), file_path(std::move(path)) { this->kind = AppActionKind::Command; this->group = _u8L("Recent Projects"); @@ -538,15 +537,17 @@ void ActionRegistry::materialize_setting_actions() // title = the option leaf name (last label segment); group stays empty so the source path // (above) is the single display/search breadcrumb rather than being duplicated. - auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, boost::nowide::narrow(label_w), - std::string(), opt.category_local, boost::nowide::narrow(path)); + auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, boost::nowide::narrow(label_w), std::string(), + opt.category_local, boost::nowide::narrow(path)); - action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); + action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); if (auto it = stats.find(id); it != stats.end() && it->is_object()) { action->count = it->value("count", 0); action->last = it->value("last", 0LL); } - m_actions.insert_or_assign(action->id(), std::shared_ptr<AppAction>(std::move(action))); + auto const action_id = action->id(); + auto const app_action = std::shared_ptr<AppAction>(std::move(action)); + m_actions.insert_or_assign(action_id, app_action); } // Drop SettingActions whose option no longer exists in the current configs (e.g. the printer @@ -600,13 +601,15 @@ void ActionRegistry::materialize_plate_actions() if (!name.empty()) title += " (" + name + ")"; - auto action = std::make_unique<PlateAction>(int(i), title, kOrcaSourceName); + auto action = std::make_unique<PlateAction>(int(i), title, kOrcaSourceName); action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); if (auto it = stats.find(id); it != stats.end() && it->is_object()) { action->count = it->value("count", 0); action->last = it->value("last", 0LL); } - m_actions.insert_or_assign(action->id(), std::shared_ptr<AppAction>(std::move(action))); + auto const action_id = action->id(); + auto const app_action = std::shared_ptr<AppAction>(std::move(action)); + m_actions.insert_or_assign(action_id, app_action); } // Drop plate actions whose index no longer exists (a plate was deleted / moved to the front). @@ -649,13 +652,15 @@ void ActionRegistry::materialize_recent_project_actions() if (title.empty()) title = path; - auto action = std::make_unique<RecentProjectAction>(path, std::move(title), path); - action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); + auto action = std::make_unique<RecentProjectAction>(path, std::move(title), path); + action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); if (auto it = stats.find(id); it != stats.end() && it->is_object()) { action->count = it->value("count", 0); action->last = it->value("last", 0LL); } - m_actions.insert_or_assign(action->id(), std::shared_ptr<AppAction>(std::move(action))); + auto const action_id = action->id(); + auto const app_action = std::shared_ptr<AppAction>(std::move(action)); + m_actions.insert_or_assign(action_id, app_action); } // Drop recent-project actions whose file no longer exists / was removed from the recents list. diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index 0536925865..9b6564ea98 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -71,3 +71,13 @@ TEST_CASE("Recent-project actions are keyed by path, not title", "[speeddial][ac CHECK(AppAction::compose_id("orca_recent_project", "C:/Data/cube.3mf", "orca") == "orca_recent_project:C:/Data/cube.3mf:orca"); } + +// A built-in command is keyed by its stable catalog key (not the localized display title), so a +// rename or a UI-language switch never re-keys the action and its persisted favourite/stats survive. +TEST_CASE("Command actions are keyed by catalog key, not display title", "[speeddial][actions]") +{ + CHECK(AppAction::compose_id("orca_command", "save_project", "orca") == "orca_command:save_project:orca"); + // The second field is the stable key, so distinct commands never collide. + CHECK(AppAction::compose_id("orca_command", "save_project", "orca") != + AppAction::compose_id("orca_command", "load_project", "orca")); +} From 9fc6c45770b0885219cefdf3bc69f7738ab33150 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Thu, 10 Sep 2026 12:52:10 +0800 Subject: [PATCH 11/29] Fixes potential focus bug --- src/slic3r/GUI/MainFrame.cpp | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 4ba82fd402..db66afbd7b 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -44,6 +44,7 @@ // BBS #include "PartPlate.hpp" #include "Preferences.hpp" +#include "Widgets/Button.hpp" #include "Widgets/ProgressDialog.hpp" #include "BindDialog.hpp" #include "../Utils/MacDarkMode.hpp" @@ -107,6 +108,27 @@ enum class ERescaleTarget SettingsDialog }; +namespace { + +// Space opens the speed dial, but it is the activation key for buttons, checkboxes and other +// controls. CHAR_HOOK runs before the focused child, so only take Space when the focused window has +// no keyboard-activation meaning of its own. Canvases (GLCanvas3D) and panels are not wxControls and +// fall through to "open"; Notebook and wxWebView are wxControls that don't use Space, so allow them. +bool focus_keeps_space(wxWindow* focus) +{ + if (!focus) + return false; + if (dynamic_cast<wxTextEntryBase*>(focus)) + return true; // typing a space into a text field + if (dynamic_cast<::Button*>(focus)) + return true; // custom button: Space clicks it (it is a wxWindow, not a wxControl) + if (dynamic_cast<wxControl*>(focus) && !dynamic_cast<Notebook*>(focus) && !dynamic_cast<wxWebView*>(focus)) + return true; // stock button/checkbox/choice/list/etc. keep Space + return false; +} + +} // namespace + #ifdef __WXGTK__ // A thin transparent panel placed at a window edge to handle resize. // Works regardless of underlying content (GLCanvas3D, wxWebView, etc.) @@ -703,12 +725,12 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ return;} #endif // Orca: open the speed dial from any page with a bare Space. Only when no modifier is held (so - // editing shortcuts like Ctrl+Shift+Space in the canvas still reach it) and while no text field - // is focused, so typing a space into the search box or a parameter value isn't hijacked. + // editing shortcuts like Ctrl+Shift+Space in the canvas still reach it) and the focused window + // doesn't use Space to activate itself (buttons, checkboxes, list/choice controls, text fields), + // so a bare Space there still clicks/toggles instead of being hijacked. if (!evt.CmdDown() && !evt.ShiftDown() && !evt.AltDown() && evt.GetKeyCode() == WXK_SPACE) { - wxWindow* focus = wxWindow::FindFocus(); - if (focus && dynamic_cast<wxTextEntryBase*>(focus)) { - evt.Skip(); // typing in a text field - let the space reach it + if (focus_keeps_space(wxWindow::FindFocus())) { + evt.Skip(); // let the focused control keep Space return; } wxGetApp().open_speed_dial(); From 16d285fea6fae0db16e9d0c7f9e12cd884694a13 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Thu, 10 Sep 2026 13:00:23 +0800 Subject: [PATCH 12/29] Cleanup comments and formatting --- resources/web/dialog/SpeedDial/speeddial.js | 25 +- .../web/dialog/SpeedDial/speeddial.test.js | 122 ++-- resources/web/dialog/SpeedDial/style.css | 596 +++++++++++------- src/slic3r/GUI/ActionRegistry.cpp | 4 +- src/slic3r/GUI/ActionRegistry.hpp | 47 +- src/slic3r/GUI/MainFrame.cpp | 8 +- src/slic3r/GUI/MainFrame.hpp | 9 +- src/slic3r/GUI/NativeCommands.cpp | 28 +- src/slic3r/GUI/NativeCommands.hpp | 4 +- 9 files changed, 488 insertions(+), 355 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index b92261b351..efe99db1b8 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -1,5 +1,5 @@ // 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 / commandList). +// node vm can exercise the pure helpers (searchActions / filterTabs / actionLabel / nextSel / commandList). // ---- state (populated by the C++ bridge via window.HandleStudio) ---- var ACTIONS = []; // [{id,title,source,group,input,shortcut}], already frecency-sorted by C++ @@ -17,10 +17,10 @@ var matchIndex = {}; // a bottom spacer fills the rest of the list so the scrollbar reflects the full match count and // "scroll past the last rendered row" reveals the next window. var K_ROWS = 50; -var ROW_H = 44; +var ROW_H = 44; var renderEnd = 0; -var builtKey = ""; // phase|query|listLen - when it changes, rows are rebuilt from the first window -var spacerEl = null; // the trailing height spacer, always the last child of listEl +var builtKey = ""; // phase|query|listLen - when it changes, rows are rebuilt from the first window +var spacerEl = null; // the trailing height spacer, always the last child of listEl // search-cache: the normalized (folded+lowercased) needle for the current query pass. var searchNeedle = ""; @@ -31,8 +31,8 @@ var searchNeedle = ""; var phase = "commands"; 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 -// this script - it is shared with the Plugins dialog. Speed dial search is always case-insensitive. +// why: the fuzzy matcher (NormText/FuzzyRangesNorm/WholeWordRangesNorm) lives in shared +// ../../js/fuzzy-search.js, loaded before this script. Search is always case-insensitive. // element handles, assigned in OnInit (kept null so load-time touches no DOM) var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, countEl = null, headEl = null; @@ -71,7 +71,7 @@ function fieldMatchScore(norm, wwRe) { if (wwRe) { var m = wwRe.exec(norm || ""); if (m) - return {score: 1000 - m.index * 10, ranges: [[m.index, m.index + m[0].length]], contiguous: true}; + return { score: 1000 - m.index * 10, ranges: [[m.index, m.index + m[0].length]], contiguous: true }; } var r = FuzzyRangesNorm(norm || "", searchNeedle); if (!r) return null; @@ -81,7 +81,7 @@ function fieldMatchScore(norm, wwRe) { gaps += r[i][0] - r[i - 1][1]; len += r[i][1] - r[i][0]; } - return {score: 1000 - r[0][0] * 10 - gaps * 10, ranges: r, contiguous: r.length === 1 && len === searchNeedle.length}; + return { score: 1000 - r[0][0] * 10 - gaps * 10, ranges: r, contiguous: r.length === 1 && len === searchNeedle.length }; } // Combine the per-field match scores into one comparable value. Ranking tiers, strongest first: @@ -177,9 +177,8 @@ function resultCountText(total, shown, query) { 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"). +// Display label for a notebook tab. Trim any stray whitespace; 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) || ""); @@ -537,7 +536,7 @@ function showFavMenu(x, y, id) { } // Swap a favourite with its visible neighbour (dir -1/+1) and persist the new order. Swapping -// by id inside FAVS (not the visible slice) keeps any hidden pins (no live action) in place. +// by id inside FAVS (not the visible slice) keeps the persisted order stable. function moveFav(id, dir) { var favs = currentVisibleFavs(); var vi = favs.indexOf(id); @@ -990,7 +989,7 @@ function OnInit() { // commands phase, where the pinned bar is shown. if (phase === "commands" && e.altKey && !e.ctrlKey && !e.metaKey) { var slotIdx = favIndexForDigit(e.key); - var favIds = currentVisibleFavs(); + var favIds = currentVisibleFavs(); if (slotIdx >= 0 && slotIdx < favIds.length) { e.preventDefault(); var fav = byId(favIds[slotIdx]); diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 6a0a42bc2e..5b51daffbe 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -13,13 +13,13 @@ vm.runInContext(fs.readFileSync(__dirname + "/speeddial.js", "utf8"), ctx); assert.equal(typeof ctx.parseId, "undefined", "opaque action ids must never be parsed"); const duplicateActions = [ - { id: "0123456789abcdef", title: "Repair", source: "Mesh Tools" }, - { id: "fedcba9876543210", title: "Repair", source: "Mesh Tools" } + { id: "0123456789abcdef", title: "Repair", source: "Mesh Tools" }, + { id: "fedcba9876543210", title: "Repair", source: "Mesh Tools" } ]; assert.equal( - ctx.actionLabel(duplicateActions[0], duplicateActions), - "Repair from Mesh Tools (0123456789abcdef)", - "duplicate labels should use the opaque id without interpreting its contents" + ctx.actionLabel(duplicateActions[0], duplicateActions), + "Repair from Mesh Tools (0123456789abcdef)", + "duplicate labels should use the opaque id without interpreting its contents" ); assert.equal(ctx.shouldRenderActionList(""), false, "an empty search keeps recent/empty list"); @@ -28,118 +28,118 @@ assert.equal(ctx.shouldRenderActionList("r"), true, "typing starts rendering mat // commandList: an empty query shows recents; a typed query filters all actions. assert.deepEqual(ctx.commandList(duplicateActions, [], ""), [], - "empty query + no recents shows nothing"); + "empty query + no recents shows nothing"); assert.deepEqual(ctx.commandList(duplicateActions, [duplicateActions[0]], ""), - [duplicateActions[0]], - "empty query shows the recent list"); + [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"); + "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" } + { 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"); + "empty query keeps the whole tab list"); assert.equal(ctx.filterTabs(tabOptions, "prep").length, 1, - "a typed query filters tabs by title"); + "a typed query filters tabs by title"); assert.equal(ctx.filterTabs(tabOptions, "Device").length, 1, - "a typed query matches a tab title"); + "a typed query matches a tab title"); assert.deepEqual(ctx.filterTabs(tabOptions, "zzz"), [], - "a typed query with no match returns an empty list"); + "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"); + "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"); + "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"); + "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 label shows cleanly"); + "a stray leading space in a tab title is trimmed so the label shows cleanly"); assert.equal(ctx.filterTabs([{ id: "home", title: "" }], "home").length, 1, - "an untitled tab still matches a typed query via the id/title fallback"); + "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"); + "a leading-space tab title still matches a typed query"); // The main phase is ONE pool: commands/plugins/settings are all actions, ranked by relevance // (no group headers, no actions-vs-settings discrimination). const pool = [ - { id: "c1", title: "Layer Height", source: "Quality", group: "Quality : Layers", input: "" }, - { id: "s1", title: "Go to layer (percent)", source: "OrcaSlicer", group: "Commands", input: "percent" }, - { id: "c2", title: "Top Surface Layers", source: "Quality", group: "Quality : Layers", input: "" } + { id: "c1", title: "Layer Height", source: "Quality", group: "Quality : Layers", input: "" }, + { id: "s1", title: "Go to layer (percent)", source: "OrcaSlicer", group: "Commands", input: "percent" }, + { id: "c2", title: "Top Surface Layers", source: "Quality", group: "Quality : Layers", input: "" } ]; assert.deepEqual(ctx.searchActions(pool, ""), pool, "an empty query returns the pool unchanged"); assert.equal(ctx.searchActions(pool, "zzz").length, 0, "a query with no match returns nothing"); // "layer" matches multiple; the exact-titled action ranks above the loosely-matching command. assert.equal(ctx.searchActions(pool, "layer")[0].id, "c1", - "a title-exact match ranks above a partial match"); + "a title-exact match ranks above a partial match"); assert.equal(ctx.searchActions(pool, "layer").length >= 2, true, - "both a setting and a command match the same query in the same list"); + "both a setting and a command match the same query in the same list"); assert.equal(ctx.searchActions(pool, "surface")[0].id, "c2", - "a later-but-precise match still ranks by relevance, not by pool type"); + "a later-but-precise match still ranks by relevance, not by pool type"); // A perfect match (the needle as one contiguous run) outranks a fuzzy match of the same field - and a // contiguous GROUP/header hit ("Recent Projects") beats a scattered fuzzy TITLE hit ("Retraction Length"), // which is what the old flat title-bonus ranking got backwards. const perfectPool = [ - { id: "set", title: "Retraction Length", source: "Process : Quality : Retraction", group: "", input: "" }, - { id: "recent", title: "myproject.3mf", source: "/home/me/projects/myproject.3mf", group: "Recent Projects", input: "" } + { id: "set", title: "Retraction Length", source: "Process : Quality : Retraction", group: "", input: "" }, + { id: "recent", title: "myproject.3mf", source: "/home/me/projects/myproject.3mf", group: "Recent Projects", input: "" } ]; assert.equal(ctx.searchActions(perfectPool, "recent")[0].id, "recent", - "a contiguous header/group match ranks above a scattered fuzzy title match"); + "a contiguous header/group match ranks above a scattered fuzzy title match"); // Within a perfect match, the row-name (title) outranks the header (group): the action whose TITLE // contains the needle perfectly beats the action whose GROUP does, both being contiguous matches. const titleFirstPool = [ - { id: "grp", title: "Delete Selected", source: "OrcaSlicer", group: "Object", input: "" }, - { id: "t", title: "Object Preview", source: "OrcaSlicer", group: "View", input: "" } + { id: "grp", title: "Delete Selected", source: "OrcaSlicer", group: "Object", input: "" }, + { id: "t", title: "Object Preview", source: "OrcaSlicer", group: "View", input: "" } ]; assert.equal(ctx.searchActions(titleFirstPool, "object")[0].id, "t", - "a perfect title match ranks above an equally-perfect group match"); + "a perfect title match ranks above an equally-perfect group match"); // Highlighting: the needle is matched as a whole word / most-contiguous run, so "orient" lights up the // whole word in "Auto-Orient" instead of the stray "o" of "Auto" plus "rient" (greedy-leftmost). const orientPool = [ - { id: "ao", title: "Auto-Orient", source: "OrcaSlicer", group: "Object", input: "" } + { id: "ao", title: "Auto-Orient", source: "OrcaSlicer", group: "Object", input: "" } ]; ctx.searchActions(orientPool, "orient"); assert.deepEqual(ctx.matchIndex.ao.title, [[5, 11]], - "a whole-word match highlights the full word, not a scattered fuzzy pick"); + "a whole-word match highlights the full word, not a scattered fuzzy pick"); // commandList (the main-phase list) delegates to the ranked search for a typed query and returns // the mixed recents (no discrimination) for an empty query. const mixed = [ - { id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", input: "" }, - { id: "set", title: "Sparse Infill Density", source: "Quality", group: "Quality", input: "" } + { id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", input: "" }, + { id: "set", title: "Sparse Infill Density", source: "Quality", group: "Quality", input: "" } ]; assert.equal(ctx.commandList(mixed, [], "sli")[0].id, "cmd", - "a typed query keeps the relevance-ranked action list (best match first)"); + "a typed query keeps the relevance-ranked action list (best match first)"); assert.deepEqual(ctx.commandList(mixed, mixed.slice(0, 1), "").map(function (a) { return a.id; }), ["cmd"], - "an empty query shows the mixed recents list verbatim"); + "an empty query shows the mixed recents list verbatim"); // selectedActionId: resolves the active list (recents for an empty query, filtered list otherwise). assert.equal( - ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), [], ""), - null, - "Enter with an empty query and no recents must not resolve to an action the list never showed" + ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), [], ""), + null, + "Enter with an empty query and no recents must not resolve to an action the list never showed" ); assert.equal( - ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), [], "rep"), - "0123456789abcdef", - "a typed query resolves the list selection" + ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), [], "rep"), + "0123456789abcdef", + "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" + ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [duplicateActions[0]], ""), [], ""), + "0123456789abcdef", + "Enter with an empty query resolves the recent entry" ); assert.equal( - ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"], ""), - "fedcba9876543210", - "favourites stay runnable with an empty query - the fav bar is always visible" + ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"], ""), + "fedcba9876543210", + "favourites stay runnable with an empty query - the fav bar is always visible" ); // Fav quick-launch slots: digit 1..9 -> index 0..8, digit 0 -> index 9 (the 10th), else -1. @@ -160,21 +160,21 @@ assert.equal(ctx.K_FAV_LIMIT, 10, "the slot count matches the quick-launch cap") // nextSel: arrow-nav wrapping. Down wraps at the list bottom to the first row; Up wraps at the // list top to the last row ONLY when there's no fav bar above (else it goes to the fav bar). assert.deepEqual(ctx.nextSel({ zone: "list", i: 2 }, "ArrowDown", 3, 0), { zone: "list", i: 0 }, - "ArrowDown at the last row wraps to the first row"); + "ArrowDown at the last row wraps to the first row"); assert.deepEqual(ctx.nextSel({ zone: "list", i: 1 }, "ArrowDown", 3, 0), { zone: "list", i: 2 }, - "ArrowDown in the middle advances by one"); + "ArrowDown in the middle advances by one"); assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 3, 0), { zone: "list", i: 2 }, - "ArrowUp at the first row with no fav bar wraps to the last row"); + "ArrowUp at the first row with no fav bar wraps to the last row"); assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 3, 2), { zone: "fav", i: 0 }, - "ArrowUp at the first row with a fav bar goes to the fav bar (unchanged)"); + "ArrowUp at the first row with a fav bar goes to the fav bar (unchanged)"); assert.deepEqual(ctx.nextSel({ zone: "list", i: 2 }, "ArrowUp", 3, 0), { zone: "list", i: 1 }, - "ArrowUp in the middle moves up by one"); + "ArrowUp in the middle moves up by one"); assert.deepEqual(ctx.nextSel({ zone: "fav", i: 1 }, "ArrowDown", 3, 2), { zone: "list", i: 0 }, - "ArrowDown from the fav bar lands on the first list row"); + "ArrowDown from the fav bar lands on the first list row"); assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowDown", 1, 0), { zone: "list", i: 0 }, - "a single-row list never wraps off the end"); + "a single-row list never wraps off the end"); assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 1, 0), { zone: "list", i: 0 }, - "ArrowUp on the only row stays put"); + "ArrowUp on the only row stays put"); // Windowed list reveal: how many rows must be materialized to cover `fromIndex` plus `size` more, // clamped to the total. Drives the "render the next window on scroll / arrow-nav" append. diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index f7d045bc9a..07f0c992eb 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -1,285 +1,423 @@ -* { box-sizing: border-box; } -html, body { margin: 0; } -body { - font-family: var(--orca-font, "Segoe UI", sans-serif); - font-size: 13px; - color: var(--text, var(--orca-fg, #1b1c1e)); - background: var(--bg, var(--orca-bg, #fff)); - overflow: hidden; - user-select: none; +* { + box-sizing: border-box; } + +html, +body { + margin: 0; +} + +body { + font-family: var(--orca-font, "Segoe UI", sans-serif); + font-size: 13px; + color: var(--text, var(--orca-fg, #1b1c1e)); + background: var(--bg, var(--orca-bg, #fff)); + overflow: hidden; + user-select: none; +} + .launcher { - display: flex; - flex-direction: column; - background: var(--panel, var(--orca-bg, #fff)); - border: 1px solid var(--border, var(--orca-border, #ddd)); - overflow: hidden; - /* why: no height cap here - the launcher reports its true natural height to C++, which + display: flex; + flex-direction: column; + background: var(--panel, var(--orca-bg, #fff)); + border: 1px solid var(--border, var(--orca-border, #ddd)); + overflow: hidden; + /* why: no height cap here - the launcher reports its true natural height to C++, which sizes the popup to match (HTML is source of truth). The list's own max-height is what bounds growth; capping the launcher would make the measurement circular. */ } + .fav-bar { - flex: 0 0 auto; - display: flex; - align-items: center; - gap: 8px; - padding: 9px 10px; - border-bottom: 1px solid var(--border, var(--orca-border, #ddd)); - overflow-x: auto; /* scroll horizontally once favourites overflow the row */ - scrollbar-width: thin; - /* why: keep scrollIntoView (arrow-nav) from scrolling the first/last tile flush to the edge, + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 8px; + padding: 9px 10px; + border-bottom: 1px solid var(--border, var(--orca-border, #ddd)); + overflow-x: auto; + /* scroll horizontally once favourites overflow the row */ + scrollbar-width: thin; + /* why: keep scrollIntoView (arrow-nav) from scrolling the first/last tile flush to the edge, which would clip its selected outline. Matches the 10px horizontal padding. */ - scroll-padding-inline: 10px; + scroll-padding-inline: 10px; } -.fav-bar[hidden] { display: none; } + +.fav-bar[hidden] { + display: none; +} + /* Name of the selected favourite, above the bar; left-aligned to the tiles' 10px inset. */ -.fav-eyebrow { flex: 0 0 auto; padding: 8px 10px 0; } +.fav-eyebrow { + flex: 0 0 auto; + padding: 8px 10px 0; +} + .fav-tile { - flex: 0 0 auto; /* keep tiles full-size; don't shrink to fit - scroll instead */ - width: 30px; - height: 30px; - border: 0; - border-radius: 8px; - cursor: pointer; - color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%)); - font-weight: 700; - /* why: mirror .tile centering - icons/placeholder are 18px glyphs, and a bare <button> inherits + flex: 0 0 auto; + /* keep tiles full-size; don't shrink to fit - scroll instead */ + width: 30px; + height: 30px; + border: 0; + border-radius: 8px; + cursor: pointer; + color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%)); + font-weight: 700; + /* why: mirror .tile centering - icons/placeholder are 18px glyphs, and a bare <button> inherits 13px + UA padding, which would clip them. inline-flex + pad:0 + overflow:hidden fits them. */ - font-size: 12px; - padding: 0; - display: inline-flex; - align-items: center; - justify-content: center; - overflow: hidden; - background: var(--speed-tile-bg, #f0f0f0); - border: 1px solid var(--speed-tile-border, #d8d8d8); + font-size: 12px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + overflow: hidden; + background: var(--speed-tile-bg, #f0f0f0); + 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 { + 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); } +.fav-tile.sel .fav-unpin { + opacity: 1; +} + +.fav-unpin:hover { + background: rgba(127, 127, 127, .6); +} + /* Numbered quick-launch slot (Alt/Option+digit), top-left corner of the fav tile. */ .fav-slot { - position: absolute; - top: 1px; - left: 1px; - min-width: 11px; - height: 11px; - padding: 0 2px; - border-radius: 3px; - font-size: 8px; - font-weight: 600; - line-height: 11px; - text-align: center; - color: var(--muted, var(--orca-muted, #6b7280)); - background: rgba(127,127,127,.22); + position: absolute; + top: 1px; + left: 1px; + min-width: 11px; + height: 11px; + padding: 0 2px; + border-radius: 3px; + font-size: 8px; + font-weight: 600; + line-height: 11px; + text-align: center; + color: var(--muted, var(--orca-muted, #6b7280)); + background: rgba(127, 127, 127, .22); } + /* Transient flash banner pinned to the top of the launcher (e.g. "favourites are full"). */ .dial-flash { - flex: 0 0 auto; - padding: 4px 10px; - font-size: 11px; - color: var(--plugin-status-warn, #b45309); - background: var(--plugin-status-warn-bg, rgba(255,193,7,.22)); - animation: dial-flash-fade 2.5s ease forwards; + flex: 0 0 auto; + padding: 4px 10px; + font-size: 11px; + color: var(--plugin-status-warn, #b45309); + background: var(--plugin-status-warn-bg, rgba(255, 193, 7, .22)); + animation: dial-flash-fade 2.5s ease forwards; } -@keyframes dial-flash-fade { 0% { opacity: 0; } 10% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; } } + +@keyframes dial-flash-fade { + 0% { + opacity: 0; + } + + 10% { + opacity: 1; + } + + 80% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + .ctx-menu { - position: fixed; - z-index: 10; - min-width: 120px; - padding: 4px; - background: var(--panel, var(--orca-bg, #fff)); - border: 1px solid var(--border, var(--orca-border, #ddd)); - border-radius: 7px; - box-shadow: 0 4px 16px rgba(0,0,0,.18); + position: fixed; + z-index: 10; + min-width: 120px; + padding: 4px; + background: var(--panel, var(--orca-bg, #fff)); + border: 1px solid var(--border, var(--orca-border, #ddd)); + border-radius: 7px; + box-shadow: 0 4px 16px rgba(0, 0, 0, .18); } -.ctx-menu[hidden] { display: none; } + +.ctx-menu[hidden] { + display: none; +} + .ctx-item { - display: block; - width: 100%; - padding: 6px 10px; - border: 0; - border-radius: 5px; - background: transparent; - color: var(--text, var(--orca-fg, #1b1c1e)); - font: inherit; - text-align: left; - cursor: pointer; + display: block; + width: 100%; + padding: 6px 10px; + border: 0; + border-radius: 5px; + background: transparent; + color: var(--text, var(--orca-fg, #1b1c1e)); + font: inherit; + text-align: left; + cursor: pointer; } -.ctx-item:hover { background: var(--row-hover, rgba(127,127,127,.16)); } -.ctx-item:disabled { opacity: .4; cursor: default; } -.ctx-item:disabled:hover { background: transparent; } -.dial-head { flex: 0 0 auto; padding: 8px; } + +.ctx-item:hover { + background: var(--row-hover, rgba(127, 127, 127, .16)); +} + +.ctx-item:disabled { + opacity: .4; + cursor: default; +} + +.ctx-item:disabled:hover { + background: transparent; +} + +.dial-head { + flex: 0 0 auto; + padding: 8px; +} + .plugin-search { - display: flex; - align-items: center; - gap: 4px; - height: 30px; - padding: 0 8px; - background: var(--panel, var(--orca-bg, #fff)); - border: 1px solid var(--border, var(--orca-border, #ddd)); - border-radius: 7px; + display: flex; + align-items: center; + gap: 4px; + height: 30px; + padding: 0 8px; + background: var(--panel, var(--orca-bg, #fff)); + border: 1px solid var(--border, var(--orca-border, #ddd)); + border-radius: 7px; } + .plugin-search:focus-within { - border-color: var(--main-color, var(--orca-accent, #009688)); - box-shadow: 0 0 0 2px rgba(0,150,136,.25); + border-color: var(--main-color, var(--orca-accent, #009688)); + box-shadow: 0 0 0 2px rgba(0, 150, 136, .25); } -.plugin-search-icon { display: inline-flex; color: var(--muted, var(--orca-muted, #6b7280)); } + +.plugin-search-icon { + display: inline-flex; + color: var(--muted, var(--orca-muted, #6b7280)); +} + .plugin-search-input { - flex: 1; - min-width: 0; - background: transparent; - border: 0; - outline: 0; - color: var(--text, var(--orca-fg, #1b1c1e)); - font: inherit; + flex: 1; + min-width: 0; + background: transparent; + border: 0; + outline: 0; + color: var(--text, var(--orca-fg, #1b1c1e)); + font: inherit; } + .plugin-search-clear { - display: inline-flex; - align-items: center; - justify-content: center; - width: 12px; - height: 12px; - padding: 0; - border: 0; - border-radius: 50%; - color: var(--muted, var(--orca-muted, #6b7280)); - cursor: pointer; - background: rgba(127,127,127,.20); + display: inline-flex; + align-items: center; + justify-content: center; + width: 12px; + height: 12px; + padding: 0; + border: 0; + border-radius: 50%; + color: var(--muted, var(--orca-muted, #6b7280)); + cursor: pointer; + background: rgba(127, 127, 127, .20); } -.plugin-search-clear[hidden] { display: none; } + +.plugin-search-clear[hidden] { + display: none; +} + .dial-count { - padding: 4px 4px 0; - text-align: left; - font-size: 11px; - color: var(--muted, var(--orca-muted, #6b7280)); + padding: 4px 4px 0; + text-align: left; + font-size: 11px; + color: var(--muted, var(--orca-muted, #6b7280)); } -.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-count[hidden] { + display: none; } + .dial-list { - flex: 0 1 auto; - min-height: 0; - /* ADJUST HEIGHT HERE. The popup auto-resizes to the content (HTML is the source of truth), so + flex: 0 1 auto; + min-height: 0; + /* ADJUST HEIGHT HERE. The popup auto-resizes to the content (HTML is the source of truth), so this list cap drives the whole window height: --rows full rows + a ~30% peek of the next row as a "scroll for more" affordance. Bump --rows to show more rows; change 0.3 for a bigger/ smaller peek. --row-h MUST match .row min-height (44px). */ - --row-h: 44px; - --rows: 5; - max-height: calc(var(--rows) * var(--row-h) + 0.3 * var(--row-h) + 4px); - overflow-y: auto; - padding: 4px 4px 6px; - scrollbar-width: thin; + --row-h: 44px; + --rows: 5; + max-height: calc(var(--rows) * var(--row-h) + 0.3 * var(--row-h) + 4px); + overflow-y: auto; + padding: 4px 4px 6px; + scrollbar-width: thin; } -.dial-list.empty { overflow-y: hidden; } + +.dial-list.empty { + overflow-y: hidden; +} + /* Bottom spacer for the windowed command list: fills the un-rendered tail so the scrollbar reflects the full match count, while only a near-viewport window of rows exists in the DOM. */ -.dial-spacer-bottom { flex: 0 0 auto; } +.dial-spacer-bottom { + flex: 0 0 auto; +} + .row { - display: flex; - align-items: center; - gap: 10px; - min-height: 44px; - padding: 6px 8px; - border-radius: 7px; - cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + min-height: 44px; + padding: 6px 8px; + border-radius: 7px; + cursor: pointer; } -.row:hover { background: var(--row-hover, rgba(127,127,127,.16)); } -.row.sel { background: var(--row-selected, rgba(0,150,136,.18)); } + +.row:hover { + background: var(--row-hover, rgba(127, 127, 127, .16)); +} + +.row.sel { + background: var(--row-selected, rgba(0, 150, 136, .18)); +} + .tile { - flex: 0 0 auto; - width: 26px; - height: 26px; - border-radius: 6px; - color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%)); - font-weight: 700; - font-size: 12px; - display: inline-flex; - align-items: center; - justify-content: center; - background: var(--speed-tile-bg, #f0f0f0); - border: 1px solid var(--speed-tile-border, #d8d8d8); + flex: 0 0 auto; + width: 26px; + height: 26px; + border-radius: 6px; + color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%)); + font-weight: 700; + font-size: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--speed-tile-bg, #f0f0f0); + border: 1px solid var(--speed-tile-border, #d8d8d8); } -.row-left { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; } + +.row-left { + flex: 1 1 auto; + min-width: 0; + display: flex; + flex-direction: column; +} + .row-eyebrow { - font-size: 10px; - line-height: 1.3; - color: var(--muted, var(--orca-muted, #6b7280)); - opacity: .85; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + font-size: 10px; + line-height: 1.3; + color: var(--muted, var(--orca-muted, #6b7280)); + opacity: .85; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.row-line { display: flex; align-items: center; gap: 7px; min-width: 0; } -.row-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.row-name mark, .row-eyebrow mark { background: var(--plugin-status-warn-bg); color: var(--plugin-status-warn); border-radius: 2px; } -.row-sc { flex: 0 0 auto; display: inline-flex; gap: 3px; } + +.row-line { + display: flex; + align-items: center; + gap: 7px; + min-width: 0; +} + +.row-name { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.row-name mark, +.row-eyebrow mark { + background: var(--plugin-status-warn-bg); + color: var(--plugin-status-warn); + border-radius: 2px; +} + +.row-sc { + flex: 0 0 auto; + display: inline-flex; + gap: 3px; +} + kbd { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 18px; - height: 18px; - padding: 0 5px; - font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; - font-size: 11px; - color: var(--muted, var(--orca-muted, #6b7280)); - background: rgba(127,127,127,.12); - border: 1px solid var(--border, var(--orca-border, #ddd)); - border-radius: 4px; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; + font-size: 11px; + color: var(--muted, var(--orca-muted, #6b7280)); + background: rgba(127, 127, 127, .12); + border: 1px solid var(--border, var(--orca-border, #ddd)); + border-radius: 4px; } + .pin { - flex: 0 0 auto; - width: 24px; - height: 24px; - border: 0; - border-radius: 5px; - background: transparent; - color: var(--muted, var(--orca-muted, #6b7280)); - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - opacity: 0; + flex: 0 0 auto; + width: 24px; + height: 24px; + border: 0; + border-radius: 5px; + background: transparent; + color: var(--muted, var(--orca-muted, #6b7280)); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + opacity: 0; } -.pin svg { display: block; } + +.pin svg { + display: block; +} + .row:hover .pin:not(.on), -.row.sel .pin:not(.on) { opacity: .5; } -.pin.on { opacity: 1; color: var(--main-color, var(--orca-accent, #009688)); } -.pin:hover { background: rgba(127,127,127,.18); } +.row.sel .pin:not(.on) { + opacity: .5; +} + +.pin.on { + opacity: 1; + color: var(--main-color, var(--orca-accent, #009688)); +} + +.pin:hover { + background: rgba(127, 127, 127, .18); +} + .dial-empty { - min-height: 64px; - padding: 18px 10px; - display: flex; - align-items: center; - justify-content: center; - color: var(--muted, var(--orca-muted, #6b7280)); - text-align: center; + min-height: 64px; + padding: 18px 10px; + display: flex; + align-items: center; + justify-content: center; + color: var(--muted, var(--orca-muted, #6b7280)); + text-align: center; } diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index 370d808142..050814eee3 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -200,8 +200,8 @@ struct CommandAction : AppAction private: explicit CommandAction(const NativeCommand& c) - : AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, c.title, kOrcaSourceKey, kOrcaSourceName), - command_key(c.key) + : AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, c.title, kOrcaSourceKey, kOrcaSourceName) + , command_key(c.key) { this->kind = AppActionKind::Command; this->group = c.group; diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index 678de6986d..f54b439fb8 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -19,7 +19,7 @@ namespace Slic3r { namespace GUI { // How a source's action set changed. Drives the registry's refresh handlers. enum class ActionChange { Added, Removed }; -// What kind of runnable thing an action is. Drives the palette section + dispatch. +// What kind of runnable thing an action is. Drives the run-confirm gate (plugins ask, commands don't). enum class AppActionKind { Plugin, Command }; // Result of running an AppAction, in the action layer's own vocabulary. Concrete @@ -28,8 +28,8 @@ struct AppActionRunResult { enum class Level { Success, Info, Error, Busy }; - Level level = Level::Info; - wxString message; // empty = "nothing worth showing" + Level level = Level::Info; + wxString message; // empty = "nothing worth showing" }; // Tag carrying a precomputed action id, used by the explicit-id ctor below. It exists so the @@ -64,16 +64,16 @@ struct AppAction } // seeded from AppConfig for the snapshot / sort: - bool favourite = false; - int count = 0; - long long last = 0; // epoch seconds + bool favourite = false; + int count = 0; + 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 group; + // Second-phase input descriptor for the palette: "percent" (jump to layer by a 0-100 + // value) or "tab" (pick a notebook tab). Empty = run immediately on activation. std::string input; virtual ~AppAction() = default; @@ -87,18 +87,17 @@ protected: // why: source_key (not the display name) carries identity, so renaming the source's // display name leaves the id - and its persisted stats/favourite - intact. AppAction(std::string_view prefix, std::string title, std::string source_key, std::string source_name) - : m_id(compose_id(prefix, title, source_key)), - m_title(std::move(title)), - m_source_key(std::move(source_key)), - m_source_name(std::move(source_name)) {} + : m_id(compose_id(prefix, title, source_key)) + , m_title(std::move(title)) + , m_source_key(std::move(source_key)) + , m_source_name(std::move(source_name)) + {} // Explicit-id ctor: for actions whose id must NOT be derived from the display title // (e.g. a setting action keyed by opt_key+type, so a rename/localization never re-keys it). AppAction(AppActionId id, std::string title, std::string source_key, std::string source_name) - : m_id(std::move(id.id)), - m_title(std::move(title)), - m_source_key(std::move(source_key)), - m_source_name(std::move(source_name)) {} + : m_id(std::move(id.id)), m_title(std::move(title)), m_source_key(std::move(source_key)), m_source_name(std::move(source_name)) + {} private: std::string m_id; // <prefix>:<title>:<source_key> - stable identity + AppConfig key @@ -137,7 +136,7 @@ public: void remove(const std::string& id); // Always-clean read surface. UI thread only. - const AppAction* by_id(const std::string& id) const; + const AppAction* by_id(const std::string& id) const; // Hard cap on the favourites bar: the numbered quick-launch slots (Alt/Option+1..9, 0). static constexpr size_t kFavLimit = 10; @@ -146,8 +145,8 @@ public: AppActionRunResult run(const std::string& id, const std::string& param = {}); // runs + bumps stats // Pin/unpin. Returns false when `on` would exceed kFavLimit (the bar is full) so the // caller can surface a "favourites are full" hint instead of silently dropping the pin. - bool set_favourite(const std::string& id, bool on); - void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order + bool set_favourite(const std::string& id, bool on); + void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order // Ordered pinned list (the source of truth), capped at kFavLimit and deduped, matching the // visible bar the palette renders. @@ -169,8 +168,8 @@ public: nlohmann::json tab_options() const; private: - void seed_state(AppAction& a) const; // favourite/stats from config - AppAction* find(const std::string& id); + void seed_state(AppAction& a) const; // favourite/stats from config + AppAction* find(const std::string& id); // (Re)materialise the current visible config settings as SettingActions from the live // searcher (respecting printer-tech + user-mode + visibility filtering), removing stale ones. @@ -192,8 +191,8 @@ private: void refresh_source(const std::string& plugin_key, ActionChange change); void refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change); - bool m_started = false; // init() runs exactly once; guards double-subscription - std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock + bool m_started = false; // init() runs exactly once; guards double-subscription + std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock }; }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index db66afbd7b..2bb6c7ed9a 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -4375,10 +4375,10 @@ void MainFrame::technology_changed() m_menubar->SetMenuLabel(id, pt == ptFFF ? _omitL("Material Settings") : _L("Filament settings")); } -// Opens the calibration wizard for `calib_kind`, reusing the cached member dialogs the Calibration -// menu builds. This is the single source of truth for the wizard lifecycle: the Calibration menu -// handlers and the Speed Dial native commands both call it, so they share the same per-wizard -// member (fresh on first launch, reused thereafter). Call while the Prepare (3D) panel is shown. +// Opens the calibration wizard for `calib_kind`. Single source of truth for the wizard lifecycle: +// the Calibration menu handlers and the Speed Dial native commands both call it. Most wizards are +// cached members reused across launches; cornering/input-shaping build a fresh transient dialog. +// Call while the Prepare (3D) panel is shown. void MainFrame::run_calibration(CalibKind calib_kind) { switch (calib_kind) { diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index fc23e1dad8..2c4e5cddba 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -107,7 +107,6 @@ protected: }; // Calibration wizard identity, shared by MainFrame::run_calibration and the Speed Dial command runners. -// Kept in order with the wizard list below. enum class CalibKind : int { Temperature, @@ -364,10 +363,10 @@ public: void technology_changed(); - // Opens the calibration wizard for `kind`, reusing the cached member dialogs the Calibration - // menu builds (m_*_calib_dlg). Single source of truth for the wizard lifecycle: the Calibration - // menu handlers and the Speed Dial native commands both call this. Call while the Prepare (3D) - // panel is shown (menu items are gated on is_view3D_shown; the speed dial ensures it first). + // Opens the calibration wizard for `kind`. Single source of truth for the wizard lifecycle: + // the Calibration menu handlers and the Speed Dial native commands both call this. Most wizards + // are cached members; cornering/input-shaping are transient. Call while the Prepare (3D) panel + // is shown (menu items are gated on is_view3D_shown; the speed dial ensures it first). void run_calibration(CalibKind calib_kind); //BBS diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp index d0dfc78086..25d29df116 100644 --- a/src/slic3r/GUI/NativeCommands.cpp +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -58,9 +58,9 @@ AppActionRunResult object_op(Plater* plater, bool (*ok)(Plater*), void (*op)(Pla return {AppActionRunResult::Level::Success}; } -// 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; if the slicer result is already present the slider is -// repositioned immediately, otherwise the user can re-run after slicing. +// Jump the preview to a layer selected by a 0-100 percent of the layer range. The caller has already +// switched to Preview (which may request a slice); if a slicer result is present the slider is +// repositioned immediately, otherwise the jump is a no-op until the user re-slices. void go_to_layer(Plater* plater, const std::string& param) { if (!plater) @@ -100,8 +100,8 @@ AppActionRunResult view_command(Plater* plater, const std::string& dir) return {AppActionRunResult::Level::Success}; } -// Calibration wizards. Reuses MainFrame::run_calibration so the speed dial shows the same cached -// member dialogs as the Calibration menu (the menu handlers call run_calibration too). +// Calibration wizards. Routes through MainFrame::run_calibration, the same entry point as the +// Calibration menu (which caches most of the wizard dialogs). AppActionRunResult calib_command(CalibKind kind) { MainFrame* mf = wxGetApp().mainframe; @@ -115,8 +115,8 @@ AppActionRunResult calib_command(CalibKind kind) std::vector<NativeCommand> build_command_catalog() { std::vector<NativeCommand> out; - auto add = [&](std::string key, std::string title, std::string group, - std::function<AppActionRunResult(const std::string&)> runner, std::string input = {}) { + auto add = [&](std::string key, std::string title, std::string group, std::function<AppActionRunResult(const std::string&)> runner, + std::string input = {}) { out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(runner)}); }; @@ -207,12 +207,11 @@ std::vector<NativeCommand> build_command_catalog() plater->export_gcode_3mf(false); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), - [](const std::string&) { - if (Plater* plater = wxGetApp().plater()) - plater->export_gcode_3mf(true); - return AppActionRunResult{AppActionRunResult::Level::Success}; - }); + add("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + plater->export_gcode_3mf(true); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); // ---- Calibration ---- add("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), @@ -231,8 +230,7 @@ std::vector<NativeCommand> build_command_catalog() [](const std::string&) { return calib_command(CalibKind::InputShapingFreq); }); add("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), [](const std::string&) { return calib_command(CalibKind::InputShapingDamp); }); - add("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), - [](const std::string&) { return calib_command(CalibKind::VFA); }); + add("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), [](const std::string&) { return calib_command(CalibKind::VFA); }); // ---- View ---- for (auto [key, dir, title] : diff --git a/src/slic3r/GUI/NativeCommands.hpp b/src/slic3r/GUI/NativeCommands.hpp index 80c431364d..d586678be1 100644 --- a/src/slic3r/GUI/NativeCommands.hpp +++ b/src/slic3r/GUI/NativeCommands.hpp @@ -16,12 +16,12 @@ struct NativeCommand std::string key; std::string title; std::string group; - std::string input; // "settings"/"percent"/"tab" or "" for immediate run + std::string input; // "percent"/"tab" or "" for immediate run std::function<AppActionRunResult(const std::string& param)> runner; }; namespace NativeCommands { -// The full built-in command catalog, built once (init()). UI thread only. +// The full built-in command catalog, built once on first use. UI thread only. const std::vector<NativeCommand>& catalog(); // Dispatches `key` to its runner (unknown keys return a quiet Info). UI thread only. From 1d44320f30cb621675db64dd7b9a780fac411c51 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Thu, 10 Sep 2026 13:29:57 +0800 Subject: [PATCH 13/29] Updated unit tests --- .../web/dialog/SpeedDial/speeddial.test.js | 25 +++++++++++++++ src/slic3r/GUI/ActionRegistry.cpp | 30 +++++++++--------- src/slic3r/GUI/ActionRegistry.hpp | 4 +++ tests/slic3rutils/test_action_source.cpp | 31 +++++++++++++++++++ 4 files changed, 76 insertions(+), 14 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 5b51daffbe..61976ac9b6 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -186,4 +186,29 @@ assert.equal(ctx.revealTarget(100, -5, 50), 50, "negative start is clamped to th assert.equal(ctx.revealTarget(200, 50, 100), 150, "a scroll viewpoint reveals a window past the current rows"); assert.equal(ctx.revealTarget(10, 0, 50), 10, "a list shorter than one window stays fully materialized"); +// visibleFavourites: the quick-bar drops pins whose action no longer exists (plugin unloaded, +// command removed) and collapses duplicate ids, keeping the persisted pin order. +assert.deepEqual(ctx.visibleFavourites(["a", "b", "c"], [{ id: "a" }, { id: "b" }]), + ["a", "b"], "a pin with no live action is dropped from the quick-bar"); +assert.deepEqual(ctx.visibleFavourites(["b", "a", "b"], [{ id: "a" }, { id: "b" }]), + ["b", "a"], "duplicate pins collapse to the first occurrence"); +assert.deepEqual(ctx.visibleFavourites([], [{ id: "a" }]), [], "no pins renders an empty quick-bar"); +assert.deepEqual(ctx.visibleFavourites(["a"], []), [], "a stale config with no actions renders nothing"); + +// tileCode: monogram ladder - title initial, then title+source initials, then a stable ordinal +// by id. The ordinal is keyed by id, not by list order, so frecency reshuffles never renumber tiles. +const monoPool = [ + { id: "z", title: "Repair", source: "Mesh Tools" }, + { id: "a", title: "Repair", source: "Mesh Tools" }, + { id: "b", title: "Repair", source: "Filament" } +]; +assert.equal(ctx.tileCode(monoPool[0], monoPool), "MR2", + "same title+source resolves to source+title initials with an id-keyed ordinal (id z sorts after id a)"); +assert.equal(ctx.tileCode(monoPool[1], monoPool), "MR1", + "the earlier id is numbered first among same-title+source tiles"); +assert.equal(ctx.tileCode(monoPool[2], monoPool), "FR", + "same title but different source resolves to source+title initials"); +assert.equal(ctx.tileCode({ id: "x", title: "Slice", source: "OrcaSlicer" }, [{ id: "x", title: "Slice", source: "OrcaSlicer" }]), + "S", "a unique title resolves to the bare title initial"); + console.log("ok"); diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index 050814eee3..35503af44e 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -31,6 +31,19 @@ namespace Slic3r { namespace GUI { +std::vector<std::string> cap_favourites(const std::vector<std::string>& ids, size_t limit) +{ + std::vector<std::string> out; + out.reserve(std::min(ids.size(), limit)); + for (const auto& id : ids) { + if (out.size() >= limit) + break; + if (std::find(out.begin(), out.end(), id) == out.end()) + out.push_back(id); + } + return out; +} + namespace { constexpr const char* kConfigSection = "speed_dial"; @@ -468,18 +481,8 @@ bool ActionRegistry::set_favourite(const std::string& id, bool on) std::vector<std::string> ActionRegistry::favourite_ids() const { assert(wxThread::IsMain()); - // Enforce the cap + dedupe on read so the persisted order can never grow past kFavLimit, - // even from an older config. The pinned order is intentionally preserved (slice, not sort). - std::vector<std::string> favs = read_string_array("favourite_actions"); - std::vector<std::string> out; - out.reserve(std::min(favs.size(), kFavLimit)); - for (const auto& id : favs) { - if (out.size() >= kFavLimit) - break; - if (std::find(out.begin(), out.end(), id) == out.end()) - out.push_back(id); - } - return out; + // Enforce the cap + dedupe on read so the persisted order can never grow past kFavLimit, even from an older config. + return cap_favourites(read_string_array("favourite_actions"), kFavLimit); } void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids) @@ -496,8 +499,7 @@ void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids) if (std::find(next.begin(), next.end(), id) == next.end()) next.push_back(id); // never write the bar back larger than the quick-launch slots - if (next.size() > kFavLimit) - next.resize(kFavLimit); + next = cap_favourites(next, kFavLimit); write_section("favourite_actions", nlohmann::json(next)); } diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index f54b439fb8..2b7af0d132 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -106,6 +106,10 @@ private: std::string m_source_name; // display name of the action's source }; +// Cap + dedupe a persisted favourite-id list, preserving first-occurrence order. A stale or +// hand-edited config must never grow the quick-launch bar past `limit`, and a duplicated id must collapse to its first pin. +std::vector<std::string> cap_favourites(const std::vector<std::string>& ids, size_t limit); + // Self-contained sink and single owner of runnable actions for the app session. // // Workflow: diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index 9b6564ea98..35365c58c5 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -1,10 +1,13 @@ #include <catch2/catch_test_macros.hpp> #include "slic3r/GUI/ActionRegistry.hpp" +#include "slic3r/GUI/NativeCommands.hpp" #include <memory> +#include <set> #include <string> #include <type_traits> +#include <vector> using Slic3r::GUI::AppAction; using Slic3r::GUI::AppActionRunResult; @@ -81,3 +84,31 @@ TEST_CASE("Command actions are keyed by catalog key, not display title", "[speed CHECK(AppAction::compose_id("orca_command", "save_project", "orca") != AppAction::compose_id("orca_command", "load_project", "orca")); } + +// The quick-launch cap must stay 10 to match the numbered Alt/Option+1..9,0 keys. The web palette +// mirrors it as K_FAV_LIMIT (asserted in speeddial.test.js); the C++ side pins it here. +static_assert(Slic3r::GUI::ActionRegistry::kFavLimit == 10, "kFavLimit must stay 10"); + +TEST_CASE("Favourite lists are capped and deduped preserving order", "[speeddial][actions]") +{ + using Slic3r::GUI::cap_favourites; + + CHECK(cap_favourites({}, 10) == std::vector<std::string>{}); + CHECK(cap_favourites({"a", "b", "a"}, 10) == std::vector<std::string>{"a", "b"}); + CHECK(cap_favourites({"c", "a", "b", "c"}, 3) == std::vector<std::string>{"c", "a", "b"}); + CHECK(cap_favourites({"a", "b"}, 0) == std::vector<std::string>{}); +} + +TEST_CASE("Native command catalog has unique keys and present titles", "[speeddial][actions]") +{ + const std::vector<Slic3r::GUI::NativeCommand>& commands = Slic3r::GUI::NativeCommands::catalog(); + CHECK_FALSE(commands.empty()); + + std::set<std::string> seen; + for (const auto& c : commands) { + CHECK_FALSE(c.key.empty()); + CHECK_FALSE(c.title.empty()); + // A duplicated key would silently shadow the earlier command in the palette. + CHECK(seen.insert(c.key).second); + } +} From 8bbce371b2a0549b93373db1637c6a696dd49163 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Thu, 10 Sep 2026 18:34:05 +0800 Subject: [PATCH 14/29] Add Help actions. Add Toggle Developer action. Add warning popup when switching between process mode. Add primitives/handy models actions --- resources/web/dialog/SpeedDial/speeddial.js | 72 +++++- .../web/dialog/SpeedDial/speeddial.test.js | 49 ++++ resources/web/dialog/SpeedDial/style.css | 12 + src/slic3r/GUI/ActionRegistry.cpp | 36 ++- src/slic3r/GUI/ActionRegistry.hpp | 12 + src/slic3r/GUI/GUI_Factories.cpp | 214 ++++++++++-------- src/slic3r/GUI/GUI_Factories.hpp | 18 ++ src/slic3r/GUI/NativeCommands.cpp | 133 ++++++++++- src/slic3r/GUI/Search.cpp | 21 +- src/slic3r/GUI/Search.hpp | 8 + src/slic3r/GUI/SpeedDialDialog.cpp | 42 +++- tests/slic3rutils/test_action_source.cpp | 88 +++++++ 12 files changed, 585 insertions(+), 120 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index efe99db1b8..6f251d35eb 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -10,6 +10,11 @@ var sel = { zone: "list", i: 0 }; // zone: 'list' | 'fav' var lastResizeHeight = 0; var matchIndex = {}; +// The user's current settings mode (from the C++ payload) plus the rank order of the modes. Each +// action carries the mode it requires, so "would this need a switch?" is a rank comparison. +var USER_MODE = "simple"; +var MODE_RANK = { simple: 0, advanced: 1, expert: 2, develop: 3 }; + // ---- windowed list render ---------------------------------------------------- // The command list is rendered in windows (append-on-scroll) so a huge settings pool doesn't build // the whole DOM per keystroke. Rows are exactly ROW_H tall (matches .row min-height 44px; see --row-h, @@ -105,11 +110,15 @@ function searchActions(actions, query) { matchIndex = {}; if (!q) { searchNeedle = ""; return list.slice(0); } searchNeedle = NormText(q, false); + // Mode keywords ("advanced"/"expert"/"developer") are a union, not a filter: the normal text + // search still runs on the full query, and every setting requiring a named mode is appended. + var modes = modeFilterFromQuery(q); // Compiled once per pass, reused over every field: non-global so no exec()/lastIndex state leaks // between fields, and EscapeRegExp keeps regex metachars in the query literal. var wwRe = new RegExp("\\b" + EscapeRegExp(searchNeedle) + "\\b"); var scored = []; + var seen = {}; for (var i = 0; i < list.length; i++) { var a = list[i]; var t = fieldMatchScore(titleNorm(a), wwRe); @@ -126,13 +135,28 @@ function searchActions(actions, query) { useEyebrowGroup: !!(a.group) }; scored.push({ a: a, s: score }); + seen[a.id] = true; } scored.sort(function (x, y) { if (x.s !== y.s) return y.s - x.s; if (x.a.title !== y.a.title) return x.a.title < y.a.title ? -1 : 1; return x.a.id < y.a.id ? -1 : x.a.id > y.a.id ? 1 : 0; }); - return scored.map(function (e) { return e.a; }); + var result = scored.map(function (e) { return e.a; }); + if (modes.length) { + // Settings requiring a named mode, after the ranked text matches and without duplicates. + var extras = []; + for (var j = 0; j < list.length; j++) { + if (!seen[list[j].id] && modes.indexOf(list[j].mode) !== -1) + extras.push(list[j]); + } + extras.sort(function (x, y) { + if (x.title !== y.title) return x.title < y.title ? -1 : 1; + return x.id < y.id ? -1 : x.id > y.id ? 1 : 0; + }); + result = result.concat(extras); + } + return result; } // Pure: how many rows must be materialized to cover the given starting index plus `size` more. @@ -264,6 +288,40 @@ function prettySource(source) { return String(source || "").toLowerCase().replace(/\b\w/g, function (c) { return c.toUpperCase(); }); } +// True when the action's required mode is above the user's current mode, i.e. selecting it will +// prompt a mode switch. Unknown/empty modes are treated as "simple" so commands never flag. +function needsModeSwitch(item, userMode) { + var need = MODE_RANK[(item && item.mode) || "simple"] || 0; + var have = MODE_RANK[userMode || "simple"] || 0; + return need > have; +} + +// Short mode tag for an action that needs a switch, or "" when it is already available. +function modeBadge(item, userMode) { + if (!needsModeSwitch(item, userMode)) return ""; + if (item.mode === "develop") return "Developer"; + if (item.mode === "expert") return "Expert"; + if (item.mode === "advanced") return "Advanced"; + return ""; +} + +// Search keywords that name a settings mode. "developer" (and the internal "develop") both select +// Developer; Simple is deliberately absent so it never floods the list with every command. +var MODE_WORDS = { advanced: "advanced", expert: "expert", developer: "develop", develop: "develop" }; + +// The mode values named as whole words in `query`, deduped. Case/diacritic-insensitive via Norm. A +// query with no mode keyword returns [] so the normal text search is completely unaffected. +function modeFilterFromQuery(query) { + var norm = NormText(String(query || "").trim(), false); + var found = []; + norm.split(/[^a-z0-9]+/).forEach(function (word) { + var mode = MODE_WORDS[word]; + if (mode && found.indexOf(mode) === -1) + found.push(mode); + }); + return found; +} + // 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. function actionLabel(action, actions) { @@ -289,6 +347,7 @@ function stateFromPayload(payload) { actions: payload.actions || [], favourites: payload.favourites || [], recent: payload.recent || [], + userMode: payload.user_mode || "simple", query: "", sel: { zone: "list", i: 0 }, lastResizeHeight: 0, @@ -348,6 +407,7 @@ window.HandleStudio = function (payload) { ACTIONS = next.actions; FAVS = next.favourites; RECENTS = next.recent; + USER_MODE = next.userMode; query = next.query; sel = next.sel; lastResizeHeight = next.lastResizeHeight; @@ -470,7 +530,8 @@ function renderFav() { tile.className = "fav-tile" + (sel.zone === "fav" && sel.i === i ? " sel" : ""); tile.style.setProperty("--h", hue(id)); fillTile(tile, a); - tile.title = a.title; + var tileBadge = modeBadge(a, USER_MODE); + tile.title = a.title + (tileBadge ? " (" + tileBadge + ")" : ""); tile.setAttribute("aria-label", actionLabel(a, ACTIONS)); tile.onclick = function () { sel = { zone: "fav", i: i }; activateEntry(a); }; // Numbered quick-launch badge (Alt/Option+digit), drawn on the corner. @@ -584,6 +645,13 @@ function renderActionRow(a, i) { line.className = "row-line"; var name = markedText("row-name", a.title, mi ? mi.title : null); line.appendChild(name); + var badge = modeBadge(a, USER_MODE); + if (badge) { + var tag = document.createElement("span"); + tag.className = "row-mode"; + tag.textContent = badge; + line.appendChild(tag); + } if (a.shortcut) { var sc = document.createElement("div"); sc.className = "row-sc"; diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 61976ac9b6..c8fd43add6 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -211,4 +211,53 @@ assert.equal(ctx.tileCode(monoPool[2], monoPool), "FR", assert.equal(ctx.tileCode({ id: "x", title: "Slice", source: "OrcaSlicer" }, [{ id: "x", title: "Slice", source: "OrcaSlicer" }]), "S", "a unique title resolves to the bare title initial"); +// needsModeSwitch: a setting is gated only when its required mode outranks the user's current mode. +assert.equal(ctx.needsModeSwitch({ mode: "advanced" }, "simple"), true, "Advanced is gated in Simple mode"); +assert.equal(ctx.needsModeSwitch({ mode: "expert" }, "simple"), true, "Expert is gated in Simple mode"); +assert.equal(ctx.needsModeSwitch({ mode: "expert" }, "advanced"), true, "Expert is gated in Advanced mode"); +assert.equal(ctx.needsModeSwitch({ mode: "develop" }, "expert"), true, "Developer is gated in Expert mode"); +assert.equal(ctx.needsModeSwitch({ mode: "simple" }, "simple"), false, "a Simple setting is not gated"); +assert.equal(ctx.needsModeSwitch({ mode: "advanced" }, "advanced"), false, "an Advanced setting is not gated in Advanced mode"); +assert.equal(ctx.needsModeSwitch({ mode: "develop" }, "develop"), false, "a Developer setting is not gated in Developer mode"); +assert.equal(ctx.needsModeSwitch({}, "simple"), false, "a command with no mode is never gated"); + +// modeBadge: the tag text for gated settings, empty once the setting is available. +assert.equal(ctx.modeBadge({ mode: "advanced" }, "simple"), "Advanced", "Advanced badge text"); +assert.equal(ctx.modeBadge({ mode: "expert" }, "simple"), "Expert", "Expert badge text"); +assert.equal(ctx.modeBadge({ mode: "develop" }, "simple"), "Developer", "Developer badge text"); +assert.equal(ctx.modeBadge({ mode: "advanced" }, "advanced"), "", "no badge when the mode already matches"); + +// modeFilterFromQuery: whole-word, case-insensitive mode keywords -> internal mode values. "developer" +// maps to the internal "develop"; "simple" is deliberately not a keyword; a prefix is not a match. +assert.deepEqual(ctx.modeFilterFromQuery("advanced"), ["advanced"], "Advanced is a mode keyword"); +assert.deepEqual(ctx.modeFilterFromQuery("Expert"), ["expert"], "the keyword is case-insensitive"); +assert.deepEqual(ctx.modeFilterFromQuery("developer"), ["develop"], "Developer maps to the develop value"); +assert.deepEqual(ctx.modeFilterFromQuery("develop"), ["develop"], "the internal develop spelling also works"); +assert.deepEqual(ctx.modeFilterFromQuery("expert retraction"), ["expert"], "a keyword is found among other text"); +assert.deepEqual(ctx.modeFilterFromQuery("advanced expert"), ["advanced", "expert"], "multiple keywords are deduped in order"); +assert.deepEqual(ctx.modeFilterFromQuery("advanced advanced"), ["advanced"], "a repeated keyword is deduped"); +assert.deepEqual(ctx.modeFilterFromQuery("simple"), [], "Simple is not a mode keyword"); +assert.deepEqual(ctx.modeFilterFromQuery("advance"), [], "a mode-word prefix is not a whole-word match"); +assert.deepEqual(ctx.modeFilterFromQuery(""), [], "an empty query names no mode"); + +// searchActions mode union: a mode keyword keeps the normal text matches AND appends every setting +// requiring that mode. Not a filter - a Simple setting literally named "Advanced..." still shows, and +// commands (mode "simple") are never pulled in by a keyword. +const modePool = [ + { id: "a1", title: "Top Surface Layers", source: "Quality", group: "Quality : Layers", mode: "advanced" }, + { id: "a2", title: "Advanced Detection", source: "Quality", group: "Quality", mode: "simple" }, + { id: "a3", title: "Retraction Length", source: "Process", group: "Process : Quality", mode: "expert" }, + { id: "a4", title: "Slice", source: "OrcaSlicer", group: "Commands", mode: "simple" } +]; +var adv = ctx.searchActions(modePool, "advanced"); +assert.deepEqual(adv.map(function (a) { return a.id; }), ["a2", "a1"], + "a text match (a2) ranks above the mode-only setting (a1), and no expert/command leaks in"); +var expert = ctx.searchActions(modePool, "expert"); +assert.deepEqual(expert.map(function (a) { return a.id; }), ["a3"], "the expert keyword pulls in the expert setting"); +assert.deepEqual(ctx.searchActions(modePool, "developer"), [], "no developer settings means no mode extras"); +assert.equal(ctx.searchActions(modePool, "retraction")[0].id, "a3", + "a query with no mode keyword is unaffected by the mode union"); +assert.equal(ctx.searchActions([{ id: "both", title: "Advanced", source: "Quality", group: "", mode: "advanced" }], "advanced").length, + 1, "a setting that both matches text and requires the mode appears exactly once"); + console.log("ok"); diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index 07f0c992eb..d6bc8ff8bf 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -364,6 +364,18 @@ body { gap: 3px; } +/* Mode tag on settings above the user's current mode (Advanced/Expert/Developer). */ +.row-mode { + flex: 0 0 auto; + padding: 1px 6px; + font-size: 10px; + line-height: 1.4; + border-radius: 8px; + color: var(--plugin-status-warn); + background: var(--plugin-status-warn-bg); + white-space: nowrap; +} + kbd { display: inline-flex; align-items: center; diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index 35503af44e..d1b6e13fe1 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -162,6 +162,17 @@ std::string setting_type_context(Preset::Type type) } } +// Stable, non-localized mode token for the webview, which maps it to a badge ("Developer" etc.). +const char* mode_key(ConfigOptionMode mode) +{ + switch (mode) { + case comAdvanced: return "advanced"; + case comExpert: return "expert"; + case comDevelop: return "develop"; + default: return "simple"; + } +} + // A config setting exposed as a first-class action: selecting it jumps the sidebar to the option. // The id is keyed by opt_key+type (NOT the display label), so renaming/localizing never re-keys // the action; title/group/source are purely for display + search. run() performs the jump, and @@ -180,7 +191,8 @@ struct SettingAction : AppAction std::string title, std::string group, std::wstring category_in, - std::string source_name) + std::string source_name, + ConfigOptionMode mode_in) : AppAction(AppActionId{id_for(opt_key_in, type_in)}, std::move(title), kOrcaSourceKey, std::move(source_name)) , opt_key(std::move(opt_key_in)) , type(type_in) @@ -188,8 +200,9 @@ struct SettingAction : AppAction { // A setting is a single-phase command: activating it jumps the sidebar to the option // (like the sidebar's own settings search), then the dial closes. run() performs the jump. - this->kind = AppActionKind::Command; - this->group = std::move(group); + this->kind = AppActionKind::Command; + this->group = std::move(group); + this->required_mode = mode_in; } AppActionRunResult run(const std::string& /*param*/) const override @@ -509,10 +522,9 @@ void ActionRegistry::materialize_setting_actions() // Reuse the Sidebar's live searcher: it's the only OptionsSearcher whose groups_and_categories // map is populated (Tab::add_key feeds it at build time), and it already mirrors the current - // configs/mode/printer-technology - i.e. exactly what the sidebar's own search would show. A - // fresh OptionsSearcher has an empty groups_and_categories, so append_options() would drop every - // option and nothing would materialise. Turn each visible option into a SettingAction. - const std::vector<Search::Option>& options = wxGetApp().sidebar().get_searcher().all_options(); + // configs/printer-technology. Use the all-modes view so the Speed Dial lists every setting, + // including those above the user's current mode, and can prompt to switch before jumping. + const std::vector<Search::Option>& options = wxGetApp().sidebar().get_searcher().all_modes_options(); // Load the persisted per-action state ONCE (not per-option) so a re-materialised setting keeps // its recency/favourite; mirroring seed_state but amortised over the whole option set. @@ -540,7 +552,7 @@ void ActionRegistry::materialize_setting_actions() // title = the option leaf name (last label segment); group stays empty so the source path // (above) is the single display/search breadcrumb rather than being duplicated. auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, boost::nowide::narrow(label_w), std::string(), - opt.category_local, boost::nowide::narrow(path)); + opt.category_local, boost::nowide::narrow(path), opt.mode); action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); if (auto it = stats.find(id); it != stats.end() && it->is_object()) { @@ -726,7 +738,8 @@ nlohmann::json ActionRegistry::snapshot() {"source", a->source_name()}, {"group", a->group}, {"input", a->input}, - {"shortcut", ""}}); + {"shortcut", ""}, + {"mode", mode_key(a->required_mode)}}); }; nlohmann::json actions = nlohmann::json::array(); @@ -765,7 +778,10 @@ nlohmann::json ActionRegistry::snapshot() 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)}}; + return {{"actions", std::move(actions)}, + {"favourites", std::move(favourites)}, + {"recent", std::move(recent_json)}, + {"user_mode", mode_key(wxGetApp().get_mode())}}; } // ---- tab options (enumerate the MainFrame notebook's current pages) ---------- diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index 2b7af0d132..c1667e184d 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -2,6 +2,8 @@ #include <nlohmann/json.hpp> +#include <libslic3r/Config.hpp> + #include <wx/string.h> #include <wx/thread.h> @@ -75,6 +77,9 @@ struct AppAction // Second-phase input descriptor for the palette: "percent" (jump to layer by a 0-100 // value) or "tab" (pick a notebook tab). Empty = run immediately on activation. std::string input; + // Settings mode required to edit this action (SettingActions only). The palette prompts before + // running an action whose mode is above the user's current mode. comSimple for everything else. + ConfigOptionMode required_mode = comSimple; virtual ~AppAction() = default; // Re-resolves + runs (UI thread). `param` carries an optional per-run argument for @@ -106,6 +111,13 @@ private: std::string m_source_name; // display name of the action's source }; +// True when a setting at `setting_mode` cannot be edited in `current_mode` and the UI must switch +// first. Developer settings are handled as a separate prompt by the Speed Dial. +inline bool requires_mode_switch(ConfigOptionMode setting_mode, ConfigOptionMode current_mode) +{ + return setting_mode > current_mode; +} + // Cap + dedupe a persisted favourite-id list, preserving first-occurrence order. A stale or // hand-edited config must never grow the quick-launch bar past `limit`, and a duplicated id must collapse to its first pin. std::vector<std::string> cap_favourites(const std::vector<std::string>& ids, size_t limit); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index d8e54978fa..2678cd5d4b 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -578,113 +578,131 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty return sub_menu; } +// Orca: handy models shipped under <resources>/handy_models. Defining everything in one table keeps +// the menu label, the files to load and the per-model behavior in a single place. Labels are wrapped +// in L() so they are picked up for translation. Shared with the command palette. +const std::vector<MenuFactory::HandyModel>& MenuFactory::handy_models() +{ + static const std::vector<HandyModel> models = { + {"orca_cube", L("Orca Cube"), {"OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true}, + {"orcasliced_combo", L("OrcaSliced Combo"), {"OrcaSliced.3mf", "OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true}, + {"orca_badge", L("Orca Badge"), {"OrcaBadge.3mf"}}, + {"orca_tolerance_test", L("Orca Tolerance Test"), {"OrcaToleranceTest.drc"}}, + {"3dbenchy", L("3DBenchy"), {"3DBenchy.drc"}}, + {"cali_cat", L("Cali Cat"), {"calicat.drc"}}, + {"autodesk_fdm_test", L("Autodesk FDM Test"), {"ksr_fdmtest_v4.drc"}}, + {"voron_cube", L("Voron Cube"), {"Voron_Design_Cube_v7.drc"}}, + {"stanford_bunny", L("Stanford Bunny"), {"Stanford_Bunny.drc"}}, + {"orca_string_hell", L("Orca String Hell"), {"Orca_stringhell.drc"}, false, true}, + }; + return models; +} + +void MenuFactory::load_handy_model(std::size_t index) +{ + const std::vector<HandyModel>& models = handy_models(); + if (index >= models.size()) + return; + const HandyModel& model = models[index]; + + std::vector<boost::filesystem::path> input_files; + input_files.reserve(model.file_names.size()); + for (const auto& file_name : model.file_names) + input_files.push_back((boost::filesystem::path(Slic3r::resources_dir()) / "handy_models" / file_name)); + + Plater* pl = plater(); + if (!pl) + return; + pl->load_files(input_files, LoadStrategy::LoadModel); + if (model.arrange_after_import) { + pl->set_prepare_state(Job::PREPARE_STATE_MENU); + pl->arrange(); + } + + // Suggest to change settings for stringhell + // This serves as mini tutorial for new users + if (model.is_stringhell) { + wxGetApp().CallAfter([=] { + DynamicPrintConfig* m_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + + bool is_only_one_wall_top = m_config->opt_bool("only_one_wall_top"); + auto min_width_top_surface = m_config->option<ConfigOptionFloatOrPercent>("min_width_top_surface")->value; + if (is_only_one_wall_top && min_width_top_surface > 0) { + wxString msg_text = _L("This model features text embossment on the top surface. For optimal results, it is " + "advisable to set the 'One Wall Threshold (min_width_top_surface)' " + "to 0 for the 'Only One Wall on Top Surfaces' to work best.\n" + "Yes - Change these settings automatically\n" + "No - Do not change these settings for me"); + + MessageDialog dialog(wxGetApp().plater(), msg_text, _L("Suggestion"), wxICON_WARNING | wxYES | wxNO); + if (dialog.ShowModal() == wxID_YES) { + m_config->set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(0, false)); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); + wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config(); + } + wxGetApp().plater()->update(); + } + }); + } +} + // Orca: add submenu for adding handy models wxMenu* MenuFactory::append_submenu_add_handy_model(wxMenu* menu, ModelVolumeType type) { auto sub_menu = new wxMenu; - // Orca: handy models shipped under <resources>/handy_models. Defining everything in one table - // keeps the menu label, the files to load and the per-model behavior in a single place and - // avoids repeating the label strings (and the value-vs-pointer comparison pitfalls that come - // with that). Labels are wrapped in L() so they are picked up for translation. - struct HandyModel - { - const char* label; - std::vector<std::string> file_names; - bool arrange_after_import = false; - bool is_stringhell = false; - }; - static const std::vector<HandyModel> handy_models = { - {L("Orca Cube"), {"OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true}, - {L("OrcaSliced Combo"), {"OrcaSliced.3mf", "OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true}, - {L("Orca Badge"), {"OrcaBadge.3mf"}}, - {L("Orca Tolerance Test"), {"OrcaToleranceTest.drc"}}, - {L("3DBenchy"), {"3DBenchy.drc"}}, - {L("Cali Cat"), {"calicat.drc"}}, - {L("Autodesk FDM Test"), {"ksr_fdmtest_v4.drc"}}, - {L("Voron Cube"), {"Voron_Design_Cube_v7.drc"}}, - {L("Stanford Bunny"), {"Stanford_Bunny.drc"}}, - {L("Orca String Hell"), {"Orca_stringhell.drc"}, false, true}, - }; - - for (const auto& model : handy_models) { - append_menu_item( - sub_menu, wxID_ANY, _(model.label), "", - [&model](wxCommandEvent&) { - std::vector<boost::filesystem::path> input_files; - input_files.reserve(model.file_names.size()); - for (const auto& file_name : model.file_names) - input_files.push_back((boost::filesystem::path(Slic3r::resources_dir()) / "handy_models" / file_name)); - - plater()->load_files(input_files, LoadStrategy::LoadModel); - if (model.arrange_after_import) { - plater()->set_prepare_state(Job::PREPARE_STATE_MENU); - plater()->arrange(); - } - - // Suggest to change settings for stringhell - // This serves as mini tutorial for new users - if (model.is_stringhell) { - wxGetApp().CallAfter([=] { - DynamicPrintConfig* m_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; - - bool is_only_one_wall_top = m_config->opt_bool("only_one_wall_top"); - auto min_width_top_surface = m_config->option<ConfigOptionFloatOrPercent>("min_width_top_surface")->value; - if (is_only_one_wall_top && min_width_top_surface > 0) { - wxString msg_text = _L("This model features text embossment on the top surface. For optimal results, it is " - "advisable to set the 'One Wall Threshold (min_width_top_surface)' " - "to 0 for the 'Only One Wall on Top Surfaces' to work best.\n" - "Yes - Change these settings automatically\n" - "No - Do not change these settings for me"); - - MessageDialog dialog(wxGetApp().plater(), msg_text, _L("Suggestion"), wxICON_WARNING | wxYES | wxNO); - if (dialog.ShowModal() == wxID_YES) { - m_config->set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(0, false)); - wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); - wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config(); - } - wxGetApp().plater()->update(); - } - }); - } - }, - "", menu); + const std::vector<HandyModel>& models = handy_models(); + for (std::size_t i = 0; i < models.size(); ++i) { + append_menu_item(sub_menu, wxID_ANY, _(models[i].label), "", + [i](wxCommandEvent&) { MenuFactory::load_handy_model(i); }, "", menu); } - return sub_menu; } + +// Create a Text/SVG volume through the matching gizmo. `type == INVALID` means "create a new object". +// Shared by the add menu and the command palette. +static void add_volume_with_gizmo(GLGizmosManager::EType gizmo_type, ModelVolumeType type) +{ + Plater* pl = plater(); + if (!pl) + return; + const GLCanvas3D* canvas = pl->canvas3D(); + if (!canvas) + return; + GLGizmoBase* gizmo_base = canvas->get_gizmos_manager().get_gizmo(gizmo_type); + if (!gizmo_base) + return; + + ModelVolumeType volume_type = type; + // no selected object means create new object + if (volume_type == ModelVolumeType::INVALID) + volume_type = ModelVolumeType::MODEL_PART; + + auto screen_position = canvas->get_popup_menu_position(); + if (gizmo_type == GLGizmosManager::Emboss) { + auto* emboss = dynamic_cast<GLGizmoEmboss*>(gizmo_base); + if (emboss == nullptr) + return; + if (screen_position.has_value()) + emboss->create_volume(volume_type, *screen_position); + else + emboss->create_volume(volume_type); + } else if (gizmo_type == GLGizmosManager::Svg) { + auto* svg = dynamic_cast<GLGizmoSVG*>(gizmo_base); + if (svg == nullptr) + return; + if (screen_position.has_value()) + svg->create_volume(volume_type, *screen_position); + else + svg->create_volume(volume_type); + } +} + +void MenuFactory::add_text_volume(ModelVolumeType type) { add_volume_with_gizmo(GLGizmosManager::Emboss, type); } +void MenuFactory::add_svg_volume(ModelVolumeType type) { add_volume_with_gizmo(GLGizmosManager::Svg, type); } + static void append_menu_itemm_add_(const wxString& name, GLGizmosManager::EType gizmo_type, wxMenu *menu, ModelVolumeType type, bool is_submenu_item) { - auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) { - const GLCanvas3D *canvas = plater()->canvas3D(); - const GLGizmosManager &mng = canvas->get_gizmos_manager(); - GLGizmoBase *gizmo_base = mng.get_gizmo(gizmo_type); - - ModelVolumeType volume_type = type; - // no selected object means create new object - if (volume_type == ModelVolumeType::INVALID) - volume_type = ModelVolumeType::MODEL_PART; - - auto screen_position = canvas->get_popup_menu_position(); - if (gizmo_type == GLGizmosManager::Emboss) { - auto emboss = dynamic_cast<GLGizmoEmboss *>(gizmo_base); - assert(emboss != nullptr); - if (emboss == nullptr) return; - if (screen_position.has_value()) { - emboss->create_volume(volume_type, *screen_position); - } else { - emboss->create_volume(volume_type); - } - } else if (gizmo_type == GLGizmosManager::Svg) { - auto svg = dynamic_cast<GLGizmoSVG *>(gizmo_base); - assert(svg != nullptr); - if (svg == nullptr) return; - if (screen_position.has_value()) { - svg->create_volume(volume_type, *screen_position); - } else { - svg->create_volume(volume_type); - } - } - }; + auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) { add_volume_with_gizmo(gizmo_type, type); }; if (type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER || type == ModelVolumeType::INVALID // cannot use gizmo without selected object diff --git a/src/slic3r/GUI/GUI_Factories.hpp b/src/slic3r/GUI/GUI_Factories.hpp index f80ef85d53..ec7b0a0b03 100644 --- a/src/slic3r/GUI/GUI_Factories.hpp +++ b/src/slic3r/GUI/GUI_Factories.hpp @@ -4,6 +4,7 @@ #include <map> #include <vector> #include <array> +#include <cstddef> #include <wx/bitmap.h> @@ -51,6 +52,23 @@ public: static std::vector<wxBitmap> get_text_volume_bitmaps(); static std::vector<wxBitmap> get_svg_volume_bitmaps(); + // Orca: handy models shipped under <resources>/handy_models. The menu and the command palette + // share this table so the model list and its per-model behavior live in one place. + struct HandyModel + { + const char* key; + const char* label; + std::vector<std::string> file_names; + bool arrange_after_import = false; + bool is_stringhell = false; + }; + static const std::vector<HandyModel>& handy_models(); + static void load_handy_model(std::size_t index); + + // Add a Text/SVG volume through the Emboss/SVG gizmo. Shared by the add menu and the palette. + static void add_text_volume(ModelVolumeType type); + static void add_svg_volume(ModelVolumeType type); + MenuFactory(); ~MenuFactory() = default; diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp index 25d29df116..7dba9cf155 100644 --- a/src/slic3r/GUI/NativeCommands.cpp +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -2,18 +2,23 @@ #include "calib_dlg.hpp" #include "Camera.hpp" +#include "DailyTips.hpp" #include "GCodeViewer.hpp" #include "GLCanvas3D.hpp" #include "GUI.hpp" #include "GUI_App.hpp" +#include "GUI_Factories.hpp" +#include "GUI_ObjectList.hpp" #include "I18N.hpp" #include "IMSlider.hpp" #include "MainFrame.hpp" +#include "NetworkTestDialog.hpp" #include "Plater.hpp" #include "PluginsDialog.hpp" #include "PlateSettingsDialog.hpp" #include "DeviceCore/DevManager.h" +#include <libslic3r/Model.hpp> #include <libslic3r/Utils.hpp> #include <algorithm> @@ -24,6 +29,8 @@ #include <tuple> #include <utility> +#include <wx/utils.h> + namespace Slic3r { namespace GUI { namespace { @@ -112,6 +119,19 @@ AppActionRunResult calib_command(CalibKind kind) return {AppActionRunResult::Level::Success}; } +// Palette-only: developer mode overrides the saved mode (get_mode returns comDevelop), so choosing +// Simple/Advanced/Expert must clear it first. Mirrors Preferences: persist the flag, then update. +void select_mode(ConfigOptionMode mode) +{ + GUI_App& app = wxGetApp(); + const bool was_developer = app.app_config->get_bool("developer_mode"); + if (was_developer) + app.app_config->set_bool("developer_mode", false); + app.save_mode(mode); + if (was_developer) + app.app_config->save(); +} + std::vector<NativeCommand> build_command_catalog() { std::vector<NativeCommand> out; @@ -174,17 +194,26 @@ std::vector<NativeCommand> build_command_catalog() // ---- Mode ---- add("mode_simple", _u8L("Mode: Simple"), _u8L("Mode"), [](const std::string&) { - wxGetApp().save_mode(comSimple); + select_mode(comSimple); return AppActionRunResult{AppActionRunResult::Level::Success}; }); add("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode"), [](const std::string&) { - wxGetApp().save_mode(comAdvanced); + select_mode(comAdvanced); return AppActionRunResult{AppActionRunResult::Level::Success}; }); add("mode_expert", _u8L("Mode: Expert"), _u8L("Mode"), [](const std::string&) { - wxGetApp().save_mode(comExpert); + select_mode(comExpert); return AppActionRunResult{AppActionRunResult::Level::Success}; }); + // Mirrors Preferences > Developer > Developer mode: flip the flag, persist, refresh the UI. + add("toggle_developer_mode", _u8L("Toggle Developer Mode"), _u8L("Mode"), [](const std::string&) { + GUI_App& app = wxGetApp(); + const bool on = !app.app_config->get_bool("developer_mode"); + app.app_config->set_bool("developer_mode", on); + app.app_config->save(); + app.update_mode(); + return AppActionRunResult{AppActionRunResult::Level::Success, on ? _L("Developer mode enabled.") : _L("Developer mode disabled.")}; + }); // ---- Export pipeline ---- add("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), [](const std::string&) { @@ -320,6 +349,57 @@ std::vector<NativeCommand> build_command_catalog() return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); }); }); + // ---- Add Primitive ---- (the Add > Add Primitive submenu; creates a new object) + auto add_primitive = [&](std::string key, std::string title, const char* type_name) { + add(std::move(key), std::move(title), _u8L("Add Primitive"), [type_name](const std::string&) { + Plater* plater = wxGetApp().plater(); + if (plater) { + ensure_3d_view(plater); + if (ObjectList* list = wxGetApp().obj_list()) + list->load_generic_subobject(type_name, ModelVolumeType::INVALID); + } + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + }; + add_primitive("add_primitive_cube", _u8L("Cube"), "Cube"); + add_primitive("add_primitive_cylinder", _u8L("Cylinder"), "Cylinder"); + add_primitive("add_primitive_sphere", _u8L("Sphere"), "Sphere"); + add_primitive("add_primitive_cone", _u8L("Cone"), "Cone"); + add_primitive("add_primitive_disc", _u8L("Disc"), "Disc"); + add_primitive("add_primitive_torus", _u8L("Torus"), "Torus"); + add("add_primitive_text", _u8L("Text"), _u8L("Add Primitive"), [](const std::string&) { + Plater* plater = wxGetApp().plater(); + if (plater) { + ensure_3d_view(plater); + if (GLCanvas3D* canvas = plater->canvas3D()) + canvas->clear_popup_menu_position(); + MenuFactory::add_text_volume(ModelVolumeType::INVALID); + } + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("add_primitive_svg", _u8L("SVG"), _u8L("Add Primitive"), [](const std::string&) { + Plater* plater = wxGetApp().plater(); + if (plater) { + ensure_3d_view(plater); + if (GLCanvas3D* canvas = plater->canvas3D()) + canvas->clear_popup_menu_position(); + MenuFactory::add_svg_volume(ModelVolumeType::INVALID); + } + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + + // ---- Add Handy models ---- (the Add > Add Handy models submenu) + const std::vector<MenuFactory::HandyModel>& handy = MenuFactory::handy_models(); + for (std::size_t i = 0; i < handy.size(); ++i) { + add("add_handy_" + std::string(handy[i].key), Slic3r::GUI::I18N::translate_utf8(handy[i].label), _u8L("Add Handy models"), + [i](const std::string&) { + if (Plater* plater = wxGetApp().plater()) + ensure_3d_view(plater); + MenuFactory::load_handy_model(i); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + } + // ---- Plate ---- add("plate_add", _u8L("Add Plate"), _u8L("Plate"), [](const std::string&) { Plater* plater = wxGetApp().plater(); @@ -458,6 +538,53 @@ std::vector<NativeCommand> build_command_catalog() return AppActionRunResult{AppActionRunResult::Level::Success}; }); + // ---- Help ---- (mirrors the top-bar Help menu, plus the wiki/YouTube links) + add("help_keyboard_shortcuts", _u8L("Keyboard Shortcuts"), _u8L("Help"), [](const std::string&) { + wxGetApp().keyboard_shortcuts(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("help_setup_wizard", _u8L("Setup Wizard"), _u8L("Help"), [](const std::string&) { + wxGetApp().ShowUserGuide(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("help_open_config_folder", _u8L("Show Configuration Folder"), _u8L("Help"), [](const std::string&) { + Slic3r::GUI::desktop_open_datadir_folder(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("help_troubleshoot", _u8L("Troubleshoot Center"), _u8L("Help"), [](const std::string&) { + wxGetApp().troubleshoot(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("help_network_test", _u8L("Open Network Test"), _u8L("Help"), [](const std::string&) { + NetworkTestDialog dlg(wxGetApp().mainframe); + dlg.ShowModal(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("help_tip_of_the_day", _u8L("Show Tip of the Day"), _u8L("Help"), [](const std::string&) { + if (Plater* plater = wxGetApp().plater()) { + plater->get_dailytips()->open(); + if (GLCanvas3D* canvas = plater->get_current_canvas3D()) + canvas->set_as_dirty(); + } + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("help_check_updates", _u8L("Check for Updates"), _u8L("Help"), [](const std::string&) { + wxGetApp().check_new_version_sf(true, 1); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("help_about", _u8L("About OrcaSlicer"), _u8L("Help"), [](const std::string&) { + Slic3r::GUI::about(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("open_wiki", _u8L("Open Wiki"), _u8L("Help"), [](const std::string&) { + wxLaunchDefaultBrowser("https://www.orcaslicer.com/wiki/", wxBROWSER_NEW_WINDOW); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("open_youtube", _u8L("Open YouTube Channel"), _u8L("Help"), [](const std::string&) { + wxLaunchDefaultBrowser("https://www.youtube.com/@OfficialOrcaSlicer/videos", wxBROWSER_NEW_WINDOW); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + // ---- Plugins ---- add("open_plugins", _u8L("Open Plugins"), _u8L("Plugins"), [](const std::string&) { wxGetApp().open_plugins_dialog(); diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index f8fc51ed02..77b80208fc 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -85,7 +85,7 @@ static std::string get_key(const std::string &opt_key, Preset::Type type) { retu void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode) { - auto emplace = [this, type](const std::string key, const wxString &label) { + auto emplace = [this, type](std::vector<Option> &dst, const std::string &key, const wxString &label, ConfigOptionMode opt_mode) { const GroupAndCategory &gc = groups_and_categories[key]; if (gc.group.IsEmpty() || gc.category.IsEmpty()) return; @@ -99,13 +99,14 @@ void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type ty } if (!label.IsEmpty()) - options.emplace_back(Option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(), gc.group.ToStdWstring(), - _(gc.group).ToStdWstring(), gc.category.ToStdWstring(), GUI::Tab::translate_category(gc.category, type).ToStdWstring()}); + dst.emplace_back(Option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(), gc.group.ToStdWstring(), + _(gc.group).ToStdWstring(), gc.category.ToStdWstring(), GUI::Tab::translate_category(gc.category, type).ToStdWstring(), + false, opt_mode}); }; for (std::string opt_key : config->keys()) { const ConfigOptionDef &opt = config->def()->options.at(opt_key); - if (opt.mode > mode) continue; + const bool in_filtered = opt.mode <= mode; int cnt = 0; @@ -128,12 +129,17 @@ void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type ty wxString label = opt.full_label.empty() ? opt.label : opt.full_label; std::string key = get_key(opt_key, type); + auto add = [&](const std::string &k) { + if (in_filtered) + emplace(options, k, label, opt.mode); + emplace(options_all_modes, k, label, opt.mode); + }; if (cnt == 0) - emplace(key, label); + add(key); else for (int i = 0; i < cnt; ++i) // ! It's very important to use "#". opt_key#n is a real option key used in GroupAndCategory - emplace(key + "#" + std::to_string(i), label); + add(key + "#" + std::to_string(i)); } } @@ -307,6 +313,7 @@ OptionsSearcher::~OptionsSearcher() {} void OptionsSearcher::init(std::vector<InputInfo> input_values) { options.clear(); + options_all_modes.clear(); for (auto i : input_values) append_options(i.config, i.type, i.mode); sort_options(); @@ -318,6 +325,8 @@ void OptionsSearcher::apply(DynamicPrintConfig *config, Preset::Type type, Confi if (options.empty()) return; options.erase(std::remove_if(options.begin(), options.end(), [type](Option opt) { return opt.type == type; }), options.end()); + options_all_modes.erase(std::remove_if(options_all_modes.begin(), options_all_modes.end(), [type](Option opt) { return opt.type == type; }), + options_all_modes.end()); append_options(config, type, mode); diff --git a/src/slic3r/GUI/Search.hpp b/src/slic3r/GUI/Search.hpp index 859a012f46..e28ada2675 100644 --- a/src/slic3r/GUI/Search.hpp +++ b/src/slic3r/GUI/Search.hpp @@ -63,6 +63,7 @@ struct Option std::wstring category; std::wstring category_local; bool multi_category { false }; + ConfigOptionMode mode{comSimple}; // option's visibility threshold; drives the Speed Dial's mode prompt std::string opt_key() const; }; @@ -97,6 +98,9 @@ class OptionsSearcher PrinterTechnology printer_technology; std::vector<Option> options{}; + // Every option regardless of the current UI mode (Simple/Advanced/Expert/Developer), for the + // Speed Dial. The sidebar search keeps using the mode-filtered `options`. + std::vector<Option> options_all_modes{}; std::vector<FoundOption> found{}; void append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode); @@ -152,6 +156,10 @@ public: // The full gated option set built by init() (after visibility/mode/printer-tech filtering). // Used by the Speed Dial to materialise config settings as first-class actions. const std::vector<Option>& all_options() const { return options; } + + // Every option across all UI modes (Developer included), regardless of the current mode. + // Used by the Speed Dial so it can list settings the user would have to switch mode to edit. + const std::vector<Option>& all_modes_options() const { return options_all_modes; } }; //------------------------------------------ diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index fbc446c5a0..88913b2136 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -9,6 +9,8 @@ #include "Plater.hpp" #include "Widgets/WebViewHostDialog.hpp" +#include <libslic3r/AppConfig.hpp> + #include <algorithm> #include <wx/display.h> @@ -35,6 +37,17 @@ int json_int_or(const nlohmann::json& j, const char* key, int fallback) return it != j.end() && it->is_number() ? it->get<int>() : fallback; } +// Display name of a settings mode, for the mode-switch confirmation. +wxString mode_label(ConfigOptionMode mode) +{ + switch (mode) { + case comAdvanced: return _L("Advanced"); + case comExpert: return _L("Expert"); + case comDevelop: return _L("Developer"); + default: return _L("Simple"); + } +} + wxColour bg_color() { return wxGetApp().get_window_default_clr(); } // Give the WebKitGTK widget itself input focus, not its GtkScrolledWindow container. @@ -192,6 +205,32 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti // 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 ConfigOptionMode required = a->required_mode; + + // Settings the current mode hides require a switch first. Ask while the dial is still up; a + // cancel dismisses both (the dial also auto-hides when the modal takes activation). + if (requires_mode_switch(required, wxGetApp().get_mode())) { + const wxString setting = title.empty() ? from_u8(atitle) : from_u8(title); + if (required == comDevelop) { + RichMessageDialog dlg(wxGetApp().mainframe, + wxString::Format(_L("\"%s\" is a Developer setting. Enable Developer mode to edit it?"), + setting), + _L("Developer setting"), wxOK | wxCANCEL); + if (dlg.ShowModal() != wxID_OK) + return; + wxGetApp().app_config->set_bool("developer_mode", true); + wxGetApp().update_mode(); + } else { + RichMessageDialog dlg(wxGetApp().mainframe, + wxString::Format(_L("\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?"), + setting, mode_label(required), mode_label(wxGetApp().get_mode()), mode_label(required)), + _L("Switch settings mode"), wxOK | wxCANCEL); + if (dlg.ShowModal() != wxID_OK) + return; + wxGetApp().save_mode(required); + } + } + if (IsModal()) EndModal(wxID_CANCEL); else @@ -231,7 +270,8 @@ void SpeedDialWebDialog::send_actions() call_web_handler({{"command", "list_actions"}, {"actions", std::move(snap["actions"])}, {"favourites", std::move(snap["favourites"])}, - {"recent", std::move(snap["recent"])}}); + {"recent", std::move(snap["recent"])}, + {"user_mode", std::move(snap["user_mode"])}}); } }} // namespace Slic3r::GUI diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index 35365c58c5..0dde4eb382 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -112,3 +112,91 @@ TEST_CASE("Native command catalog has unique keys and present titles", "[speeddi CHECK(seen.insert(c.key).second); } } + +// The Help-menu commands, wiki/YouTube links and the developer-mode toggle are part of the palette. +// Guard their presence and that they stay grouped with their peers, so a catalog edit cannot drop +// or scatter them. Groups are compared to the peer's own group to stay independent of translation. +TEST_CASE("Native command catalog includes the Help and developer-mode commands", "[speeddial][actions]") +{ + const std::vector<Slic3r::GUI::NativeCommand>& commands = Slic3r::GUI::NativeCommands::catalog(); + auto find = [&commands](const std::string& key) -> const Slic3r::GUI::NativeCommand* { + for (const auto& c : commands) + if (c.key == key) + return &c; + return nullptr; + }; + + const Slic3r::GUI::NativeCommand* first = find("help_keyboard_shortcuts"); + REQUIRE(first != nullptr); + for (const char* key : {"help_setup_wizard", "help_open_config_folder", "help_troubleshoot", "help_network_test", + "help_tip_of_the_day", "help_check_updates", "help_about", "open_wiki", "open_youtube"}) { + const Slic3r::GUI::NativeCommand* c = find(key); + REQUIRE(c != nullptr); + CHECK(c->group == first->group); + } + + const Slic3r::GUI::NativeCommand* mode_simple = find("mode_simple"); + const Slic3r::GUI::NativeCommand* dev_mode = find("toggle_developer_mode"); + REQUIRE(mode_simple != nullptr); + REQUIRE(dev_mode != nullptr); + CHECK(dev_mode->group == mode_simple->group); +} + +// Every "Add Primitive" item and shipped handy model has a palette command, grouped as in the Add +// menu. Groups are compared to a peer's own group to stay independent of translation. +TEST_CASE("Native command catalog covers the Add menus", "[speeddial][actions]") +{ + const std::vector<Slic3r::GUI::NativeCommand>& commands = Slic3r::GUI::NativeCommands::catalog(); + auto group_of = [&commands](const std::string& key) -> const std::string* { + for (const auto& c : commands) + if (c.key == key) + return &c.group; + return nullptr; + }; + + const std::string* primitive_group = group_of("add_primitive_cube"); + REQUIRE(primitive_group != nullptr); + for (const char* key : {"add_primitive_cylinder", "add_primitive_sphere", "add_primitive_cone", "add_primitive_disc", + "add_primitive_torus", "add_primitive_text", "add_primitive_svg"}) { + const std::string* group = group_of(key); + INFO(key); + REQUIRE(group != nullptr); + CHECK(*group == *primitive_group); + } + + const std::string* handy_group = group_of("add_handy_orca_cube"); + REQUIRE(handy_group != nullptr); + for (const char* key : {"add_handy_orcasliced_combo", "add_handy_orca_badge", "add_handy_orca_tolerance_test", + "add_handy_3dbenchy", "add_handy_cali_cat", "add_handy_autodesk_fdm_test", "add_handy_voron_cube", + "add_handy_stanford_bunny", "add_handy_orca_string_hell"}) { + const std::string* group = group_of(key); + INFO(key); + REQUIRE(group != nullptr); + CHECK(*group == *handy_group); + } +} + +// A setting whose mode is above the user's current mode must be prompted before it can be edited. +// Developer settings (comDevelop) are above every non-developer mode, so they always prompt then. +TEST_CASE("Settings above the current mode require a switch", "[speeddial][actions]") +{ + using Slic3r::GUI::requires_mode_switch; + using Slic3r::comAdvanced; + using Slic3r::comDevelop; + using Slic3r::comExpert; + using Slic3r::comSimple; + + CHECK(requires_mode_switch(comAdvanced, comSimple)); + CHECK(requires_mode_switch(comExpert, comSimple)); + CHECK(requires_mode_switch(comExpert, comAdvanced)); + CHECK(requires_mode_switch(comDevelop, comSimple)); + CHECK(requires_mode_switch(comDevelop, comAdvanced)); + CHECK(requires_mode_switch(comDevelop, comExpert)); + + CHECK_FALSE(requires_mode_switch(comSimple, comSimple)); + CHECK_FALSE(requires_mode_switch(comSimple, comAdvanced)); + CHECK_FALSE(requires_mode_switch(comAdvanced, comAdvanced)); + CHECK_FALSE(requires_mode_switch(comAdvanced, comExpert)); + CHECK_FALSE(requires_mode_switch(comExpert, comExpert)); + CHECK_FALSE(requires_mode_switch(comDevelop, comDevelop)); +} From 42bee12481254ebaccb425533526d6e2548e8200 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Fri, 11 Sep 2026 11:36:24 +0800 Subject: [PATCH 15/29] Speed Dial: replace tile monograms with native SVG icons Tiles previously rendered a colored monogram (title/source initials plus an ordinal) with a per-id hue. Show the matching native SVG icon instead: - AppAction/NativeCommand gain an `icon` field; commands get a curated key->icon table and settings inherit their group header's icon. - Notebook tracks each page's resource icon name and reports it in tab_options(), so the tab picker can show it too. - Searcher records the group icon so settings keep it through search. - Web tile rendering swaps monogramFor/hue for an <img>; drop the now-unused hue/text CSS vars. Add a test asserting every non-empty icon resolves to a shipped SVG. --- resources/web/dialog/SpeedDial/speeddial.js | 67 +++++-------- .../web/dialog/SpeedDial/speeddial.test.js | 24 ++--- resources/web/dialog/SpeedDial/style.css | 21 +++-- resources/web/dialog/css/theme.css | 4 - src/slic3r/GUI/ActionRegistry.cpp | 19 +++- src/slic3r/GUI/ActionRegistry.hpp | 3 + src/slic3r/GUI/NativeCommands.cpp | 94 ++++++++++++++++++- src/slic3r/GUI/NativeCommands.hpp | 4 +- src/slic3r/GUI/Notebook.cpp | 2 + src/slic3r/GUI/Notebook.hpp | 10 ++ src/slic3r/GUI/OptionsGroup.cpp | 2 +- src/slic3r/GUI/Search.cpp | 7 +- src/slic3r/GUI/Search.hpp | 5 +- src/slic3r/GUI/Tab.cpp | 8 +- tests/slic3rutils/test_action_source.cpp | 43 +++++++++ 15 files changed, 231 insertions(+), 82 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 6f251d35eb..9045262ea3 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -222,42 +222,29 @@ function shouldRenderActionList(query) { return !!((query || "").trim()); } -// 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 items stay distinct. -// 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. -function monogramFor(item, list, titleOf, sourceOf, idOf) { - var items = list || []; - var title = titleOf(item) || " "; - var ti = title.charAt(0).toUpperCase(); - var sameTitle = items.filter(function (o) { return (titleOf(o) || " ").charAt(0).toUpperCase() === ti; }); - if (sameTitle.length <= 1) - return ti; - var source = sourceOf(item) || " "; - var pi = source.charAt(0).toUpperCase(); - var sameSource = sameTitle.filter(function (o) { return (sourceOf(o) || " ").charAt(0).toUpperCase() === pi; }); - if (sameSource.length <= 1) - return pi + ti; - 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++) - if (sameSource[i] === item || idOf(sameSource[i]) === idOf(item)) - return pi + ti + (i + 1); - return pi + ti; +// Tile pictogram base path. The page lives at resources/web/dialog/SpeedDial/, so this climbs to +// resources/images/ where the same SVG icons the native GUI controls use are shipped. +var ICON_BASE = "../../../images/"; + +// SVG base name for an action's tile pictogram, or "" when it has none (commands without a GUI +// icon, plugins). Pure so the node-vm test can exercise it. +function actionIcon(a) { + return (a && a.icon) ? a.icon : ""; } -// Action tile code - see monogramFor for the escalation ladder. Settings are actions now, so -// they share this ladder (title initial, then source, then a stable ordinal). -function tileCode(action, actions) { - return monogramFor(action, actions, - function (o) { return o.title; }, - function (o) { return o.source; }, - function (o) { return o.id; }); -} - -// Put an action's monogram into a tile (search row or favourites tile). A null action (a tab row -// with no backing action) renders an empty tile. -function fillTile(tile, a) { - tile.textContent = a ? tileCode(a, ACTIONS) : ""; +// Put a pictogram into a tile (search row, favourites tile, or tab row). No icon leaves the tile +// blank. `mono` marks the white tab-strip glyphs, which the CSS recolors to the shared gray. +function fillTile(tile, a, mono) { + tile.textContent = ""; + var icon = actionIcon(a); + if (!icon) + return; + var img = document.createElement("img"); + img.className = mono ? "tile-icon tab-mono" : "tile-icon"; + img.src = ICON_BASE + icon + ".svg"; + img.alt = ""; + img.setAttribute("aria-hidden", "true"); + tile.appendChild(img); } // The active list for the main phase. A typed query ranks every action (commands/plugins/settings) @@ -461,13 +448,6 @@ function currentList() { return []; // percent - the input itself is the only field } -function hue(id) { - var h = 0; - for (var i = 0; i < id.length; i++) - h = (h * 31 + id.charCodeAt(i)) >>> 0; - return h % 360; -} - // Build a <div class=className> with the search-match ranges wrapped in <mark>. Used for both the // title and the source eyebrow. Pure (only touches the document factory), so the node-vm test never // calls it and load-time stays DOM-free. @@ -528,7 +508,6 @@ function renderFav() { var a = byId(id); var tile = document.createElement("button"); tile.className = "fav-tile" + (sel.zone === "fav" && sel.i === i ? " sel" : ""); - tile.style.setProperty("--h", hue(id)); fillTile(tile, a); var tileBadge = modeBadge(a, USER_MODE); tile.title = a.title + (tileBadge ? " (" + tileBadge + ")" : ""); @@ -629,7 +608,6 @@ function renderActionRow(a, i) { var tile = document.createElement("div"); tile.className = "tile"; - tile.style.setProperty("--h", hue(a.id)); fillTile(tile, a); var left = document.createElement("div"); @@ -805,8 +783,7 @@ function renderTabRow(t, i) { var tile = document.createElement("div"); tile.className = "tile"; - tile.style.setProperty("--h", hue(t.id)); - fillTile(tile, null); + fillTile(tile, t, true); var left = document.createElement("div"); left.className = "row-left"; diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index c8fd43add6..4fe72aba11 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -195,21 +195,15 @@ assert.deepEqual(ctx.visibleFavourites(["b", "a", "b"], [{ id: "a" }, { id: "b" assert.deepEqual(ctx.visibleFavourites([], [{ id: "a" }]), [], "no pins renders an empty quick-bar"); assert.deepEqual(ctx.visibleFavourites(["a"], []), [], "a stale config with no actions renders nothing"); -// tileCode: monogram ladder - title initial, then title+source initials, then a stable ordinal -// by id. The ordinal is keyed by id, not by list order, so frecency reshuffles never renumber tiles. -const monoPool = [ - { id: "z", title: "Repair", source: "Mesh Tools" }, - { id: "a", title: "Repair", source: "Mesh Tools" }, - { id: "b", title: "Repair", source: "Filament" } -]; -assert.equal(ctx.tileCode(monoPool[0], monoPool), "MR2", - "same title+source resolves to source+title initials with an id-keyed ordinal (id z sorts after id a)"); -assert.equal(ctx.tileCode(monoPool[1], monoPool), "MR1", - "the earlier id is numbered first among same-title+source tiles"); -assert.equal(ctx.tileCode(monoPool[2], monoPool), "FR", - "same title but different source resolves to source+title initials"); -assert.equal(ctx.tileCode({ id: "x", title: "Slice", source: "OrcaSlicer" }, [{ id: "x", title: "Slice", source: "OrcaSlicer" }]), - "S", "a unique title resolves to the bare title initial"); +// actionIcon: the SVG base name for a tile's pictogram, or "" when the action has none (blank tile). +assert.equal(ctx.actionIcon({ id: "x", title: "Slice", icon: "media_play" }), "media_play", + "an action's icon base name is returned verbatim"); +assert.equal(ctx.actionIcon({ id: "x", title: "Go to tab...", icon: "" }), "", + "an empty icon renders a blank tile"); +assert.equal(ctx.actionIcon({ id: "x", title: "Plugin action" }), "", + "a missing icon field renders a blank tile"); +assert.equal(ctx.actionIcon(null), "", + "a null action (tab row) renders a blank tile"); // needsModeSwitch: a setting is gated only when its required mode outranks the user's current mode. assert.equal(ctx.needsModeSwitch({ mode: "advanced" }, "simple"), true, "Advanced is gated in Simple mode"); diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index d6bc8ff8bf..3aea0cf15a 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -60,9 +60,7 @@ body { border: 0; border-radius: 8px; cursor: pointer; - color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%)); - font-weight: 700; - /* why: mirror .tile centering - icons/placeholder are 18px glyphs, and a bare <button> inherits + /* why: mirror .tile centering - icons/placeholder are 16px, and a bare <button> inherits 13px + UA padding, which would clip them. inline-flex + pad:0 + overflow:hidden fits them. */ font-size: 12px; padding: 0; @@ -310,9 +308,6 @@ body { width: 26px; height: 26px; border-radius: 6px; - color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%)); - font-weight: 700; - font-size: 12px; display: inline-flex; align-items: center; justify-content: center; @@ -320,6 +315,20 @@ body { border: 1px solid var(--speed-tile-border, #d8d8d8); } +/* Native SVG pictogram in a tile; blank tiles (no icon) have no child. */ +.tile-icon { + width: 16px; + height: 16px; + display: block; + pointer-events: none; +} + +/* Tab-strip glyphs are drawn white for the dark tab bar; recolor to the shared #949494 gray so + they read on both the light and dark tile backgrounds. */ +.tile-icon.tab-mono { + filter: brightness(0) invert(.58); +} + .row-left { flex: 1 1 auto; min-width: 0; diff --git a/resources/web/dialog/css/theme.css b/resources/web/dialog/css/theme.css index b0c78712ef..00680bf0b9 100644 --- a/resources/web/dialog/css/theme.css +++ b/resources/web/dialog/css/theme.css @@ -55,8 +55,6 @@ /* Speed dial icon tile treatment */ --speed-tile-bg: #efefef; --speed-tile-border: #d8d8d8; - --speed-tile-text-s: 74%; - --speed-tile-text-l: 34%; } :root[data-orca-theme="dark"] { @@ -95,8 +93,6 @@ /* Speed dial icon tile treatment */ --speed-tile-bg: #34343b; --speed-tile-border: #50505a; - --speed-tile-text-s: 82%; - --speed-tile-text-l: 84%; } /* Re-theme the shared common.css chrome through variables (replaces dark.css's diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index d1b6e13fe1..d1ce814b23 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -232,6 +232,7 @@ private: this->kind = AppActionKind::Command; this->group = c.group; this->input = c.input; + this->icon = c.icon; } }; @@ -554,6 +555,19 @@ void ActionRegistry::materialize_setting_actions() auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, boost::nowide::narrow(label_w), std::string(), opt.category_local, boost::nowide::narrow(path), opt.mode); + // Tile pictogram = the icon of the setting's own group header (e.g. Advanced -> param_advanced), + // the one shown next to it in the page. Fall back to the page/category icon for groups + // without one. Keys are the English titles the GUI registers. + action->icon = opt.group_icon; + if (action->icon.empty() && !opt.category.empty()) { + if (Tab* tab = wxGetApp().get_tab(opt.type); tab) { + const auto& icons = tab->get_category_icon_map(); + auto it = icons.find(wxString(opt.category)); + if (it != icons.end()) + action->icon = it->second; + } + } + action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); if (auto it = stats.find(id); it != stats.end() && it->is_object()) { action->count = it->value("count", 0); @@ -738,6 +752,7 @@ nlohmann::json ActionRegistry::snapshot() {"source", a->source_name()}, {"group", a->group}, {"input", a->input}, + {"icon", a->icon}, {"shortcut", ""}, {"mode", mode_key(a->required_mode)}}); }; @@ -800,7 +815,9 @@ nlohmann::json ActionRegistry::tab_options() const const wxString id = notebook->GetPageName(i); if (id.empty()) continue; - out.push_back({{"id", id.ToStdString()}, {"title", notebook->GetPageText(i).ToStdString()}}); + out.push_back({{"id", id.ToStdString()}, + {"title", notebook->GetPageText(i).ToStdString()}, + {"icon", notebook->GetPageIcon(i)}}); } return out; } diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index c1667e184d..cc73d3ad5c 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -77,6 +77,9 @@ struct AppAction // Second-phase input descriptor for the palette: "percent" (jump to layer by a 0-100 // value) or "tab" (pick a notebook tab). Empty = run immediately on activation. std::string input; + // Tile pictogram: SVG base name under resources/images; empty renders a blank tile (commands + // without a GUI icon, plugins). Set from NativeCommands / the setting's category icon. + std::string icon; // Settings mode required to edit this action (SettingActions only). The palette prompts before // running an action whose mode is above the user's current mode. comSimple for everything else. ConfigOptionMode required_mode = comSimple; diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp index 7dba9cf155..3886f8eb11 100644 --- a/src/slic3r/GUI/NativeCommands.cpp +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -25,6 +25,7 @@ #include <cmath> #include <cstdlib> #include <exception> +#include <map> #include <string> #include <tuple> #include <utility> @@ -132,12 +133,103 @@ void select_mode(ConfigOptionMode mode) app.app_config->save(); } +// Tile pictogram per command: the SVG base name of the icon the matching GUI control already uses +// (menu/toolbar/sidebar). Absent key => blank tile. Keeping this as one table makes the curation +// reviewable and lets a test check every value resolves to a real file. +const std::map<std::string, std::string>& command_icons() +{ + static const std::map<std::string, std::string> icons = { + // Slice & Export + {"slice_and_preview", "media_play"}, + {"export_gcode", "menu_export_gcode"}, + {"export_stl", "menu_export_stl"}, + {"export_stl_multi", "menu_export_stl"}, + {"export_sliced_file", "menu_export_sliced_file"}, + {"export_all_sliced_file", "menu_export_sliced_file"}, + {"export_toolpaths_obj", "menu_export_toolpaths"}, + {"export_config", "menu_export_config"}, + {"export_3mf", "menu_save"}, + {"export_drc_single", "menu_export_stl"}, + {"export_drc_multi", "menu_export_stl"}, + // Commands + {"load_project", "menu_open"}, + {"save_project", "menu_save"}, + {"save_project_as", "menu_save"}, + {"open_preferences", "cog"}, + {"go_to_layer", "height_range_layer"}, + // Mode: the sidebar mode toggle's own icon (ParamsPanel). + {"mode_simple", "advanced"}, + {"mode_advanced", "advanced"}, + {"mode_expert", "advanced"}, + {"toggle_developer_mode", "advanced"}, + // Calibration + {"calib_temperature", "calib_sf"}, + {"calib_max_volumetric", "calib_sf"}, + {"calib_pressure_advance", "calib_sf"}, + {"calib_flow_ratio", "calib_sf"}, + {"calib_retraction", "calib_sf"}, + {"calib_cornering", "calib_sf"}, + {"calib_input_shaping_freq", "calib_sf"}, + {"calib_input_shaping_damp", "calib_sf"}, + {"calib_vfa", "calib_sf"}, + // View + {"reset_window_layout", "toolbar_reset"}, + // Object + {"obj_delete", "menu_delete"}, + {"obj_delete_all", "menu_remove"}, + {"obj_mirror_x", "menu_mirror_x"}, + {"obj_mirror_y", "menu_mirror_y"}, + {"obj_mirror_z", "menu_mirror_z"}, + {"obj_split_objects", "menu_split_objects"}, + {"obj_split_parts", "menu_split_parts"}, + {"obj_drop", "toolbar_flatten"}, + {"obj_instances_up", "instance_add"}, + {"obj_instances_down", "instance_remove"}, + {"obj_arrange", "toolbar_arrange"}, + {"obj_orient", "toolbar_orient"}, + // Add Primitive + {"add_primitive_cube", "menu_obj_cube"}, + {"add_primitive_cylinder", "menu_obj_cylinder"}, + {"add_primitive_sphere", "menu_obj_sphere"}, + {"add_primitive_cone", "menu_obj_cone"}, + {"add_primitive_disc", "menu_obj_disc"}, + {"add_primitive_torus", "menu_obj_torus"}, + {"add_primitive_text", "menu_obj_text"}, + {"add_primitive_svg", "menu_obj_svg"}, + // Plate + {"plate_add", "toolbar_add_plate"}, + {"plate_duplicate", "menu_copy"}, + {"plate_delete", "menu_delete"}, + {"plate_rename", "plate_name_edit"}, + {"plate_toggle_lock", "lock_normal"}, + {"plate_goto", "go_next_plate"}, + // Printer / Presets + {"sync_ams", "ams_fila_sync"}, + {"sync_presets", "printer_sync_ok"}, + {"preset_bundle", "menu_edit_preset"}, + // Import + {"import_file", "menu_import"}, + {"import_zip_archive", "menu_import"}, + {"import_configs", "menu_import"}, + // Help + {"help_open_config_folder", "folder-closed"}, + {"help_tip_of_the_day", "info"}, + {"help_check_updates", "ams_refresh_normal"}, + {"help_about", "OrcaSlicer_about"}, + {"open_wiki", "link_wiki_img"}, + }; + return icons; +} + std::vector<NativeCommand> build_command_catalog() { std::vector<NativeCommand> out; auto add = [&](std::string key, std::string title, std::string group, std::function<AppActionRunResult(const std::string&)> runner, std::string input = {}) { - out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(runner)}); + std::string icon; + if (auto it = command_icons().find(key); it != command_icons().end()) + icon = it->second; + out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(icon), std::move(runner)}); }; // ---- Slice & Export ---- diff --git a/src/slic3r/GUI/NativeCommands.hpp b/src/slic3r/GUI/NativeCommands.hpp index d586678be1..52143ed506 100644 --- a/src/slic3r/GUI/NativeCommands.hpp +++ b/src/slic3r/GUI/NativeCommands.hpp @@ -10,13 +10,15 @@ namespace Slic3r { namespace GUI { // A built-in speed-dial command: identity + how to run it. The registry keeps commands as thin // values (CommandAction) and routes run() here, so this catalog is the single source of truth for -// the behaviour (runner => an owner method) and the presentation (title/group/input). +// the behaviour (runner => an owner method), the presentation (title/group/input), and the tile +// pictogram (icon = an SVG base name under resources/images, "" for no icon). struct NativeCommand { std::string key; std::string title; std::string group; std::string input; // "percent"/"tab" or "" for immediate run + std::string icon; // SVG base name, or "" to render a blank tile std::function<AppActionRunResult(const std::string& param)> runner; }; diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index 5d10e230e9..a9eaffbc8d 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -201,6 +201,7 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* Slic3r::GUI::wxGetApp().UpdateDarkUI(btn); m_pageButtons.insert(m_pageButtons.begin() + n, btn); m_pageLabels.insert(m_pageLabels.begin() + n, text); // ORCA + m_pageIcons.insert(m_pageIcons.begin() + n, bmp_name); m_buttons_sizer->Insert(n, new wxSizerItem(btn)); m_buttons_sizer->SetCols(m_buttons_sizer->GetCols() + 1); m_sizer->Layout(); @@ -220,6 +221,7 @@ void ButtonsListCtrl::RemovePage(size_t n) Button* btn = m_pageButtons[n]; m_pageButtons.erase(m_pageButtons.begin() + n); m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA + m_pageIcons.erase(m_pageIcons.begin() + n); m_buttons_sizer->Remove(n); #if __WXOSX__ RemoveChild(btn); diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp index 4734122b18..6e166a337c 100644 --- a/src/slic3r/GUI/Notebook.hpp +++ b/src/slic3r/GUI/Notebook.hpp @@ -33,6 +33,8 @@ public: void SetPageText(size_t n, const wxString& strText); void SetCompact(size_t n, bool compact); // ORCA wxString GetPageText(size_t n) const; + // Resource name the page was inserted with (empty for plugin pages, which pass a wxBitmap). + const std::string& GetPageIcon(size_t n) const { return m_pageIcons[n]; } wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA // ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g. // an overflow indicator. Pass nullptr to remove it; ownership stays with the caller. @@ -47,6 +49,7 @@ private: int m_btn_margin; int m_line_margin; std::vector<wxString> m_pageLabels; // ORCA + std::vector<std::string> m_pageIcons; // ORCA: resource icon name per page, plugin pages empty wxWindow* m_overflow_button{nullptr}; // ORCA }; @@ -241,6 +244,13 @@ public: return GetBtnsListCtrl()->GetPageText(n); } + // Resource icon name the page was inserted with; empty for pages added with a wxBitmap. + std::string GetPageIcon(size_t n) const + { + wxCHECK_MSG(n < GetPageCount(), std::string(), wxS("Invalid page")); + return GetBtnsListCtrl()->GetPageIcon(n); + } + virtual bool SetPageImage(size_t WXUNUSED(n), int WXUNUSED(imageId)) override { return false; diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 25c13c4b8d..af8936a75a 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -650,7 +650,7 @@ Option ConfigOptionsGroup::get_option(const std::string& opt_key, int opt_index m_opt_map.emplace(opt_id, pair); if (m_use_custom_ctrl) // fill group and category values just for options from Settings Tab - wxGetApp().sidebar().get_searcher().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category()); + wxGetApp().sidebar().get_searcher().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category(), this->icon); return Option(*m_config->def()->get(opt_key), opt_id); } diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index 77b80208fc..aece404515 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -100,7 +100,7 @@ void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type ty if (!label.IsEmpty()) dst.emplace_back(Option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(), gc.group.ToStdWstring(), - _(gc.group).ToStdWstring(), gc.category.ToStdWstring(), GUI::Tab::translate_category(gc.category, type).ToStdWstring(), + _(gc.group).ToStdWstring(), into_u8(gc.icon), gc.category.ToStdWstring(), GUI::Tab::translate_category(gc.category, type).ToStdWstring(), false, opt_mode}); }; @@ -394,6 +394,7 @@ static Option create_option(const std::string &opt_key, const wxString &label, P (_(label) + suffix_local).ToStdWstring(), gc.group.ToStdWstring(), _(gc.group).ToStdWstring(), + into_u8(gc.icon), gc.category.ToStdWstring(), GUI::Tab::translate_category(category, type).ToStdWstring()}; } @@ -447,9 +448,9 @@ void OptionsSearcher::dlg_msw_rescale() if (search_dialog) search_dialog->msw_rescale(); } -void OptionsSearcher::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category) +void OptionsSearcher::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, const wxString &icon) { - groups_and_categories[get_key(opt_key, type)] = GroupAndCategory{group, category}; + groups_and_categories[get_key(opt_key, type)] = GroupAndCategory{group, category, icon}; } //------------------------------------------ // SearchItem diff --git a/src/slic3r/GUI/Search.hpp b/src/slic3r/GUI/Search.hpp index e28ada2675..b32f2b11ca 100644 --- a/src/slic3r/GUI/Search.hpp +++ b/src/slic3r/GUI/Search.hpp @@ -45,6 +45,7 @@ struct GroupAndCategory { wxString group; wxString category; + wxString icon; // icon of the group's own header, or empty }; struct Option @@ -60,6 +61,7 @@ struct Option std::wstring label_local; std::wstring group; std::wstring group_local; + std::string group_icon; // SVG base name of the group's own header icon, or empty std::wstring category; std::wstring category_local; bool multi_category { false }; @@ -128,7 +130,8 @@ public: bool search(); bool search(const std::string &search, bool force = false, Preset::Type type = Preset::TYPE_INVALID); - void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category); + void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, + const wxString &icon = wxEmptyString); size_t size() const { return found_size(); } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 2dedb23365..d404283c70 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5050,7 +5050,7 @@ void TabPrinter::build_fff() // Register by hand so the UnsavedChanges dialog can render a row for it. wxGetApp().sidebar().get_searcher().add_key("printer_agent", m_type, optgroup->title, - optgroup->config_category()); + optgroup->config_category(), optgroup->icon); } } @@ -5765,7 +5765,7 @@ if (is_marlin_flavor) for (auto &group : m_pages[n_before_extruders]->m_optgroups) { group->set_config_category_and_type(first_extruder_title, m_type); for (auto &opt : group->opt_map()) - searcher.add_key(opt.first + "#0", m_type, group->title, first_extruder_title); + searcher.add_key(opt.first + "#0", m_type, group->title, first_extruder_title, group->icon); } Thaw(); @@ -7869,8 +7869,8 @@ wxSizer* TabPrinter::create_bed_shape_widget(wxWindow* parent) { Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher(); const Search::GroupAndCategory& gc = searcher.get_group_and_category("printable_area"); - searcher.add_key("bed_custom_texture", m_type, gc.group, gc.category); - searcher.add_key("bed_custom_model", m_type, gc.group, gc.category); + searcher.add_key("bed_custom_texture", m_type, gc.group, gc.category, gc.icon); + searcher.add_key("bed_custom_model", m_type, gc.group, gc.category, gc.icon); } return sizer; diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index 0dde4eb382..8c5e074a2e 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -3,6 +3,8 @@ #include "slic3r/GUI/ActionRegistry.hpp" #include "slic3r/GUI/NativeCommands.hpp" +#include <boost/filesystem.hpp> + #include <memory> #include <set> #include <string> @@ -113,6 +115,47 @@ TEST_CASE("Native command catalog has unique keys and present titles", "[speeddi } } +// Every command's tile pictogram is the SVG the matching GUI control already uses; an absent icon +// means a blank tile (like the tab picker). Guard representative names and that every non-empty +// value resolves to a shipped file, so a rename/typo cannot leave broken images in the palette. +TEST_CASE("Native command icons resolve to shipped SVGs", "[speeddial][actions]") +{ + const std::vector<Slic3r::GUI::NativeCommand>& commands = Slic3r::GUI::NativeCommands::catalog(); + auto icon_of = [&commands](const std::string& key) -> const std::string* { + for (const auto& c : commands) + if (c.key == key) + return &c.icon; + return nullptr; + }; + + struct Expected + { + const char* key; + const char* icon; + }; + for (const Expected& e : {Expected{"load_project", "menu_open"}, + Expected{"save_project", "menu_save"}, + Expected{"sync_ams", "ams_fila_sync"}, + Expected{"mode_simple", "advanced"}, + Expected{"calib_temperature", "calib_sf"}, + Expected{"plate_add", "toolbar_add_plate"}, + Expected{"add_primitive_cube", "menu_obj_cube"}, + Expected{"go_to_tab", ""}}) { + const std::string* icon = icon_of(e.key); + INFO(e.key); + REQUIRE(icon != nullptr); + CHECK(*icon == e.icon); + } + + const boost::filesystem::path images = boost::filesystem::path(PROFILES_DIR).parent_path() / "images"; + for (const auto& c : commands) { + if (c.icon.empty()) + continue; + INFO(c.key << " -> " << c.icon); + CHECK(boost::filesystem::exists(images / (c.icon + ".svg"))); + } +} + // The Help-menu commands, wiki/YouTube links and the developer-mode toggle are part of the palette. // Guard their presence and that they stay grouped with their peers, so a catalog edit cannot drop // or scatter them. Groups are compared to the peer's own group to stay independent of translation. From 7c2991d00c3c9fc6554bf329129cad10170176bc Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Fri, 11 Sep 2026 13:05:39 +0800 Subject: [PATCH 16/29] Update translations. Code dedup and cleanup --- .github/workflows/unit_tests.yml | 6 + localization/i18n/OrcaSlicer.pot | 76 +++-- localization/i18n/ca/OrcaSlicer_ca.po | 116 +++++-- localization/i18n/cs/OrcaSlicer_cs.po | 116 +++++-- localization/i18n/de/OrcaSlicer_de.po | 116 +++++-- localization/i18n/en/OrcaSlicer_en.po | 76 +++-- localization/i18n/es/OrcaSlicer_es.po | 108 +++++-- localization/i18n/eu/OrcaSlicer_eu.po | 114 +++++-- localization/i18n/fr/OrcaSlicer_fr.po | 116 +++++-- localization/i18n/hu/OrcaSlicer_hu.po | 116 +++++-- localization/i18n/it/OrcaSlicer_it.po | 116 +++++-- localization/i18n/ja/OrcaSlicer_ja.po | 116 +++++-- localization/i18n/ko/OrcaSlicer_ko.po | 116 +++++-- localization/i18n/lt/OrcaSlicer_lt.po | 116 +++++-- localization/i18n/nl/OrcaSlicer_nl.po | 116 +++++-- localization/i18n/pl/OrcaSlicer_pl.po | 116 +++++-- localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 110 +++++-- localization/i18n/ru/OrcaSlicer_ru.po | 112 +++++-- localization/i18n/sv/OrcaSlicer_sv.po | 116 +++++-- localization/i18n/th/OrcaSlicer_th.po | 116 +++++-- localization/i18n/tr/OrcaSlicer_tr.po | 116 +++++-- localization/i18n/uk/OrcaSlicer_uk.po | 118 +++++-- localization/i18n/vi/OrcaSlicer_vi.po | 116 +++++-- localization/i18n/zh_CN/OrcaSlicer_zh_CN.po | 116 +++++-- localization/i18n/zh_TW/OrcaSlicer_zh_TW.po | 116 +++++-- resources/web/data/text.js | 30 ++ resources/web/dialog/SpeedDial/index.html | 1 + resources/web/dialog/SpeedDial/speeddial.js | 276 +++++++++-------- .../web/dialog/SpeedDial/speeddial.test.js | 16 +- resources/web/js/fuzzy-search.js | 38 ++- resources/web/js/fuzzy-search.test.js | 11 +- src/slic3r/GUI/ActionRegistry.cpp | 132 +++----- src/slic3r/GUI/ActionRegistry.hpp | 30 +- src/slic3r/GUI/GUI_App.cpp | 47 ++- src/slic3r/GUI/GUI_App.hpp | 5 + src/slic3r/GUI/KBShortcutsDialog.cpp | 4 +- src/slic3r/GUI/MainFrame.cpp | 19 +- src/slic3r/GUI/NativeCommands.cpp | 291 +++++++----------- src/slic3r/GUI/NativeCommands.hpp | 16 +- src/slic3r/GUI/Notebook.cpp | 6 + src/slic3r/GUI/Notebook.hpp | 15 +- src/slic3r/GUI/PluginsDialog.cpp | 27 +- src/slic3r/GUI/Preferences.cpp | 5 + src/slic3r/GUI/Search.cpp | 4 +- src/slic3r/GUI/Search.hpp | 4 - src/slic3r/GUI/SpeedDialDialog.cpp | 15 +- tests/slic3rutils/test_action_source.cpp | 55 +++- 47 files changed, 2559 insertions(+), 1180 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 5f54f6581d..e05a927a03 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -49,6 +49,12 @@ jobs: cmakeVersion: "~4.3.0" # use most recent 4.3.x version useLocalCache: true useCloudCache: true + - name: Run web dialog JS tests + timeout-minutes: 5 + shell: bash + run: | + node resources/web/js/fuzzy-search.test.js + node resources/web/dialog/SpeedDial/speeddial.test.js - name: Unpackage and Run Unit Tests timeout-minutes: 20 shell: bash diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index bbdc0e59be..15768b24ea 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -2293,13 +2293,6 @@ msgstr "" msgid "%s has been removed." msgstr "" - -msgid "Select the language" -msgstr "" - -msgid "Language" -msgstr "" - #, possible-c-format, possible-boost-format msgid "Switching Orca Slicer to language %s failed." msgstr "" @@ -2327,6 +2320,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "" +msgid "Plugins refreshed." +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + msgid "Plugin Terminal" msgstr "" @@ -4322,6 +4328,12 @@ msgid "" "Error message: %1%" msgstr "" +#, possible-boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, possible-boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "" @@ -6129,6 +6141,9 @@ msgstr "" msgid "Preferences" msgstr "" +msgid "Open speed dial..." +msgstr "" + msgctxt "Menu" msgid "Edit" msgstr "" @@ -8292,7 +8307,6 @@ msgstr "" msgid "Language selection" msgstr "" - msgid "Asia-Pacific" msgstr "" @@ -8386,6 +8400,9 @@ msgstr "" msgid "General" msgstr "" +msgid "Language" +msgstr "" + msgid "Metric" msgstr "" @@ -8568,6 +8585,12 @@ msgstr "" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "" +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "" @@ -9232,6 +9255,9 @@ msgstr "" msgid "Detach from parent" msgstr "" +msgid "Save without parent" +msgstr "" + msgid "Unique preset" msgstr "" @@ -10983,6 +11009,16 @@ msgstr "" msgid "Switch table page" msgstr "" +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "" @@ -11138,13 +11174,6 @@ msgstr "" msgid "Switch between Prepare/Preview" msgstr "" -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "" - -msgid "Open actions speed dial" -msgstr "" - msgid "Plater" msgstr "" @@ -11403,7 +11432,7 @@ msgstr "" msgid "Copying of file %1% to %2% failed: %3%" msgstr "" -msgid "Please check any unsaved changes before updating the configuration." +msgid "Downloading new vendor profile(s): " msgstr "" msgid "Configuration package: " @@ -11412,6 +11441,12 @@ msgstr "" msgid " updated to " msgstr "" +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "" + msgid "Open G-code file:" msgstr "" @@ -12754,6 +12789,9 @@ msgstr "" msgid "Concentric" msgstr "" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "" @@ -12823,7 +12861,7 @@ msgid "Top surface fill order" msgstr "" msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" @@ -12832,7 +12870,7 @@ msgid "Bottom surface fill order" msgstr "" msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po index 373eb6e9fd..d121446844 100644 --- a/localization/i18n/ca/OrcaSlicer_ca.po +++ b/localization/i18n/ca/OrcaSlicer_ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: 2025-03-15 10:55+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -2522,13 +2522,6 @@ msgstr "Hi ha una actualització disponible. Obriu el quadre de diàleg del paqu msgid "%s has been removed." msgstr "%s s'ha eliminat." - -msgid "Select the language" -msgstr "Seleccioneu l'idioma" - -msgid "Language" -msgstr "Idioma" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2566,6 +2559,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "No s'ha pogut obrir el quadre de diàleg de Connectors (error desconegut)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "Terminal de connectors" @@ -4687,6 +4693,12 @@ msgstr "" "Error en copiar el codi-G temporal al codi-G de sortida. Potser la targeta SD està bloquejada contra escriptura?\n" "Missatge d'error: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "Error en copiar el codi-G temporal al codi-G de sortida. Hi pot haver un problema amb el dispositiu de destinació, intenteu exportar novament o utilitzeu un dispositiu diferent. El codi-G de sortida malmès és a %1%.tmp." @@ -6599,6 +6611,9 @@ msgstr "Mostrar el contorn al voltant de l'objecte seleccionat a l'escena 3D" msgid "Preferences" msgstr "Preferències" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8922,7 +8937,6 @@ msgstr "Voleu continuar?" msgid "Language selection" msgstr "Selecció d'idiomes" - msgid "Asia-Pacific" msgstr "Àsia-Pacífic" @@ -9028,6 +9042,9 @@ msgstr "Ruta de la Instància Actual: " msgid "General" msgstr "General" +msgid "Language" +msgstr "Idioma" + msgid "Metric" msgstr "Mètric" @@ -9236,6 +9253,12 @@ msgstr "Gestió multidispositiu" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Amb aquesta opció habilitada, podeu enviar una tasca a diversos dispositius alhora i gestionar múltiples dispositius." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Finestra emergent per seleccionar el mode d'agrupació de filaments" @@ -9989,6 +10012,9 @@ msgstr "Copia en aquest perfil tots els valors heretats del perfil pare i elimin msgid "Detach from parent" msgstr "Desvincula del pare" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Perfil únic" @@ -11912,6 +11938,17 @@ msgstr "Mostrar/Amagar quadre de diàleg Configuració de Dispositius 3Dconnexio msgid "Switch table page" msgstr "Canviar de pàgina de taula" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Espai" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Mostrar la llista de dreceres de teclat" @@ -12077,15 +12114,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Canviar entre Preparar/Previsualitzar" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Espai" - -# AI Translated -msgid "Open actions speed dial" -msgstr "Obrir el marcatge ràpid d'accions" - msgid "Plater" msgstr "Plataforma" @@ -12359,8 +12387,8 @@ msgstr "Reparació cancel·lada" msgid "Copying of file %1% to %2% failed: %3%" msgstr "La còpia del fitxer %1% a %2% ha fallat: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Cal comprovar els canvis no desats abans de les actualitzacions de configuració." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Paquet de configuració: " @@ -12368,6 +12396,12 @@ msgstr "Paquet de configuració: " msgid " updated to " msgstr " actualitzat a " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Cal comprovar els canvis no desats abans de les actualitzacions de configuració." + msgid "Open G-code file:" msgstr "Obre el fitxer de Codi-G:" @@ -13991,6 +14025,9 @@ msgstr "Alineat Rectilini" msgid "Concentric" msgstr "Concèntric" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Corba de Hilbert" @@ -14080,29 +14117,21 @@ msgstr "" msgid "Top surface fill order" msgstr "Ordre d'emplenament de la superfície superior" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Direcció en què s'omplen les superfícies superiors quan s'utilitza un patró centrat (Concèntric, Acords d'Arquimedes, Octograma en Espiral).\n" -"Cap a fora comença al centre de la superfície, de manera que l'excés de material es desplaça cap a la vora, on és menys visible. Cap a dins comença a la vora i acaba amb les corbes tancades del centre.\n" -"Per defecte utilitza l'ordenació pel camí més curt, que pot anar en qualsevol direcció." # AI Translated msgid "Bottom surface fill order" msgstr "Ordre d'emplenament de la superfície inferior" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Direcció en què s'omplen les superfícies inferiors quan s'utilitza un patró centrat (Concèntric, Acords d'Arquimedes, Octograma en Espiral).\n" -"Cap a dins comença cada superfície amb les corbes exteriors més amples, cosa que millora l'adherència de la capa inicial en plaques on les corbes tancades del centre poden no adherir-se. Cap a fora comença al centre i desplaça l'excés de material cap a la vora.\n" -"Per defecte utilitza l'ordenació pel camí més curt, que pot anar en qualsevol direcció." msgid "Internal solid infill pattern" msgstr "Patró de farciment sòlid intern" @@ -22204,6 +22233,33 @@ msgstr "" "Evitar la deformació( warping )\n" "Sabíeu que quan imprimiu materials propensos a deformar-se, com ara l'ABS, augmentar adequadament la temperatura del llit pot reduir la probabilitat de deformació?" +#~ msgid "Select the language" +#~ msgstr "Seleccioneu l'idioma" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "Obrir el marcatge ràpid d'accions" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Direcció en què s'omplen les superfícies superiors quan s'utilitza un patró centrat (Concèntric, Acords d'Arquimedes, Octograma en Espiral).\n" +#~ "Cap a fora comença al centre de la superfície, de manera que l'excés de material es desplaça cap a la vora, on és menys visible. Cap a dins comença a la vora i acaba amb les corbes tancades del centre.\n" +#~ "Per defecte utilitza l'ordenació pel camí més curt, que pot anar en qualsevol direcció." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Direcció en què s'omplen les superfícies inferiors quan s'utilitza un patró centrat (Concèntric, Acords d'Arquimedes, Octograma en Espiral).\n" +#~ "Cap a dins comença cada superfície amb les corbes exteriors més amples, cosa que millora l'adherència de la capa inicial en plaques on les corbes tancades del centre poden no adherir-se. Cap a fora comença al centre i desplaça l'excés de material cap a la vora.\n" +#~ "Per defecte utilitza l'ordenació pel camí més curt, que pot anar en qualsevol direcció." + # AI Translated #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "La visualització en directe nativa de Wayland requereix el sink de vídeo GTK de GStreamer. Instal·leu el connector gtksink per a GStreamer i reinicieu l'OrcaSlicer." diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po index b0d64c8005..0fee4eedd1 100644 --- a/localization/i18n/cs/OrcaSlicer_cs.po +++ b/localization/i18n/cs/OrcaSlicer_cs.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: \n" "Last-Translator: Jakub Hencl\n" "Language-Team: \n" @@ -2482,13 +2482,6 @@ msgstr "Je k dispozici aktualizace. Otevřete dialog balíčku předvoleb a prov msgid "%s has been removed." msgstr "%s bylo odstraněno." - -msgid "Select the language" -msgstr "Zvolte jazyk" - -msgid "Language" -msgstr "Jazyk" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2526,6 +2519,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Nepodařilo se otevřít dialog Pluginy (neznámá chyba)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "Terminál pluginu" @@ -4647,6 +4653,12 @@ msgstr "" "Kopírování dočasného G-kódu do výstupního G-kódu selhalo. Možná je SD karta uzamčená pro zápis?\n" "Chybová zpráva: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "Kopírování dočasného G-kódu do výstupního G-kódu selhalo. Může být problém s cílovým zařízením, zkuste prosím exportovat znovu nebo použijte jiné zařízení. Poškozený výstupní G-kód je na %1%.tmp." @@ -6564,6 +6576,9 @@ msgstr "Zobrazit obrys kolem vybraného objektu ve 3D scéně." msgid "Preferences" msgstr "Předvolby" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8880,7 +8895,6 @@ msgstr "Chcete pokračovat?" msgid "Language selection" msgstr "Výběr jazyka" - msgid "Asia-Pacific" msgstr "Asie-Pacifik" @@ -8986,6 +9000,9 @@ msgstr "Cesta k aktuální instanci: " msgid "General" msgstr "Obecné" +msgid "Language" +msgstr "Jazyk" + msgid "Metric" msgstr "Metrika" @@ -9190,6 +9207,12 @@ msgstr "Správa více zařízení" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Pokud je tato volba povolena, můžete odeslat úlohu na více zařízení současně a spravovat více zařízení." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Zobrazit dialog pro výběr režimu seskupení filamentů" @@ -9938,6 +9961,9 @@ msgstr "Zkopíruje do této předvolby všechny hodnoty zděděné z nadřazené msgid "Detach from parent" msgstr "Oddělit od nadřazeného" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Samostatná předvolba" @@ -11899,6 +11925,17 @@ msgstr "Zobrazit/Skrýt dialog nastavení zařízení 3Dconnexion" msgid "Switch table page" msgstr "Přepnout stránku tabulky" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Mezerník" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Zobrazit seznam klávesových zkratek" @@ -12061,15 +12098,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Přepnout mezi Přípravou/Náhledem" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Mezerník" - -# AI Translated -msgid "Open actions speed dial" -msgstr "Otevřít rychlou nabídku akcí" - msgid "Plater" msgstr "Deska" @@ -12343,8 +12371,8 @@ msgstr "Oprava zrušena" msgid "Copying of file %1% to %2% failed: %3%" msgstr "Kopírování souboru %1% do %2% selhalo: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Nejprve je třeba zkontrolovat neuložené změny před aktualizací konfigurace." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Balíček konfigurace: " @@ -12352,6 +12380,12 @@ msgstr "Balíček konfigurace: " msgid " updated to " msgstr " aktualizováno na " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Nejprve je třeba zkontrolovat neuložené změny před aktualizací konfigurace." + msgid "Open G-code file:" msgstr "Otevřít G-code soubor:" @@ -13956,6 +13990,9 @@ msgstr "Zarovnaná pravoúhlá" msgid "Concentric" msgstr "Koncentrický" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Hilbertova křivka" @@ -14045,29 +14082,21 @@ msgstr "" msgid "Top surface fill order" msgstr "Pořadí vyplňování horního povrchu" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Směr, ve kterém jsou horní povrchy vyplňovány při použití vzoru vycházejícího ze středu (Koncentrický, Archimédovy akordy, Oktagramová spirála).\n" -"Ven začíná ve středu povrchu, takže je přebytečný materiál vytlačen k okraji, kde je nejméně viditelný. Dovnitř začíná u okraje a končí těsnými oblouky ve středu.\n" -"Výchozí používá řazení podle nejkratší dráhy, které může probíhat v obou směrech." # AI Translated msgid "Bottom surface fill order" msgstr "Pořadí vyplňování spodního povrchu" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Směr, ve kterém jsou spodní povrchy vyplňovány při použití vzoru vycházejícího ze středu (Koncentrický, Archimédovy akordy, Oktagramová spirála).\n" -"Dovnitř začíná každý povrch širšími vnějšími oblouky, což zlepšuje přilnavost první vrstvy na podložkách, kde se těsné oblouky ve středu nemusí přichytit. Ven začíná ve středu a přebytečný materiál vytlačuje k okraji.\n" -"Výchozí používá řazení podle nejkratší dráhy, které může probíhat v obou směrech." msgid "Internal solid infill pattern" msgstr "Vzor vnitřní plné výplně" @@ -22185,6 +22214,33 @@ msgstr "" "Zamezte kroucení\n" "Víte, že při tisku materiálů náchylných ke kroucení, jako je ABS, může vhodné zvýšení teploty vyhřívané desky snížit pravděpodobnost kroucení?" +#~ msgid "Select the language" +#~ msgstr "Zvolte jazyk" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "Otevřít rychlou nabídku akcí" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Směr, ve kterém jsou horní povrchy vyplňovány při použití vzoru vycházejícího ze středu (Koncentrický, Archimédovy akordy, Oktagramová spirála).\n" +#~ "Ven začíná ve středu povrchu, takže je přebytečný materiál vytlačen k okraji, kde je nejméně viditelný. Dovnitř začíná u okraje a končí těsnými oblouky ve středu.\n" +#~ "Výchozí používá řazení podle nejkratší dráhy, které může probíhat v obou směrech." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Směr, ve kterém jsou spodní povrchy vyplňovány při použití vzoru vycházejícího ze středu (Koncentrický, Archimédovy akordy, Oktagramová spirála).\n" +#~ "Dovnitř začíná každý povrch širšími vnějšími oblouky, což zlepšuje přilnavost první vrstvy na podložkách, kde se těsné oblouky ve středu nemusí přichytit. Ven začíná ve středu a přebytečný materiál vytlačuje k okraji.\n" +#~ "Výchozí používá řazení podle nejkratší dráhy, které může probíhat v obou směrech." + # AI Translated #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "Nativní živý náhled ve Waylandu vyžaduje video sink GStreamer GTK. Nainstalujte prosím plugin gtksink pro GStreamer a poté restartujte OrcaSlicer." diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index bd25405cc3..8b61d9e08d 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: \n" "Last-Translator: Heiko Liebscher <hliebschergmail.com>\n" "Language-Team: \n" @@ -2430,13 +2430,6 @@ msgstr "Es ist ein Update verfügbar. Öffnen Sie den Profilbündel-Dialog, um e msgid "%s has been removed." msgstr "%s wurde entfernt." - -msgid "Select the language" -msgstr "Sprache wählen" - -msgid "Language" -msgstr "Sprache" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2474,6 +2467,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Öffnen des Plugins-Dialogs fehlgeschlagen (unbekannter Fehler)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "Plugin-Terminal" @@ -4554,6 +4560,12 @@ msgstr "" "Das Kopieren des temporären G-Codes in den Ausgabe-G-Code ist fehlgeschlagen. Ist die SD-Karte schreibgeschützt?\n" "Fehlermeldung: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "Das Kopieren des temporären G-Codes in den Ausgabe-G-Code ist fehlgeschlagen. Es könnte ein Problem mit dem Zielgerät geben. Versuchen Sie es erneut oder verwenden Sie ein anderes Gerät. Der beschädigte Ausgabe-G-Code befindet sich in %1%.tmp." @@ -6452,6 +6464,9 @@ msgstr "Kontur um das ausgewählte Objekt in der 3D-Szene anzeigen." msgid "Preferences" msgstr "Einstellungen" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8752,7 +8767,6 @@ msgstr "Möchten Sie fortfahren?" msgid "Language selection" msgstr "Sprachauswahl" - msgid "Asia-Pacific" msgstr "Asien-Pazifik" @@ -8857,6 +8871,9 @@ msgstr "Aktueller Instanzpfad: " msgid "General" msgstr "Allgemein" +msgid "Language" +msgstr "Sprache" + msgid "Metric" msgstr "Metrisch" @@ -9061,6 +9078,12 @@ msgstr "Multi-Geräte-Verwaltung" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Wenn diese Option aktiviert ist, können Sie eine Aufgabe gleichzeitig an mehrere Geräte senden und mehrere Geräte verwalten." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Popup zum Auswählen des Filament-Gruppierungsmodus" @@ -9771,6 +9794,9 @@ msgstr "Kopiert alle vom übergeordneten Profil geerbten Werte in dieses Profil msgid "Detach from parent" msgstr "Vom übergeordneten Element trennen" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Eigenständiges Profil" @@ -11655,6 +11681,17 @@ msgstr "Dialogfeld für 3Dconnexion Geräteeinstellungen anzeigen/ausblenden" msgid "Switch table page" msgstr "Seitenwechsel" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Leertaste" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Liste der Tastaturkürzel anzeigen" @@ -11815,15 +11852,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Zwischen Vorbereiten/ Vorschau wechseln" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Leertaste" - -# AI Translated -msgid "Open actions speed dial" -msgstr "Aktions-Schnellwahl öffnen" - msgid "Plater" msgstr "Druckplatte" @@ -12090,8 +12118,8 @@ msgstr "Reparatur abgebrochen" msgid "Copying of file %1% to %2% failed: %3%" msgstr "Kopieren der Datei %1% nach %2% fehlgeschlagen: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Vor der Aktualisierung der Konfiguration müssen die nicht gespeicherten Änderungen überprüft werden." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Konfigurationspaket:" @@ -12099,6 +12127,12 @@ msgstr "Konfigurationspaket:" msgid " updated to " msgstr " aktualisiert auf " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Vor der Aktualisierung der Konfiguration müssen die nicht gespeicherten Änderungen überprüft werden." + msgid "Open G-code file:" msgstr "Öffne G-Code-Datei:" @@ -13665,6 +13699,9 @@ msgstr "Geradlinig ausgerichtet" msgid "Concentric" msgstr "Konzentrisch" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Hilbert-Kurve" @@ -13754,29 +13791,21 @@ msgstr "" msgid "Top surface fill order" msgstr "Füllreihenfolge der oberen Oberfläche" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Richtung, in der obere Oberflächen gefüllt werden, wenn ein mittenbasiertes Muster (Konzentrisch, Archimedische Akkorde, Oktagramm Spirale) verwendet wird.\n" -"Nach außen beginnt in der Mitte der Oberfläche, sodass überschüssiges Material zum Rand geschoben wird, wo es am wenigsten sichtbar ist. Nach innen beginnt am Rand und endet mit den engen Kurven in der Mitte.\n" -"Standard verwendet die Sortierung nach kürzestem Pfad, die in beide Richtungen verlaufen kann." # AI Translated msgid "Bottom surface fill order" msgstr "Füllreihenfolge der unteren Oberfläche" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Richtung, in der untere Oberflächen gefüllt werden, wenn ein mittenbasiertes Muster (Konzentrisch, Archimedische Akkorde, Oktagramm Spirale) verwendet wird.\n" -"Nach innen beginnt jede Oberfläche mit den breiteren äußeren Kurven, was die Haftung der ersten Schicht auf Druckbetten verbessert, auf denen die engen Kurven in der Mitte möglicherweise nicht haften. Nach außen beginnt in der Mitte und schiebt überschüssiges Material zum Rand.\n" -"Standard verwendet die Sortierung nach kürzestem Pfad, die in beide Richtungen verlaufen kann." msgid "Internal solid infill pattern" msgstr "Muster für das interne feste Füllmuster" @@ -21605,6 +21634,33 @@ msgstr "" "Verwerfungen vermeiden\n" "Wussten Sie, dass beim Drucken von Materialien, die zu Verwerfungen neigen, wie z.B. ABS, durch eine entsprechende Erhöhung der Heizbetttemperatur die Wahrscheinlichkeit von Verwerfungen verringert werden kann?" +#~ msgid "Select the language" +#~ msgstr "Sprache wählen" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "Aktions-Schnellwahl öffnen" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Richtung, in der obere Oberflächen gefüllt werden, wenn ein mittenbasiertes Muster (Konzentrisch, Archimedische Akkorde, Oktagramm Spirale) verwendet wird.\n" +#~ "Nach außen beginnt in der Mitte der Oberfläche, sodass überschüssiges Material zum Rand geschoben wird, wo es am wenigsten sichtbar ist. Nach innen beginnt am Rand und endet mit den engen Kurven in der Mitte.\n" +#~ "Standard verwendet die Sortierung nach kürzestem Pfad, die in beide Richtungen verlaufen kann." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Richtung, in der untere Oberflächen gefüllt werden, wenn ein mittenbasiertes Muster (Konzentrisch, Archimedische Akkorde, Oktagramm Spirale) verwendet wird.\n" +#~ "Nach innen beginnt jede Oberfläche mit den breiteren äußeren Kurven, was die Haftung der ersten Schicht auf Druckbetten verbessert, auf denen die engen Kurven in der Mitte möglicherweise nicht haften. Nach außen beginnt in der Mitte und schiebt überschüssiges Material zum Rand.\n" +#~ "Standard verwendet die Sortierung nach kürzestem Pfad, die in beide Richtungen verlaufen kann." + #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "Native Wayland Liveview erfordert das GStreamer GTK Video Sink. Bitte installieren Sie das gtksink-Plugin für GStreamer und starten Sie OrcaSlicer neu." diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po index 35c7f8a87a..0cdadcb5d3 100644 --- a/localization/i18n/en/OrcaSlicer_en.po +++ b/localization/i18n/en/OrcaSlicer_en.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: \n" @@ -2289,13 +2289,6 @@ msgstr "" msgid "%s has been removed." msgstr "" - -msgid "Select the language" -msgstr "" - -msgid "Language" -msgstr "" - #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." msgstr "" @@ -2323,6 +2316,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "" +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + msgid "Plugin Terminal" msgstr "" @@ -4318,6 +4324,12 @@ msgid "" "Error message: %1%" msgstr "" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "" @@ -6125,6 +6137,9 @@ msgstr "" msgid "Preferences" msgstr "" +msgid "Open speed dial..." +msgstr "" + msgctxt "Menu" msgid "Edit" msgstr "" @@ -8288,7 +8303,6 @@ msgstr "" msgid "Language selection" msgstr "" - msgid "Asia-Pacific" msgstr "" @@ -8382,6 +8396,9 @@ msgstr "" msgid "General" msgstr "" +msgid "Language" +msgstr "" + msgid "Metric" msgstr "" @@ -8564,6 +8581,12 @@ msgstr "" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "" +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "" @@ -9228,6 +9251,9 @@ msgstr "" msgid "Detach from parent" msgstr "" +msgid "Save without parent" +msgstr "" + msgid "Unique preset" msgstr "" @@ -10979,6 +11005,16 @@ msgstr "" msgid "Switch table page" msgstr "" +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "" @@ -11134,13 +11170,6 @@ msgstr "" msgid "Switch between Prepare/Preview" msgstr "" -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "" - -msgid "Open actions speed dial" -msgstr "" - msgid "Plater" msgstr "" @@ -11399,7 +11428,7 @@ msgstr "" msgid "Copying of file %1% to %2% failed: %3%" msgstr "" -msgid "Please check any unsaved changes before updating the configuration." +msgid "Downloading new vendor profile(s): " msgstr "" msgid "Configuration package: " @@ -11408,6 +11437,12 @@ msgstr "" msgid " updated to " msgstr "" +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "" + msgid "Open G-code file:" msgstr "" @@ -12750,6 +12785,9 @@ msgstr "" msgid "Concentric" msgstr "" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "" @@ -12819,7 +12857,7 @@ msgid "Top surface fill order" msgstr "" msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" @@ -12828,7 +12866,7 @@ msgid "Bottom surface fill order" msgstr "" msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po index efe4c7dbcd..9ea245a1bd 100644 --- a/localization/i18n/es/OrcaSlicer_es.po +++ b/localization/i18n/es/OrcaSlicer_es.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: \n" "Last-Translator: Ian A. Bassi <>\n" "Language-Team: \n" @@ -2356,13 +2356,6 @@ msgstr "Hay una actualización disponible. Abra el cuadro de diálogo del paquet msgid "%s has been removed." msgstr "Se ha eliminado %s." - -msgid "Select the language" -msgstr "Seleccionar el idioma" - -msgid "Language" -msgstr "Idioma" - #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." msgstr "No se pudo cambiar Orca Slicer al idioma %s." @@ -2394,6 +2387,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "No se pudo abrir el cuadro de diálogo de plugins (error desconocido)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + msgid "Plugin Terminal" msgstr "Terminal de plugins" @@ -4424,6 +4430,12 @@ msgstr "" "Error al copiar el G-Code temporal en el G-Code de salida. ¿Quizás la tarjeta SD está protegida contra escritura?\n" "Mensaje de error: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "La copia del G-Code temporal al G-Code de salida ha fallado. Puede haber un problema con el dispositivo de destino, intenta exportar nuevamente o usa un dispositivo diferente. El G-Code de salida dañado está en %1%.tmp." @@ -6307,6 +6319,9 @@ msgstr "Mostrar el contorno alrededor del objeto seleccionado en la escena 3D." msgid "Preferences" msgstr "Preferencias" +msgid "Open speed dial..." +msgstr "" + msgctxt "Menu" msgid "Edit" msgstr "Edición" @@ -8527,7 +8542,6 @@ msgstr "¿Quieres continuar?" msgid "Language selection" msgstr "Selección de idiomas" - msgid "Asia-Pacific" msgstr "Asia-Pacífico" @@ -8632,6 +8646,9 @@ msgstr "Ruta de Instancia Actual: " msgid "General" msgstr "General" +msgid "Language" +msgstr "Idioma" + msgid "Metric" msgstr "Métrico" @@ -8828,6 +8845,12 @@ msgstr "Gestión multidispositivo" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Con esta opción activada, puede enviar una tarea a varios dispositivos al mismo tiempo y gestionar varios dispositivos." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Ventana emergente para seleccionar el modo de agrupación de filamentos" @@ -9528,6 +9551,9 @@ msgstr "Copia en este perfil todos los valores heredados del perfil padre y elim msgid "Detach from parent" msgstr "Separar del elemento padre" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Perfil único" @@ -11357,6 +11383,16 @@ msgstr "Mostrar/Ocultar el diálogo de ajustes de los dispositivos 3Dconnexion" msgid "Switch table page" msgstr "Cambiar de página de tabla" +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Espacio" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Muestra lista de atajos de teclado" @@ -11512,13 +11548,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Cambiar entre Preparar/Previsualizar" -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Espacio" - -msgid "Open actions speed dial" -msgstr "Abrir el menú rápido de acciones" - msgid "Plater" msgstr "Cama" @@ -11779,8 +11808,8 @@ msgstr "Reparación cancelada" msgid "Copying of file %1% to %2% failed: %3%" msgstr "La copia del archivo %1% a %2% falló: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Es necesario comprobar los cambios no guardados antes de actualizar la configuración." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Paquete de configuración: " @@ -11788,6 +11817,12 @@ msgstr "Paquete de configuración: " msgid " updated to " msgstr " Actualizado a " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Es necesario comprobar los cambios no guardados antes de actualizar la configuración." + msgid "Open G-code file:" msgstr "Abrir archivo G-Code:" @@ -13344,6 +13379,9 @@ msgstr "Rectilíneo Alineado" msgid "Concentric" msgstr "Concéntrico" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Curva de Hilbert" @@ -13424,25 +13462,19 @@ msgid "Top surface fill order" msgstr "Orden de relleno de la superficie superior" msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Dirección en la que se rellenan las superficies superiores al usar un patrón basado en el centro (Concéntrico, Cuerdas de Arquímedes, Espiral de octograma).\n" -"Hacia afuera comienza en el centro de la superficie, de modo que cualquier exceso de material se empuja hacia el borde, donde es menos visible. Hacia adentro comienza en el borde y termina con las curvas cerradas del centro.\n" -"Por defecto usa el orden de ruta más corta, que puede ir en cualquier dirección." msgid "Bottom surface fill order" msgstr "Orden de relleno de la superficie inferior" msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Dirección en la que se rellenan las superficies inferiores al usar un patrón basado en el centro (Concéntrico, Cuerdas de Arquímedes, Espiral de octograma).\n" -"Hacia adentro comienza cada superficie con las curvas exteriores más amplias, lo que mejora la adherencia de la primera capa en las camas donde las curvas cerradas del centro pueden no adherirse. Hacia afuera comienza en el centro, empujando cualquier exceso de material hacia el borde.\n" -"Por defecto usa el orden de ruta más corta, que puede ir en cualquier dirección." msgid "Internal solid infill pattern" msgstr "Patrón de relleno sólido interno" @@ -21148,6 +21180,30 @@ msgstr "" "Evita la deformación\n" "¿Sabías que al imprimir materiales propensos a la deformación como el ABS, aumentar adecuadamente la temperatura de la cama térmica puede reducir la probabilidad de deformaciones?" +#~ msgid "Select the language" +#~ msgstr "Seleccionar el idioma" + +#~ msgid "Open actions speed dial" +#~ msgstr "Abrir el menú rápido de acciones" + +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Dirección en la que se rellenan las superficies superiores al usar un patrón basado en el centro (Concéntrico, Cuerdas de Arquímedes, Espiral de octograma).\n" +#~ "Hacia afuera comienza en el centro de la superficie, de modo que cualquier exceso de material se empuja hacia el borde, donde es menos visible. Hacia adentro comienza en el borde y termina con las curvas cerradas del centro.\n" +#~ "Por defecto usa el orden de ruta más corta, que puede ir en cualquier dirección." + +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Dirección en la que se rellenan las superficies inferiores al usar un patrón basado en el centro (Concéntrico, Cuerdas de Arquímedes, Espiral de octograma).\n" +#~ "Hacia adentro comienza cada superficie con las curvas exteriores más amplias, lo que mejora la adherencia de la primera capa en las camas donde las curvas cerradas del centro pueden no adherirse. Hacia afuera comienza en el centro, empujando cualquier exceso de material hacia el borde.\n" +#~ "Por defecto usa el orden de ruta más corta, que puede ir en cualquier dirección." + #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "La función de visualización en directo nativa de Wayland requiere el receptor de vídeo GTK de GStreamer. Instale el plugin gtksink para GStreamer y, a continuación, reinicie OrcaSlicer." diff --git a/localization/i18n/eu/OrcaSlicer_eu.po b/localization/i18n/eu/OrcaSlicer_eu.po index 698941018e..558c1a80dc 100644 --- a/localization/i18n/eu/OrcaSlicer_eu.po +++ b/localization/i18n/eu/OrcaSlicer_eu.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: 2026-07-20 13:33+0200\n" "Last-Translator: Manu Goiogana <mgoiogana@gmail.com>\n" "Language-Team: \n" @@ -2390,13 +2390,6 @@ msgstr "Eguneratze bat dago erabilgarri. Ireki aurrezarpen-paketeen elkarrizketa msgid "%s has been removed." msgstr "%s kendu da." - -msgid "Select the language" -msgstr "Hautatu hizkuntza" - -msgid "Language" -msgstr "Hizkuntza" - #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." msgstr "Ezin izan da OrcaSlicer %s hizkuntzara aldatu." @@ -2428,6 +2421,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Ezin izan da pluginen elkarrizketa-koadroa ireki (errore ezezaguna)" +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + msgid "Plugin Terminal" msgstr "Plugin-terminala" @@ -4470,6 +4476,12 @@ msgstr "" "Aldi baterako G-code-a irteerako G-code-an kopiatzeak huts egin du. Agian SD txartela idazteko blokeatuta dago?\n" "Errore- mezua: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "Aldi baterako G-code-a irteerako G-code-an kopiatzeak huts egin du. Arazoak egon daitezke helburuko gailuarekin, saiatu berriro esportatzen edo beste gailu bat erabiltzen. Hondatutako irteerako G-code-a hemen da: %1%.tmp." @@ -6353,6 +6365,9 @@ msgstr "Erakutsi hautatutako objektuaren inguruko ingerada 3D eszenan." msgid "Preferences" msgstr "Hobespenak" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8608,7 +8623,6 @@ msgstr "Jarraitu nahi duzu?" msgid "Language selection" msgstr "Hizkuntza-hautaketa" - msgid "Asia-Pacific" msgstr "Asia-Pazifikoa" @@ -8713,6 +8727,9 @@ msgstr "Uneko instantziaren bide-izena: " msgid "General" msgstr "Orokorra" +msgid "Language" +msgstr "Hizkuntza" + msgid "Metric" msgstr "Metrikoa" @@ -8909,6 +8926,12 @@ msgstr "Gailu anitzen kudeaketa" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Aukera hau gaituta, zeregin bat hainbat gailutara bidali eta hainbat gailu kudea ditzakezu aldi berean." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Erakutsi filamentuak taldekatzeko modua hautatzeko leihoa" @@ -9614,6 +9637,9 @@ msgstr "Aurrezarpen honetara gurasoaren balio heredatu guztiak kopiatzen ditu et msgid "Detach from parent" msgstr "Bereizi gurasotik" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Aurrezarpen bakarra" @@ -11467,6 +11493,17 @@ msgstr "Erakutsi/Ezkutatu 3Dconnexion gailuen ezarpenen elkarrizketa-koadroa" msgid "Switch table page" msgstr "Aldatu taula-orria" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Zuriunea" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Erakutsi teklatu-lasterbideen zerrenda" @@ -11627,14 +11664,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Aldatu Prestatu/Aurrebista artean" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Zuriunea" - -msgid "Open actions speed dial" -msgstr "Ekintzen markatze azkarra ireki" - msgid "Plater" msgstr "Plaka-antolatzailea" @@ -11899,8 +11928,8 @@ msgstr "Konponketa bertan behera utzi da" msgid "Copying of file %1% to %2% failed: %3%" msgstr "%1% fitxategia %2% helmugara kopiatzeak huts egin du: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Egiaztatu gorde gabeko aldaketarik dagoen konfigurazioa eguneratu aurretik." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Konfigurazio-paketea: " @@ -11908,6 +11937,12 @@ msgstr "Konfigurazio-paketea: " msgid " updated to " msgstr " hona eguneratu da: " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Egiaztatu gorde gabeko aldaketarik dagoen konfigurazioa eguneratu aurretik." + msgid "Open G-code file:" msgstr "Ireki G-code fitxategia:" @@ -13477,6 +13512,9 @@ msgstr "Lerrozuzen lerrokatua" msgid "Concentric" msgstr "Kontzentrikoa" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Hilbert kurba" @@ -13560,28 +13598,20 @@ msgstr "" msgid "Top surface fill order" msgstr "Goiko gainazala betetzeko ordena" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Goiko gainazalak betetzen diren noranzkoa zentroan oinarritutako patroia erabiltzen denean (Kontzentrikoa, Arkimedesen kordak, Oktagrama-kiribila).\n" -"Kanporanzkoa erdialdean hasten da, beraz, gehiegizko materiala gutxien ikusten den ertzera bultzatzen da. Barruranzkoa ertzean hasten da eta erdian kurba estuekin amaitzen da.\n" -"Lehenetsiak bide laburreneko ordena erabiltzen du, zeina norabide batean zein bestean ibil daitekeen." msgid "Bottom surface fill order" msgstr "Beheko gainazala betetzeko ordena" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Beheko gainazalak betetzen diren noranzkoa zentroan oinarritutako patroia erabiltzen denean (Kontzentrikoa, Arkimedesen kordak, Oktagrama-kiribila).\n" -"Barruranzkoa hasten da gainazal bakoitza kanpoko kurba zabalagoekin, eta horrek lehen geruzaren atxikimendua hobetzen du erdiko kurba estuak itsatsi ez daitezkeen inprimatze-plaketan. Kanporanzkoa erdialdean hasten da, gehiegizko materiala ertzera bultzatuz.\n" -"Lehenetsiak bide laburreneko ordena erabiltzen du, zeina norabide batean zein bestean ibil daitekeen." # AI Translated msgid "Internal solid infill pattern" @@ -21337,6 +21367,32 @@ msgstr "" "Saihestu okertzea\n" "Ba al zenekien ABS bezalako okertzeko joera duten materialak inprimatzean ohe beroaren tenperatura egoki igotzeak okertzeko probabilitatea murriztu dezakeela?" +#~ msgid "Select the language" +#~ msgstr "Hautatu hizkuntza" + +#~ msgid "Open actions speed dial" +#~ msgstr "Ekintzen markatze azkarra ireki" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Goiko gainazalak betetzen diren noranzkoa zentroan oinarritutako patroia erabiltzen denean (Kontzentrikoa, Arkimedesen kordak, Oktagrama-kiribila).\n" +#~ "Kanporanzkoa erdialdean hasten da, beraz, gehiegizko materiala gutxien ikusten den ertzera bultzatzen da. Barruranzkoa ertzean hasten da eta erdian kurba estuekin amaitzen da.\n" +#~ "Lehenetsiak bide laburreneko ordena erabiltzen du, zeina norabide batean zein bestean ibil daitekeen." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Beheko gainazalak betetzen diren noranzkoa zentroan oinarritutako patroia erabiltzen denean (Kontzentrikoa, Arkimedesen kordak, Oktagrama-kiribila).\n" +#~ "Barruranzkoa hasten da gainazal bakoitza kanpoko kurba zabalagoekin, eta horrek lehen geruzaren atxikimendua hobetzen du erdiko kurba estuak itsatsi ez daitezkeen inprimatze-plaketan. Kanporanzkoa erdialdean hasten da, gehiegizko materiala ertzera bultzatuz.\n" +#~ "Lehenetsiak bide laburreneko ordena erabiltzen du, zeina norabide batean zein bestean ibil daitekeen." + #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "Waylanden jatorrizko zuzeneko ikuspegiak GStreamer GTK bideo-hustubidea behar du. Instalatu GStreamerrerako gtksink plugina eta berrabiarazi OrcaSlicer." diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index 6344696234..aaac6d576a 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Guislain Cyril, Thomas Lété\n" @@ -2414,13 +2414,6 @@ msgstr "Une mise à jour est disponible. Ouvrez la boîte de dialogue du paquet msgid "%s has been removed." msgstr "%s a été supprimé." - -msgid "Select the language" -msgstr "Sélectionner la langue" - -msgid "Language" -msgstr "Langue" - #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." msgstr "Le passage d’Orca Slicer à la langue %s a échoué." @@ -2455,6 +2448,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Échec de l'ouverture de la boîte de dialogue des plugins (erreur inconnue)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "Terminal des plugins" @@ -4507,6 +4513,12 @@ msgstr "" "La copie du G-code temporaire vers le G-code de sortie a échoué. La carte SD est peut-être bloquée en écriture ?\n" "Message d’erreur : %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "La copie du G-code temporaire vers le G-code de sortie a échoué. Il se peut qu’il y ait un problème avec le dispositif cible, veuillez essayer d’exporter à nouveau ou d’utiliser un autre périphérique. Le G-code de sortie corrompu se trouve dans %1%.tmp." @@ -6401,6 +6413,9 @@ msgstr "Afficher le tracé contour de l’objet sélectionné dans la scène 3D. msgid "Preferences" msgstr "Préférences" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8676,7 +8691,6 @@ msgstr "Voulez-vous continuer ?" msgid "Language selection" msgstr "Sélection de la langue" - msgid "Asia-Pacific" msgstr "Asie-Pacifique" @@ -8781,6 +8795,9 @@ msgstr "Chemin d’accès à l’instance courante : " msgid "General" msgstr "Général" +msgid "Language" +msgstr "Langue" + msgid "Metric" msgstr "Métrique" @@ -8977,6 +8994,12 @@ msgstr "Gestion multi appareils" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Si cette option est activée, vous pouvez envoyer une tâche à plusieurs appareils en même temps et gérer plusieurs appareils." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Fenêtre contextuelle pour sélectionner le mode de regroupement des filaments" @@ -9685,6 +9708,9 @@ msgstr "Copie dans ce préréglage toutes les valeurs héritées du préréglage msgid "Detach from parent" msgstr "Détacher du parent" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Préréglage unique" @@ -11563,6 +11589,17 @@ msgstr "Afficher/Masquer la boîte de dialogue des paramètres des périphériqu msgid "Switch table page" msgstr "Page du tableau de commutation" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Espace" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Afficher la liste des raccourcis clavier" @@ -11723,15 +11760,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Basculer entre Préparer/Aperçu" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Espace" - -# AI Translated -msgid "Open actions speed dial" -msgstr "Ouvrir le menu d'actions rapides" - msgid "Plater" msgstr "Plateau" @@ -11997,8 +12025,8 @@ msgstr "Réparation annulée" msgid "Copying of file %1% to %2% failed: %3%" msgstr "Échec de la copie du fichier %1% vers %2% : %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Besoin de vérifier les modifications non enregistrées avant les mises à jour de configuration." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Paquet de configuration : " @@ -12006,6 +12034,12 @@ msgstr "Paquet de configuration : " msgid " updated to " msgstr " mis à jour en " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Besoin de vérifier les modifications non enregistrées avant les mises à jour de configuration." + msgid "Open G-code file:" msgstr "Ouvrir un fichier G-code :" @@ -13571,6 +13605,9 @@ msgstr "Rectiligne Aligné" msgid "Concentric" msgstr "Concentrique" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Courbe de Hilbert" @@ -13660,29 +13697,21 @@ msgstr "" msgid "Top surface fill order" msgstr "Ordre de remplissage de la surface supérieure" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Direction dans laquelle les surfaces supérieures sont remplies lors de l'utilisation d'un motif centré (Concentrique, Cordes d'Archimède, Spirale d'octogramme).\n" -"Vers l'extérieur commence au centre de la surface, de sorte que tout excès de matière est poussé vers le bord où il est le moins visible. Vers l'intérieur commence au bord et se termine par les courbes serrées au centre.\n" -"Par défaut utilise un ordonnancement par chemin le plus court, qui peut aller dans les deux sens." # AI Translated msgid "Bottom surface fill order" msgstr "Ordre de remplissage de la surface inférieure" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Direction dans laquelle les surfaces inférieures sont remplies lors de l'utilisation d'un motif centré (Concentrique, Cordes d'Archimède, Spirale d'octogramme).\n" -"Vers l'intérieur commence chaque surface par les courbes extérieures plus larges, ce qui améliore l'adhérence de la première couche sur les plateaux où les courbes serrées au centre peuvent ne pas adhérer. Vers l'extérieur commence au centre, poussant tout excès de matière vers le bord.\n" -"Par défaut utilise un ordonnancement par chemin le plus court, qui peut aller dans les deux sens." msgid "Internal solid infill pattern" msgstr "Motif de remplissage plein interne" @@ -21495,6 +21524,33 @@ msgstr "" "Éviter la déformation\n" "Saviez-vous que lors de l’impression de matériaux susceptibles de se déformer, tels que l’ABS, une augmentation appropriée de la température du plateau chauffant peut réduire la probabilité de déformation?" +#~ msgid "Select the language" +#~ msgstr "Sélectionner la langue" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "Ouvrir le menu d'actions rapides" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Direction dans laquelle les surfaces supérieures sont remplies lors de l'utilisation d'un motif centré (Concentrique, Cordes d'Archimède, Spirale d'octogramme).\n" +#~ "Vers l'extérieur commence au centre de la surface, de sorte que tout excès de matière est poussé vers le bord où il est le moins visible. Vers l'intérieur commence au bord et se termine par les courbes serrées au centre.\n" +#~ "Par défaut utilise un ordonnancement par chemin le plus court, qui peut aller dans les deux sens." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Direction dans laquelle les surfaces inférieures sont remplies lors de l'utilisation d'un motif centré (Concentrique, Cordes d'Archimède, Spirale d'octogramme).\n" +#~ "Vers l'intérieur commence chaque surface par les courbes extérieures plus larges, ce qui améliore l'adhérence de la première couche sur les plateaux où les courbes serrées au centre peuvent ne pas adhérer. Vers l'extérieur commence au centre, poussant tout excès de matière vers le bord.\n" +#~ "Par défaut utilise un ordonnancement par chemin le plus court, qui peut aller dans les deux sens." + #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "L’aperçu en direct natif sous Wayland nécessite le récepteur vidéo GStreamer GTK. Veuillez installer le plugin gtksink pour GStreamer, puis redémarrer OrcaSlicer." diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po index 78c06dd4c0..f92b87a246 100644 --- a/localization/i18n/hu/OrcaSlicer_hu.po +++ b/localization/i18n/hu/OrcaSlicer_hu.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -2460,13 +2460,6 @@ msgstr "Frissítés érhető el. Nyisd meg a beállításcsomag párbeszédablak msgid "%s has been removed." msgstr "%s eltávolítva." - -msgid "Select the language" -msgstr "Válaszd ki a nyelvet" - -msgid "Language" -msgstr "Nyelv" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2504,6 +2497,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Nem sikerült megnyitni a Bővítmények párbeszédablakot (ismeretlen hiba)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "Bővítményterminál" @@ -4601,6 +4607,12 @@ msgstr "" "Nem sikerült az ideiglenes G-kódot a kimeneti G-kódba másolni. Lehet, hogy az SD-kártya írásvédett?\n" "Hibaüzenet: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "Nem sikerült az ideiglenes G-kódot a kimeneti G-kódba másolni. Probléma lehet a céleszközzel. Kérlek, exportáld újra, vagy használj másik eszközt. A sérült kimeneti G-kód helye: %1%.tmp." @@ -6503,6 +6515,9 @@ msgstr "Körvonal megjelenítése a kijelölt objektum körül a 3D nézetben." msgid "Preferences" msgstr "Beállítások" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8804,7 +8819,6 @@ msgstr "Szeretnéd folytatni?" msgid "Language selection" msgstr "Nyelv kiválasztása" - msgid "Asia-Pacific" msgstr "Ázsia-Csendes-óceáni térség" @@ -8909,6 +8923,9 @@ msgstr "Jelenlegi példány útvonala: " msgid "General" msgstr "Általános" +msgid "Language" +msgstr "Nyelv" + msgid "Metric" msgstr "Metrikus" @@ -9113,6 +9130,12 @@ msgstr "Többeszközös kezelés" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Ezzel az opcióval egyszerre több eszközre küldhetsz feladatot és több eszközt kezelhetsz." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Felugró ablak a filamentcsoportosítási mód kiválasztásához" @@ -9845,6 +9868,9 @@ msgstr "Az összes örökölt értéket átmásolja a szülő előbeállításb msgid "Detach from parent" msgstr "Leválasztás a szülőről" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Önálló előbeállítás" @@ -11747,6 +11773,17 @@ msgstr "3Dconnexion-eszközbeállítások párbeszédablak megjelenítése/elrej msgid "Switch table page" msgstr "Váltás táblázatra" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Szóköz" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Gyorsgombok listájának megjelenítése" @@ -11907,15 +11944,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Váltás előkészítés/előnézet között" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Szóköz" - -# AI Translated -msgid "Open actions speed dial" -msgstr "Műveletek gyorsmenüjének megnyitása" - msgid "Plater" msgstr "Tálca" @@ -12187,8 +12215,8 @@ msgstr "Javítás megszakítva" msgid "Copying of file %1% to %2% failed: %3%" msgstr "%1% fájl másolása sikertelen a következő helyre: %2% Hiba: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Kérlek, ellenőrizd a nem mentett módosításokat a konfiguráció frissítése előtt." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Konfigurációs csomag: " @@ -12196,6 +12224,12 @@ msgstr "Konfigurációs csomag: " msgid " updated to " msgstr " frissítve erre: " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Kérlek, ellenőrizd a nem mentett módosításokat a konfiguráció frissítése előtt." + msgid "Open G-code file:" msgstr "G-kód fájl megnyitása:" @@ -13794,6 +13828,9 @@ msgstr "Igazított vonal" msgid "Concentric" msgstr "Koncentrikus" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Hilbert-görbe" @@ -13883,29 +13920,21 @@ msgstr "" msgid "Top surface fill order" msgstr "Felső felület kitöltési sorrendje" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Az az irány, amelyben a felső felületek kitöltése történik középpont alapú mintázat (Koncentrikus, Archimédeszi vonalak, Nyolcágú spirál) használatakor.\n" -"A Kifelé a felület közepén kezd, így a felesleges anyag a szélek felé tolódik, ahol a legkevésbé látszik. A Befelé a szélén kezd, és a középen lévő szűk ívekkel fejeződik be.\n" -"Az Alapértelmezett a legrövidebb útvonal szerinti sorrendet használja, amely bármelyik irányba haladhat." # AI Translated msgid "Bottom surface fill order" msgstr "Alsó felület kitöltési sorrendje" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Az az irány, amelyben az alsó felületek kitöltése történik középpont alapú mintázat (Koncentrikus, Archimédeszi vonalak, Nyolcágú spirál) használatakor.\n" -"A Befelé minden felületet a szélesebb külső ívekkel kezd, ami javítja az első réteg tapadását azokon az asztalokon, ahol a középen lévő szűk ívek nem tapadnak meg jól. A Kifelé a közepén kezd, a felesleges anyagot a szélek felé tolva.\n" -"Az Alapértelmezett a legrövidebb útvonal szerinti sorrendet használja, amely bármelyik irányba haladhat." # AI Translated msgid "Internal solid infill pattern" @@ -21926,6 +21955,33 @@ msgstr "" "Kunkorodás elkerülése\n" "Tudtad, hogy a kunkorodásra hajlamos anyagok (például ABS) nyomtatásakor az asztal hőmérsékletének növelése csökkentheti a kunkorodás valószínűségét?" +#~ msgid "Select the language" +#~ msgstr "Válaszd ki a nyelvet" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "Műveletek gyorsmenüjének megnyitása" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Az az irány, amelyben a felső felületek kitöltése történik középpont alapú mintázat (Koncentrikus, Archimédeszi vonalak, Nyolcágú spirál) használatakor.\n" +#~ "A Kifelé a felület közepén kezd, így a felesleges anyag a szélek felé tolódik, ahol a legkevésbé látszik. A Befelé a szélén kezd, és a középen lévő szűk ívekkel fejeződik be.\n" +#~ "Az Alapértelmezett a legrövidebb útvonal szerinti sorrendet használja, amely bármelyik irányba haladhat." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Az az irány, amelyben az alsó felületek kitöltése történik középpont alapú mintázat (Koncentrikus, Archimédeszi vonalak, Nyolcágú spirál) használatakor.\n" +#~ "A Befelé minden felületet a szélesebb külső ívekkel kezd, ami javítja az első réteg tapadását azokon az asztalokon, ahol a középen lévő szűk ívek nem tapadnak meg jól. A Kifelé a közepén kezd, a felesleges anyagot a szélek felé tolva.\n" +#~ "Az Alapértelmezett a legrövidebb útvonal szerinti sorrendet használja, amely bármelyik irányba haladhat." + #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "A natív Wayland élőképhez a GStreamer GTK videonyelő szükséges. Telepítsd a gtksink beépülő modult a GStreamerhez, majd indítsd újra az OrcaSlicert." diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po index cf5099bca3..814c2b627d 100644 --- a/localization/i18n/it/OrcaSlicer_it.po +++ b/localization/i18n/it/OrcaSlicer_it.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -2466,13 +2466,6 @@ msgstr "È disponibile un aggiornamento. Apri la finestra di dialogo del bundle msgid "%s has been removed." msgstr "%s è stato rimosso." - -msgid "Select the language" -msgstr "Seleziona la lingua" - -msgid "Language" -msgstr "Lingua" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2510,6 +2503,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Impossibile aprire la finestra di dialogo Plugin (errore sconosciuto)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "Terminale plugin" @@ -4603,6 +4609,12 @@ msgstr "" "Copia del G-code temporaneo sul G-code di uscita non riuscita. Forse la scheda SD è protetta da scrittura?\n" "Messaggio di errore: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "Copia del G-code temporaneo nel G-code di uscita non riuscita. Potrebbe esserci un problema nel dispositivo di destinazione. Prova ad esportare di nuovo o usa un dispositivo diverso. Il file G-code corrotto è su %1%.tmp." @@ -6505,6 +6517,9 @@ msgstr "Mostra il contorno attorno all'oggetto selezionato nella scena 3D." msgid "Preferences" msgstr "Preferenze" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8805,7 +8820,6 @@ msgstr "Vuoi continuare?" msgid "Language selection" msgstr "Selezione lingua" - msgid "Asia-Pacific" msgstr "Asia-Pacifico" @@ -8911,6 +8925,9 @@ msgstr "Percorso istanza attuale: " msgid "General" msgstr "Generale" +msgid "Language" +msgstr "Lingua" + msgid "Metric" msgstr "Metrico" @@ -9115,6 +9132,12 @@ msgstr "Gestione multi-dispositivo" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Abilitando questa opzione, puoi inviare un'attività a più dispositivi contemporaneamente e gestire più dispositivi." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Popup per selezionare la modalità di raggruppamento filamenti" @@ -9863,6 +9886,9 @@ msgstr "Copia in questo profilo tutti i valori ereditati dal profilo padre e rim msgid "Detach from parent" msgstr "Scollega dal genitore" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Profilo unico" @@ -11767,6 +11793,17 @@ msgstr "Mostra/nascondi la finestra di dialogo delle impostazioni dei dispositiv msgid "Switch table page" msgstr "Cambia pagina tabella" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Spazio" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Mostra elenco scorciatoie da tastiera" @@ -11928,15 +11965,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Passa tra Prepara e Anteprima" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Spazio" - -# AI Translated -msgid "Open actions speed dial" -msgstr "Apri la selezione rapida delle azioni" - msgid "Plater" msgstr "Piatto" @@ -12208,8 +12236,8 @@ msgstr "Riparazione annullata" msgid "Copying of file %1% to %2% failed: %3%" msgstr "Copia del file %1% su %2% non riuscita: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Controllare le modifiche non salvate prima di aggiornare la configurazione." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Pacchetto di configurazione: " @@ -12217,6 +12245,12 @@ msgstr "Pacchetto di configurazione: " msgid " updated to " msgstr " aggiornato a " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Controllare le modifiche non salvate prima di aggiornare la configurazione." + msgid "Open G-code file:" msgstr "Apri un file G-code:" @@ -13814,6 +13848,9 @@ msgstr "Rettilineo allineato" msgid "Concentric" msgstr "Concentrico" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Curva di Hilbert" @@ -13903,29 +13940,21 @@ msgstr "" msgid "Top surface fill order" msgstr "Ordine di riempimento della superficie superiore" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Direzione in cui vengono riempite le superfici superiori quando si utilizza un motivo basato sul centro (Concentrico, Corde di Archimede, Spirale a ottagramma).\n" -"Verso l'esterno inizia dal centro della superficie, in modo che il materiale in eccesso venga spinto verso il bordo dove è meno visibile. Verso l'interno inizia dal bordo e termina con le curve strette al centro.\n" -"L'impostazione predefinita utilizza l'ordinamento a percorso più breve, che può procedere in entrambe le direzioni." # AI Translated msgid "Bottom surface fill order" msgstr "Ordine di riempimento della superficie inferiore" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Direzione in cui vengono riempite le superfici inferiori quando si utilizza un motivo basato sul centro (Concentrico, Corde di Archimede, Spirale a ottagramma).\n" -"Verso l'interno inizia ogni superficie con le curve esterne più ampie, il che migliora l'adesione del primo strato sui piatti di stampa dove le curve strette al centro potrebbero non aderire. Verso l'esterno inizia dal centro, spingendo il materiale in eccesso verso il bordo.\n" -"L'impostazione predefinita utilizza l'ordinamento a percorso più breve, che può procedere in entrambe le direzioni." msgid "Internal solid infill pattern" msgstr "Motivo riempimento solido interno" @@ -21950,6 +21979,33 @@ msgstr "" "Evita le deformazioni\n" "Sapevi che quando si stampano materiali soggetti a deformazioni come l'ABS, aumentare in modo appropriato la temperatura del piano riscaldato può ridurre la probabilità di deformazione?" +#~ msgid "Select the language" +#~ msgstr "Seleziona la lingua" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "Apri la selezione rapida delle azioni" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Direzione in cui vengono riempite le superfici superiori quando si utilizza un motivo basato sul centro (Concentrico, Corde di Archimede, Spirale a ottagramma).\n" +#~ "Verso l'esterno inizia dal centro della superficie, in modo che il materiale in eccesso venga spinto verso il bordo dove è meno visibile. Verso l'interno inizia dal bordo e termina con le curve strette al centro.\n" +#~ "L'impostazione predefinita utilizza l'ordinamento a percorso più breve, che può procedere in entrambe le direzioni." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Direzione in cui vengono riempite le superfici inferiori quando si utilizza un motivo basato sul centro (Concentrico, Corde di Archimede, Spirale a ottagramma).\n" +#~ "Verso l'interno inizia ogni superficie con le curve esterne più ampie, il che migliora l'adesione del primo strato sui piatti di stampa dove le curve strette al centro potrebbero non aderire. Verso l'esterno inizia dal centro, spingendo il materiale in eccesso verso il bordo.\n" +#~ "L'impostazione predefinita utilizza l'ordinamento a percorso più breve, che può procedere in entrambe le direzioni." + #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "La funzione di visualizzazione in tempo reale nativa di Wayland richiede il ricevitore video GTK di GStreamer. Installare il modulo gtksink per GStreamer e riavviare OrcaSlicer." diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po index 99cd0d0a83..c170f2f6c1 100644 --- a/localization/i18n/ja/OrcaSlicer_ja.po +++ b/localization/i18n/ja/OrcaSlicer_ja.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -2473,13 +2473,6 @@ msgstr "アップデートが利用可能です。プリセットバンドルの msgid "%s has been removed." msgstr "%sを削除しました。" - -msgid "Select the language" -msgstr "言語を選択" - -msgid "Language" -msgstr "言語" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2517,6 +2510,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "プラグインダイアログを開けませんでした (不明なエラー)。" +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "プラグインターミナル" @@ -4610,6 +4616,12 @@ msgstr "" "一時的なGコードの出力Gコードへのコピーに失敗しました。 もしかしたらSDカードが書き込みロックされていませんか?\n" "エラーメッセージ:%1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "一時Gコードの出力Gコードへのコピーに失敗しました。 ターゲットデバイスに問題がある可能性があります。もう一度エクスポートするか、別のデバイスを使用してみてください。 破損した出力Gコードは%1%.tmpにあります。" @@ -6514,6 +6526,9 @@ msgstr "3Dシーンで選択したオブジェクトの周りにアウトライ msgid "Preferences" msgstr "設定" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8823,7 +8838,6 @@ msgstr "続行しますか?" msgid "Language selection" msgstr "言語選択" - msgid "Asia-Pacific" msgstr "アジア太平洋地域" @@ -8928,6 +8942,9 @@ msgstr "現在のインスタンスのパス: " msgid "General" msgstr "一般" +msgid "Language" +msgstr "言語" + msgid "Metric" msgstr "メートル" @@ -9135,6 +9152,12 @@ msgstr "マルチデバイス管理" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "このオプションを有効にすると、複数のデバイスに同時にタスクを送信し、複数のデバイスを管理できます。" +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "フィラメントグルーピングモード選択のポップアップ" @@ -9885,6 +9908,9 @@ msgstr "親プリセットから継承したすべての値をこのプリセッ msgid "Detach from parent" msgstr "親から分離" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "独立したプリセット" @@ -11788,6 +11814,17 @@ msgstr "3Dconnexion設定を表示/非表示" msgid "Switch table page" msgstr "テーブルページを切り替え" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Space" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "ショートカット一覧を表示" @@ -11950,15 +11987,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "準備/プレビュー間の切り替え" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Space" - -# AI Translated -msgid "Open actions speed dial" -msgstr "アクションスピードダイヤルを開く" - msgid "Plater" msgstr "準備" @@ -12234,8 +12262,8 @@ msgstr "修復を取消しました" msgid "Copying of file %1% to %2% failed: %3%" msgstr "ファイル %1% を %2% へのコピーが失敗しました (%3%)" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "構成を更新する前に、未保存の変更をご確認ください" +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "設定パッケージ: " @@ -12243,6 +12271,12 @@ msgstr "設定パッケージ: " msgid " updated to " msgstr " を更新しました " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "構成を更新する前に、未保存の変更をご確認ください" + msgid "Open G-code file:" msgstr "G-codeファイルを開く" @@ -13905,6 +13939,9 @@ msgstr "整列直線" msgid "Concentric" msgstr "同心" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "ヒルベルト曲線" @@ -13996,29 +14033,21 @@ msgstr "" msgid "Top surface fill order" msgstr "上面の充填順序" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"中心を基準とするパターン (同心円、アルキメデス弦、八芒星スパイラル) を使用する場合に、上面を充填する方向です。\n" -"外向きは面の中心から始まるため、余分な材料が最も目立たない縁へ押し出されます。内向きは縁から始まり、中心の細かいカーブで終わります。\n" -"デフォルトは最短経路順で、どちらの方向にもなり得ます。" # AI Translated msgid "Bottom surface fill order" msgstr "底面の充填順序" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"中心を基準とするパターン (同心円、アルキメデス弦、八芒星スパイラル) を使用する場合に、底面を充填する方向です。\n" -"内向きは各面を幅の広い外側のカーブから始めるため、中心の細かいカーブが定着しにくいベッドでも1層目の密着性が向上します。外向きは中心から始まり、余分な材料を縁へ押し出します。\n" -"デフォルトは最短経路順で、どちらの方向にもなり得ます。" msgid "Internal solid infill pattern" msgstr "内部ソリッドインフィルパターン" @@ -22505,6 +22534,33 @@ msgstr "" "反りを避ける\n" "ABSのような反りやすい素材を印刷する場合、ヒートベッドの温度を適切に上げることで、反りが発生する確率を下げることができることをご存知ですか?" +#~ msgid "Select the language" +#~ msgstr "言語を選択" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "アクションスピードダイヤルを開く" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "中心を基準とするパターン (同心円、アルキメデス弦、八芒星スパイラル) を使用する場合に、上面を充填する方向です。\n" +#~ "外向きは面の中心から始まるため、余分な材料が最も目立たない縁へ押し出されます。内向きは縁から始まり、中心の細かいカーブで終わります。\n" +#~ "デフォルトは最短経路順で、どちらの方向にもなり得ます。" + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "中心を基準とするパターン (同心円、アルキメデス弦、八芒星スパイラル) を使用する場合に、底面を充填する方向です。\n" +#~ "内向きは各面を幅の広い外側のカーブから始めるため、中心の細かいカーブが定着しにくいベッドでも1層目の密着性が向上します。外向きは中心から始まり、余分な材料を縁へ押し出します。\n" +#~ "デフォルトは最短経路順で、どちらの方向にもなり得ます。" + # AI Translated #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "ネイティブWaylandのライブビューにはGStreamer GTKビデオシンクが必要です。GStreamer用のgtksinkプラグインをインストールし、OrcaSlicerを再起動してください。" diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po index 8d12c90228..2e883a3f99 100644 --- a/localization/i18n/ko/OrcaSlicer_ko.po +++ b/localization/i18n/ko/OrcaSlicer_ko.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: 2025-06-02 17:12+0900\n" "Last-Translator: crwusiz <crwusiz@gmail.com>\n" "Language-Team: \n" @@ -2481,13 +2481,6 @@ msgstr "사용 가능한 업데이트가 있습니다. 사전 설정 번들 대 msgid "%s has been removed." msgstr "%s이(가) 제거되었습니다." - -msgid "Select the language" -msgstr "언어 선택" - -msgid "Language" -msgstr "언어" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2525,6 +2518,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "플러그인 대화 상자를 열지 못했습니다(알 수 없는 오류)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "플러그인 터미널" @@ -4625,6 +4631,12 @@ msgstr "" "임시 Gcode를 출력 Gcode로 복사하지 못했습니다. SD 카드가 쓰기 잠겨 있나요?\n" "오류 메시지입니다: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "임시 Gcode를 출력 Gcode로 복사하지 못했습니다. 대상 장치에 문제가 있을 수 있으니 다시 내보내거나 다른 장치를 사용해 보세요. 손상된 출력 Gcode는 %1%.tmp에 있습니다." @@ -6528,6 +6540,9 @@ msgstr "3D 장면에서 선택한 객체 주변에 윤곽선 표시" msgid "Preferences" msgstr "기본 설정" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8858,7 +8873,6 @@ msgstr "계속하시겠습니까?" msgid "Language selection" msgstr "언어 선택" - msgid "Asia-Pacific" msgstr "아시아 태평양" @@ -8972,6 +8986,9 @@ msgstr "현재 인스턴스 경로: " msgid "General" msgstr "일반" +msgid "Language" +msgstr "언어" + msgid "Metric" msgstr "미터법" @@ -9197,6 +9214,12 @@ msgstr "다중 장치 관리" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "활성화하면 여러 장치에 동시에 작업을 보내고 여러 장치를 관리할 수 있습니다." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "필라멘트 그룹화 모드를 선택하기 위한 팝업" @@ -9976,6 +9999,9 @@ msgstr "상위 사전 설정에서 상속한 모든 값을 이 사전 설정으 msgid "Detach from parent" msgstr "상위 항목에서 분리" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "독립 사전 설정" @@ -11922,6 +11948,17 @@ msgstr "3D 연결 장치 설정 표시/숨기기 대화상자" msgid "Switch table page" msgstr "테이블 페이지 전환" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Space" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "키보드 단축키 목록 보기" @@ -12086,15 +12123,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "준비 하기/미리 보기 전환" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Space" - -# AI Translated -msgid "Open actions speed dial" -msgstr "작업 스피드 다이얼 열기" - msgid "Plater" msgstr "출력판" @@ -12368,8 +12396,8 @@ msgstr "수리 취소됨" msgid "Copying of file %1% to %2% failed: %3%" msgstr "파일 %1%를 %2%으로 복사 실패: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "구성 업데이트 전에 저장되지 않은 변경 사항을 확인해야 합니다." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "구성 패키지: " @@ -12377,6 +12405,12 @@ msgstr "구성 패키지: " msgid " updated to " msgstr " 로 업데이트되었습니다 " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "구성 업데이트 전에 저장되지 않은 변경 사항을 확인해야 합니다." + msgid "Open G-code file:" msgstr "Gcode 파일 열기:" @@ -14033,6 +14067,9 @@ msgstr "정렬된 직선" msgid "Concentric" msgstr "동심" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "힐베르트 곡선" @@ -14124,29 +14161,21 @@ msgstr "" msgid "Top surface fill order" msgstr "상단 표면 채우기 순서" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"중심 기반 패턴(동심원, 아르키메데스 현, 팔각별 나선)을 사용할 때 상단 표면을 채우는 방향입니다.\n" -"바깥쪽은 표면 중앙에서 시작하므로 남는 재료가 가장 눈에 덜 띄는 가장자리로 밀려납니다. 안쪽은 가장자리에서 시작하여 중앙의 좁은 곡선에서 끝납니다.\n" -"기본값은 최단 경로 순서를 사용하며 어느 방향으로든 진행될 수 있습니다." # AI Translated msgid "Bottom surface fill order" msgstr "하단 표면 채우기 순서" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"중심 기반 패턴(동심원, 아르키메데스 현, 팔각별 나선)을 사용할 때 하단 표면을 채우는 방향입니다.\n" -"안쪽은 각 표면을 더 넓은 바깥쪽 곡선에서 시작하므로, 중앙의 좁은 곡선이 잘 붙지 않는 빌드 플레이트에서 초기 레이어 접착력이 향상됩니다. 바깥쪽은 중앙에서 시작하여 남는 재료를 가장자리로 밀어냅니다.\n" -"기본값은 최단 경로 순서를 사용하며 어느 방향으로든 진행될 수 있습니다." msgid "Internal solid infill pattern" msgstr "꽉찬 내부 채우기 패턴" @@ -22372,6 +22401,33 @@ msgstr "" "뒤틀림 방지\n" "ABS와 같이 뒤틀림이 발생하기 쉬운 소재를 출력할 때, 히트베드 온도를 적절하게 높이면 뒤틀림 가능성을 줄일 수 있다는 사실을 알고 계셨나요?" +#~ msgid "Select the language" +#~ msgstr "언어 선택" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "작업 스피드 다이얼 열기" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "중심 기반 패턴(동심원, 아르키메데스 현, 팔각별 나선)을 사용할 때 상단 표면을 채우는 방향입니다.\n" +#~ "바깥쪽은 표면 중앙에서 시작하므로 남는 재료가 가장 눈에 덜 띄는 가장자리로 밀려납니다. 안쪽은 가장자리에서 시작하여 중앙의 좁은 곡선에서 끝납니다.\n" +#~ "기본값은 최단 경로 순서를 사용하며 어느 방향으로든 진행될 수 있습니다." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "중심 기반 패턴(동심원, 아르키메데스 현, 팔각별 나선)을 사용할 때 하단 표면을 채우는 방향입니다.\n" +#~ "안쪽은 각 표면을 더 넓은 바깥쪽 곡선에서 시작하므로, 중앙의 좁은 곡선이 잘 붙지 않는 빌드 플레이트에서 초기 레이어 접착력이 향상됩니다. 바깥쪽은 중앙에서 시작하여 남는 재료를 가장자리로 밀어냅니다.\n" +#~ "기본값은 최단 경로 순서를 사용하며 어느 방향으로든 진행될 수 있습니다." + # AI Translated #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "네이티브 Wayland 실시간 보기에는 GStreamer GTK 비디오 싱크가 필요합니다. GStreamer용 gtksink 플러그인을 설치한 후 OrcaSlicer를 다시 시작하십시오." diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po index 67d02bef6d..6a642229f8 100644 --- a/localization/i18n/lt/OrcaSlicer_lt.po +++ b/localization/i18n/lt/OrcaSlicer_lt.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: 2026-07-02 14:13+0300\n" "Last-Translator: Gintaras Kučinskas <sharanchius@gmail.com>\n" "Language-Team: \n" @@ -2449,13 +2449,6 @@ msgstr "Yra prieinamas atnaujinimas. Atidarykite profilių paketo dialogo langą msgid "%s has been removed." msgstr "%s buvo pašalintas." - -msgid "Select the language" -msgstr "Pasirinkite kalbą" - -msgid "Language" -msgstr "Kalba" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2493,6 +2486,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Nepavyko atidaryti papildinių dialogo lango (nežinoma klaida)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "Papildinio terminalas" @@ -4587,6 +4593,12 @@ msgstr "" "Nepavyko nukopijuoti laikinojo G-kodo į išvesties G-kodą. Gal draudžiama įrašinėti į SD kortelę?\n" "Klaidos pranešimas: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "Nepavyko nukopijuoti laikinojo G-kodo į išvesties G-kodą. Gali kilti problemų dėl tikslinio įrenginio. Bandykite eksportuoti dar kartą arba naudokite kitą įrenginį. Sugadintas išvesties G-kodas yra %1%.tmp." @@ -6490,6 +6502,9 @@ msgstr "Rodyti kontūrą aplink pasirinktą objektą 3D scenoje." msgid "Preferences" msgstr "Parinktys" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8795,7 +8810,6 @@ msgstr "Ar norite tęsti?" msgid "Language selection" msgstr "Kalbos pasirinkimas" - msgid "Asia-Pacific" msgstr "Azija-Ramusis vandenynas" @@ -8900,6 +8914,9 @@ msgstr "Dabartinės versijos kelias: " msgid "General" msgstr "Bendras" +msgid "Language" +msgstr "Kalba" + msgid "Metric" msgstr "Metrinė" @@ -9096,6 +9113,12 @@ msgstr "Kelių įrenginių valdymas" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Kai įjungta ši funkcija, jūs galite siųsti užduotį keliems įrenginiams vienu metu, taip apt kontroliuoti keletą įrenginių." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Iššokantis langas gijų grupavimo režimui pasirinkti" @@ -9802,6 +9825,9 @@ msgstr "Nukopijuoja į šį profilį visas iš pirminio profilio paveldėtas rei msgid "Detach from parent" msgstr "Atskirti nuo tėvinio profilio" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Savarankiškas profilis" @@ -11692,6 +11718,17 @@ msgstr "Rodyti/slėpti 3DConnexion įrenginių nustatymų dialogo langą" msgid "Switch table page" msgstr "Perjungti lentelės puslapį" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Tarpas" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Rodyti sparčiųjų klavišų sąrašą" @@ -11852,15 +11889,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Perjungimas tarp Paruošti / Peržiūrėti" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Tarpas" - -# AI Translated -msgid "Open actions speed dial" -msgstr "Atidaryti veiksmų greitosios prieigos meniu" - msgid "Plater" msgstr "Plokštė" @@ -12128,8 +12156,8 @@ msgstr "Taisymas atšauktas" msgid "Copying of file %1% to %2% failed: %3%" msgstr "Failo %1% kopijavimas į %2% nepavyko: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Prieš atnaujinant konfigūraciją reikia patikrinti neišsaugotus pakeitimus." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Konfigūracijos paketas: " @@ -12137,6 +12165,12 @@ msgstr "Konfigūracijos paketas: " msgid " updated to " msgstr " atnaujintas į " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Prieš atnaujinant konfigūraciją reikia patikrinti neišsaugotus pakeitimus." + msgid "Open G-code file:" msgstr "Atidaryti G-kodo failą:" @@ -13703,6 +13737,9 @@ msgstr "Sulygiuotas tiesiaeigis" msgid "Concentric" msgstr "Koncentrinis" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Hilberto kreivė" @@ -13792,29 +13829,21 @@ msgstr "" msgid "Top surface fill order" msgstr "Viršutinio paviršiaus užpildymo tvarka" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Kryptis, kuria užpildomi viršutiniai paviršiai naudojant į centrą orientuotą raštą (koncentrinį, Archimedo stygų, Oktogramos spiralės).\n" -"„Į išorę“ prasideda paviršiaus centre, todėl bet koks perteklinė medžiaga stumiama link krašto, kur ji mažiausiai matoma. „Į vidų“ prasideda nuo krašto ir baigiasi ankštomis kreivėmis centre.\n" -"Numatytoji tvarka naudoja trumpiausio kelio rikiavimą, kuris gali vykti bet kuria kryptimi." # AI Translated msgid "Bottom surface fill order" msgstr "Apatinio paviršiaus užpildymo tvarka" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Kryptis, kuria užpildomi apatiniai paviršiai naudojant į centrą orientuotą raštą (koncentrinį, Archimedo stygų, Oktogramos spiralės).\n" -"„Į vidų“ pradeda kiekvieną paviršių platesnėmis išorinėmis kreivėmis, o tai pagerina pirmojo sluoksnio sukibimą ant pagrindų, kur ankštos kreivės centre gali nesilaikyti. „Į išorę“ prasideda centre, stumdama bet kokią perteklinę medžiagą link krašto.\n" -"Numatytoji tvarka naudoja trumpiausio kelio rikiavimą, kuris gali vykti bet kuria kryptimi." msgid "Internal solid infill pattern" msgstr "Vidinio tvirto užpildo raštas" @@ -21652,6 +21681,33 @@ msgstr "" "Venkite deformacijų (warping)\n" "Ar žinojote, kad spausdinant medžiagas, kurios yra linkusios trauktis ir riestis (pvz., ABS), tinkamas kaitinamojo pagrindo temperatūros padidinimas gali sumažinti deformacijų (warping) tikimybę?" +#~ msgid "Select the language" +#~ msgstr "Pasirinkite kalbą" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "Atidaryti veiksmų greitosios prieigos meniu" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Kryptis, kuria užpildomi viršutiniai paviršiai naudojant į centrą orientuotą raštą (koncentrinį, Archimedo stygų, Oktogramos spiralės).\n" +#~ "„Į išorę“ prasideda paviršiaus centre, todėl bet koks perteklinė medžiaga stumiama link krašto, kur ji mažiausiai matoma. „Į vidų“ prasideda nuo krašto ir baigiasi ankštomis kreivėmis centre.\n" +#~ "Numatytoji tvarka naudoja trumpiausio kelio rikiavimą, kuris gali vykti bet kuria kryptimi." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Kryptis, kuria užpildomi apatiniai paviršiai naudojant į centrą orientuotą raštą (koncentrinį, Archimedo stygų, Oktogramos spiralės).\n" +#~ "„Į vidų“ pradeda kiekvieną paviršių platesnėmis išorinėmis kreivėmis, o tai pagerina pirmojo sluoksnio sukibimą ant pagrindų, kur ankštos kreivės centre gali nesilaikyti. „Į išorę“ prasideda centre, stumdama bet kokią perteklinę medžiagą link krašto.\n" +#~ "Numatytoji tvarka naudoja trumpiausio kelio rikiavimą, kuris gali vykti bet kuria kryptimi." + #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "Tiesioginei „Native Wayland“ peržiūrai reikalingas „GStreamer GTK“ vaizdo sinchronizatorius (video sink). Įdiekite „GStreamer“ skirtą „gtksink“ papildinį, tada iš naujo paleiskite „OrcaSlicer“." diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po index eff0fdc6b0..bcf29ef0ea 100644 --- a/localization/i18n/nl/OrcaSlicer_nl.po +++ b/localization/i18n/nl/OrcaSlicer_nl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -2679,13 +2679,6 @@ msgstr "Er is een update beschikbaar. Open het dialoogvenster voor de voorinstel msgid "%s has been removed." msgstr "%s is verwijderd." - -msgid "Select the language" -msgstr "Kies de taal" - -msgid "Language" -msgstr "Taal" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2723,6 +2716,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Openen van het dialoogvenster Plug-ins is mislukt (onbekende fout)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "Plug-in-terminal" @@ -5008,6 +5014,12 @@ msgstr "" "Fout bij het exporteren naar output-G-code. Is de SD-kaart geblokkeerd tegen schrijven?\n" "Foutbericht: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "Fout bij het exporteren naar output-G-code. Het probleem ligt mogelijk bij het doelapparaat. Probeer het opnieuw te exporteren of gebruik een ander apparat. De beschadigde G-code is opgeslagen als %1%.tmp." @@ -7086,6 +7098,9 @@ msgstr "Toon een omtrek rond het geselecteerde object in de 3D-scène." msgid "Preferences" msgstr "Voorkeuren" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -9600,7 +9615,6 @@ msgstr "Wilt u doorgaan?" msgid "Language selection" msgstr "Taal selectie" - msgid "Asia-Pacific" msgstr "Azië-Pacific" @@ -9715,6 +9729,9 @@ msgstr "Huidig instancepad: " msgid "General" msgstr "Algemeen" +msgid "Language" +msgstr "Taal" + msgid "Metric" msgstr "Metrisch" @@ -9946,6 +9963,12 @@ msgstr "Beheer van meerdere apparaten" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Met deze optie ingeschakeld kunt u een taak tegelijkertijd naar meerdere apparaten sturen en meerdere apparaten beheren." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + # AI Translated msgid "Pop up to select filament grouping mode" msgstr "Pop-up om de filamentgroeperingsmodus te kiezen" @@ -10737,6 +10760,9 @@ msgstr "Kopieert alle overgeërfde waarden van de bovenliggende voorinstelling n msgid "Detach from parent" msgstr "Losmaken van bovenliggend element" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Unieke voorinstelling" @@ -12818,6 +12844,17 @@ msgstr "Dialoogvenster met instellingen voor 3Dconnexion-apparaten weergeven/ver msgid "Switch table page" msgstr "Schakeltabel pagina" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Spatie" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Toon lijst met sneltoetsen" @@ -12985,15 +13022,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Wisselen tussen Voorbereiden/Voorvertoning" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Spatie" - -# AI Translated -msgid "Open actions speed dial" -msgstr "Snelmenu met acties openen" - msgid "Plater" msgstr "Plaat" @@ -13292,8 +13320,8 @@ msgstr "Repareren geannuleerd" msgid "Copying of file %1% to %2% failed: %3%" msgstr "Het kopieeren van bestand %1% naar %2% is mislukt: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Controleer niet-opgeslagen wijzigingen voordat u de configuratie bijwerkt." +msgid "Downloading new vendor profile(s): " +msgstr "" # AI Translated msgid "Configuration package: " @@ -13303,6 +13331,12 @@ msgstr "Configuratiepakket: " msgid " updated to " msgstr " bijgewerkt naar " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Controleer niet-opgeslagen wijzigingen voordat u de configuratie bijwerkt." + msgid "Open G-code file:" msgstr "Open G-code bestand:" @@ -15049,6 +15083,9 @@ msgstr "Uitgelijnd Rechtlijnig" msgid "Concentric" msgstr "Concentrisch" +msgid "Spiral Inset" +msgstr "" + # AI Translated msgid "Hilbert Curve" msgstr "Hilbertkromme" @@ -15144,29 +15181,21 @@ msgstr "" msgid "Top surface fill order" msgstr "Vulvolgorde bovenoppervlak" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Richting waarin bovenoppervlakken worden gevuld bij gebruik van een patroon dat vanuit het midden werkt (Concentrisch, Archimedische koorden, Octagramspiraal).\n" -"Naar buiten begint in het midden van het oppervlak, zodat overtollig materiaal naar de rand wordt geduwd, waar het het minst zichtbaar is. Naar binnen begint aan de rand en eindigt met de krappe bochten in het midden.\n" -"Standaard gebruikt de volgorde van het kortste pad, die beide kanten op kan lopen." # AI Translated msgid "Bottom surface fill order" msgstr "Vulvolgorde onderoppervlak" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Richting waarin onderoppervlakken worden gevuld bij gebruik van een patroon dat vanuit het midden werkt (Concentrisch, Archimedische koorden, Octagramspiraal).\n" -"Naar binnen begint elk oppervlak met de bredere buitenbochten, wat de hechting van de eerste laag verbetert op printbedden waar de krappe bochten in het midden mogelijk niet hechten. Naar buiten begint in het midden en duwt overtollig materiaal naar de rand.\n" -"Standaard gebruikt de volgorde van het kortste pad, die beide kanten op kan lopen." msgid "Internal solid infill pattern" msgstr "Intern massief invulpatroon" @@ -24092,6 +24121,33 @@ msgstr "" "Kromtrekken voorkomen\n" "Wist je dat bij het printen van materialen die gevoelig zijn voor kromtrekken, zoals ABS, een juiste verhoging van de temperatuur van het warmtebed de kans op kromtrekken kan verkleinen?" +#~ msgid "Select the language" +#~ msgstr "Kies de taal" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "Snelmenu met acties openen" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Richting waarin bovenoppervlakken worden gevuld bij gebruik van een patroon dat vanuit het midden werkt (Concentrisch, Archimedische koorden, Octagramspiraal).\n" +#~ "Naar buiten begint in het midden van het oppervlak, zodat overtollig materiaal naar de rand wordt geduwd, waar het het minst zichtbaar is. Naar binnen begint aan de rand en eindigt met de krappe bochten in het midden.\n" +#~ "Standaard gebruikt de volgorde van het kortste pad, die beide kanten op kan lopen." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Richting waarin onderoppervlakken worden gevuld bij gebruik van een patroon dat vanuit het midden werkt (Concentrisch, Archimedische koorden, Octagramspiraal).\n" +#~ "Naar binnen begint elk oppervlak met de bredere buitenbochten, wat de hechting van de eerste laag verbetert op printbedden waar de krappe bochten in het midden mogelijk niet hechten. Naar buiten begint in het midden en duwt overtollig materiaal naar de rand.\n" +#~ "Standaard gebruikt de volgorde van het kortste pad, die beide kanten op kan lopen." + # AI Translated #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "Voor de native Wayland-liveview is de GStreamer GTK-videosink nodig. Installeer de gtksink-plug-in voor GStreamer en start OrcaSlicer opnieuw." diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po index 6f74f4b602..880d786d5e 100644 --- a/localization/i18n/pl/OrcaSlicer_pl.po +++ b/localization/i18n/pl/OrcaSlicer_pl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer 2.3.0-rc\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: \n" "Last-Translator: Krzysztof Morga <<tlumaczeniebs@gmail.com>>\n" "Language-Team: \n" @@ -2512,13 +2512,6 @@ msgstr "Dostępna jest aktualizacja. Otwórz okno pakietu profili, aby ją zains msgid "%s has been removed." msgstr "%s został usunięty." - -msgid "Select the language" -msgstr "Wybierz język" - -msgid "Language" -msgstr "Język" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2556,6 +2549,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Nie udało się otworzyć okna wtyczek (nieznany błąd)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "Terminal wtyczek" @@ -4702,6 +4708,12 @@ msgstr "" "Kopiowanie tymczasowego G-code do wyjściowego pliku G-code nie powiodło się. Być może karta SD jest zablokowana do zapisu?\n" "Komunikat błędu: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "Kopiowanie tymczasowego G-code do wyjściowego pliku G-code nie powiodło się. Może być problem z urządzeniem docelowym, spróbuj ponownie wyeksportować lub użyć innego urządzenia. Uszkodzony plik wyjściowego G-code znajduje się w pliku %1%.tmp." @@ -6651,6 +6663,9 @@ msgstr "Przełącza wyświetlanie konturu wokół zaznaczonego obiektu w scenie msgid "Preferences" msgstr "Preferencje" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -9014,7 +9029,6 @@ msgstr "Czy kontynuować?" msgid "Language selection" msgstr "Wybór języka" - msgid "Asia-Pacific" msgstr "Azja i Pacyfik" @@ -9128,6 +9142,9 @@ msgstr "Aktualna ścieżka instancji: " msgid "General" msgstr "Ogólne" +msgid "Language" +msgstr "Język" + msgid "Metric" msgstr "Metryczne" @@ -9352,6 +9369,12 @@ msgstr "Obsługiwanie wielu urządzeń" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Umożliwia wysyłanie zadania do wielu urządzeń jednocześnie i zarządzanie nimi." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Okno dialogowe do wyboru trybu grupowania filamentów" @@ -10132,6 +10155,9 @@ msgstr "Kopiuje do tego profilu wszystkie wartości odziedziczone z profilu nadr msgid "Detach from parent" msgstr "Odłącz od elementu nadrzędnego" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Profil niezależny" @@ -12095,6 +12121,17 @@ msgstr "Pokaż/ukryj okno dialogowe ustawień urządzeń 3Dconnexion" msgid "Switch table page" msgstr "Przełącz stronę tabeli" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Spacja" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Pokaż listę skrótów klawiszowych" @@ -12257,15 +12294,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Przełączanie między przygotowaniem/podglądem" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Spacja" - -# AI Translated -msgid "Open actions speed dial" -msgstr "Otwórz szybkie menu akcji" - msgid "Plater" msgstr "Płyta" @@ -12540,8 +12568,8 @@ msgstr "Naprawa anulowana" msgid "Copying of file %1% to %2% failed: %3%" msgstr "Nie udało się skopiować pliku %1% do %2%: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Należy sprawdzić niezapisane zmiany przed aktualizacją konfiguracji." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Pakiet konfiguracyjny:" @@ -12549,6 +12577,12 @@ msgstr "Pakiet konfiguracyjny:" msgid " updated to " msgstr " aktualizacja do " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Należy sprawdzić niezapisane zmiany przed aktualizacją konfiguracji." + msgid "Open G-code file:" msgstr "Otwórz plik G-code:" @@ -14206,6 +14240,9 @@ msgstr "Wyrównany prostoliniowy" msgid "Concentric" msgstr "Koncentryczny" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Krzywa Hilberta" @@ -14297,29 +14334,21 @@ msgstr "" msgid "Top surface fill order" msgstr "Kolejność wypełniania górnej powierzchni" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Kierunek wypełniania górnych powierzchni przy użyciu wzoru opartego na środku (Koncentryczny, Cięciwy Archimedesa, Spirala Octagram).\n" -"Na zewnątrz zaczyna od środka powierzchni, dzięki czemu nadmiar materiału jest wypychany ku krawędzi, gdzie jest najmniej widoczny. Do wewnątrz zaczyna od krawędzi i kończy ciasnymi łukami na środku.\n" -"Domyślnie używana jest kolejność najkrótszej ścieżki, która może przebiegać w dowolnym kierunku." # AI Translated msgid "Bottom surface fill order" msgstr "Kolejność wypełniania dolnej powierzchni" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Kierunek wypełniania dolnych powierzchni przy użyciu wzoru opartego na środku (Koncentryczny, Cięciwy Archimedesa, Spirala Octagram).\n" -"Do wewnątrz rozpoczyna każdą powierzchnię od szerszych łuków zewnętrznych, co poprawia przyczepność pierwszej warstwy na stołach, do których ciasne łuki na środku mogą nie przylegać. Na zewnątrz zaczyna od środka, wypychając nadmiar materiału ku krawędzi.\n" -"Domyślnie używana jest kolejność najkrótszej ścieżki, która może przebiegać w dowolnym kierunku." msgid "Internal solid infill pattern" msgstr "Wzór wewnętrznego pełnego wypełnienia" @@ -22549,6 +22578,33 @@ msgstr "" "Unikaj odkształceń\n" "Czy wiesz, że podczas drukowania filamentami podatnymi na odkształcenia, takimi jak ABS, odpowiednie zwiększenie temperatury podgrzewanej płyty może zmniejszyć prawdopodobieństwo odkształceń?" +#~ msgid "Select the language" +#~ msgstr "Wybierz język" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "Otwórz szybkie menu akcji" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Kierunek wypełniania górnych powierzchni przy użyciu wzoru opartego na środku (Koncentryczny, Cięciwy Archimedesa, Spirala Octagram).\n" +#~ "Na zewnątrz zaczyna od środka powierzchni, dzięki czemu nadmiar materiału jest wypychany ku krawędzi, gdzie jest najmniej widoczny. Do wewnątrz zaczyna od krawędzi i kończy ciasnymi łukami na środku.\n" +#~ "Domyślnie używana jest kolejność najkrótszej ścieżki, która może przebiegać w dowolnym kierunku." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Kierunek wypełniania dolnych powierzchni przy użyciu wzoru opartego na środku (Koncentryczny, Cięciwy Archimedesa, Spirala Octagram).\n" +#~ "Do wewnątrz rozpoczyna każdą powierzchnię od szerszych łuków zewnętrznych, co poprawia przyczepność pierwszej warstwy na stołach, do których ciasne łuki na środku mogą nie przylegać. Na zewnątrz zaczyna od środka, wypychając nadmiar materiału ku krawędzi.\n" +#~ "Domyślnie używana jest kolejność najkrótszej ścieżki, która może przebiegać w dowolnym kierunku." + # AI Translated #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "Natywny podgląd na żywo w Wayland wymaga ujścia wideo GStreamer GTK. Zainstaluj wtyczkę gtksink dla GStreamer, a następnie uruchom ponownie OrcaSlicer." diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 6f7bf473c0..183c8f7088 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: 2026-07-26 11:14-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: Portuguese, Brazilian\n" @@ -2356,13 +2356,6 @@ msgstr "Há uma atualização disponível. Abra a caixa de diálogo do pacote de msgid "%s has been removed." msgstr "%s foi removido." - -msgid "Select the language" -msgstr "Selecione o idioma" - -msgid "Language" -msgstr "Idioma" - #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." msgstr "Falha ao mudar o idioma do OrcaSlicer para %s." @@ -2395,6 +2388,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Falha ao abrir a caixa de diálogo de Plugins (erro desconhecido)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "Terminal de plugin" @@ -4441,6 +4447,12 @@ msgstr "" "A cópia do G-code temporário para o G-code de saída falhou. Talvez o cartão SD esteja travado pra escrita?\n" "Mensagem de erro: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "A cópia do G-code temporário para o G-code de saída falhou. Pode haver problema com o dispositivo de destino, por favor tente exportar novamente ou usar outro dispositivo. O G-code de saída corrompido está em %1%.tmp." @@ -6323,6 +6335,9 @@ msgstr "Mostrar contorno ao redor do objeto selecionado na cena 3D." msgid "Preferences" msgstr "Preferências" +msgid "Open speed dial..." +msgstr "" + msgctxt "Menu" msgid "Edit" msgstr "Editar" @@ -8570,7 +8585,6 @@ msgstr "Você deseja continuar?" msgid "Language selection" msgstr "Seleção de idioma" - msgid "Asia-Pacific" msgstr "Ásia-Pacífico" @@ -8675,6 +8689,9 @@ msgstr "Caminho da Instância Atual: " msgid "General" msgstr "Geral" +msgid "Language" +msgstr "Idioma" + msgid "Metric" msgstr "Métrico" @@ -8879,6 +8896,12 @@ msgstr "Gerenciamento de multi dispositivos" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Com esta opção habilitada, você pode enviar uma tarefa para vários dispositivos ao mesmo tempo e gerenciar vários dispositivos." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Abrir seleção do modo de agrupamento de filamento" @@ -9587,6 +9610,9 @@ msgstr "Copia para esta predefinição todos os valores herdados da predefiniç msgid "Detach from parent" msgstr "Separar do pai" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Predefinição única" @@ -11450,6 +11476,16 @@ msgstr "Mostrar/Ocultar diálogo de configurações de dispositivos 3Dconnexion" msgid "Switch table page" msgstr "Trocar página da tabela" +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Espaço" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Mostrar lista de atalhos de teclado" @@ -11605,14 +11641,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Alternar entre Preparar/Pré-visualizar" -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Espaço" - -# AI Translated -msgid "Open actions speed dial" -msgstr "Abrir menu rápido de ações" - msgid "Plater" msgstr "Mesa" @@ -11877,8 +11905,8 @@ msgstr "Reparo cancelado" msgid "Copying of file %1% to %2% failed: %3%" msgstr "Falha ao copiar o arquivo %1% para %2%: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Verifique as alterações não salvas antes de atualizar a configuração." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Pacote de configuração: " @@ -11886,6 +11914,12 @@ msgstr "Pacote de configuração: " msgid " updated to " msgstr " atualizado para " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Verifique as alterações não salvas antes de atualizar a configuração." + msgid "Open G-code file:" msgstr "Abrir arquivo G-code:" @@ -13455,6 +13489,9 @@ msgstr "Retilíneo alinhado" msgid "Concentric" msgstr "Concêntrico" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Curva de Hilbert" @@ -13535,25 +13572,19 @@ msgid "Top surface fill order" msgstr "Ordem de preenchimento da superfície superior" msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Direção em que as superfícies superiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral de Octograma).\n" -"Para fora começa no centro da superfície, de modo que qualquer excesso de material seja empurrado em direção à borda, onde é menos visível. Para dentro começa na borda e termina com as curvas fechadas no centro.\n" -"O padrão usa a ordenação de caminho mais curto, que pode seguir em qualquer direção." msgid "Bottom surface fill order" msgstr "Ordem de preenchimento da superfície inferior" msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Direção em que as superfícies inferiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral de Octograma).\n" -"Para dentro começa cada superfície com as curvas externas mais largas, o que melhora a aderência da primeira camada em mesas onde as curvas fechadas no centro podem não aderir. Para fora começa no centro, empurrando qualquer excesso de material em direção à borda.\n" -"O padrão usa a ordenação de caminho mais curto, que pode seguir em qualquer direção." msgid "Internal solid infill pattern" msgstr "Padrão de preenchimento sólido interno" @@ -21261,6 +21292,31 @@ msgstr "" "Evitar empenamento\n" "Você sabia que ao imprimir materiais propensos ao empenamento como ABS, aumentar adequadamente a temperatura da mesa aquecida pode reduzir a probabilidade de empenamento?" +#~ msgid "Select the language" +#~ msgstr "Selecione o idioma" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "Abrir menu rápido de ações" + +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Direção em que as superfícies superiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral de Octograma).\n" +#~ "Para fora começa no centro da superfície, de modo que qualquer excesso de material seja empurrado em direção à borda, onde é menos visível. Para dentro começa na borda e termina com as curvas fechadas no centro.\n" +#~ "O padrão usa a ordenação de caminho mais curto, que pode seguir em qualquer direção." + +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Direção em que as superfícies inferiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral de Octograma).\n" +#~ "Para dentro começa cada superfície com as curvas externas mais largas, o que melhora a aderência da primeira camada em mesas onde as curvas fechadas no centro podem não aderir. Para fora começa no centro, empurrando qualquer excesso de material em direção à borda.\n" +#~ "O padrão usa a ordenação de caminho mais curto, que pode seguir em qualquer direção." + #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "A visualização ao vivo nativa do Wayland requer o receptor de vídeo GTK do GStreamer. Instale o plugin gtksink para GStreamer e reinicie o OrcaSlicer." diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po index 2e33546ae8..8d1bf38c13 100644 --- a/localization/i18n/ru/OrcaSlicer_ru.po +++ b/localization/i18n/ru/OrcaSlicer_ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer V2.5.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: 2026-02-25 13:38+0300\n" "Last-Translator: Felix14_v2\n" "Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg <andylg@yandex.ru>\n" @@ -2431,13 +2431,6 @@ msgstr "Доступно обновление. Проверьте меню па msgid "%s has been removed." msgstr "%s был удалён." - -msgid "Select the language" -msgstr "Выбор языка" - -msgid "Language" -msgstr "Язык" - #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." msgstr "Не удалось переключить язык на %s." @@ -2469,6 +2462,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Не удалось открыть меню плагинов (неизвестная ошибка)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + msgid "Plugin Terminal" msgstr "Консоль плагина" @@ -4577,6 +4583,12 @@ msgstr "" "Не удалось скопировать временный G-код в целевое расположение. Возможно, накопитель защищён от записи?\n" "Сообщение об ошибке: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "Не удалось скопировать временный G-код в целевое расположение. Возможно, проблема с устройством хранения, попробуйте выполнить экспорт снова или использовать другое устройство. Повреждённый выходной файл G-кода находится в %1%.tmp." @@ -6558,6 +6570,9 @@ msgstr "Отображение контура вокруг выбранных м msgid "Preferences" msgstr "Настройки" +msgid "Open speed dial..." +msgstr "" + msgctxt "Menu" msgid "Edit" msgstr "Правка" @@ -8850,7 +8865,6 @@ msgstr "Хотите продолжить?" msgid "Language selection" msgstr "Выбор языка" - msgid "Asia-Pacific" msgstr "Азиатско-Тихоокеанский" @@ -8956,6 +8970,9 @@ msgstr "Расположение: " msgid "General" msgstr "Общие" +msgid "Language" +msgstr "Язык" + msgid "Metric" msgstr "Метрическая СИ" @@ -9159,6 +9176,12 @@ msgstr "Управление несколькими устройствами Bam msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Если включено, вы сможете управлять несколькими устройствами и отправлять задания на печать на несколько устройств одновременно." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + # Запрашивать выбор режима группировки? msgid "Pop up to select filament grouping mode" msgstr "Всплывающее окно для выбора режима группировки материалов" @@ -9879,6 +9902,9 @@ msgstr "Копирует в этот профиль все значения, у msgid "Detach from parent" msgstr "Сделать независимым" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Независимый профиль" @@ -11780,6 +11806,16 @@ msgstr "Показать/скрыть диалоговое окно настро msgid "Switch table page" msgstr "Переключение между вкладками" +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Пробел" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Показать список сочетаний клавиш" @@ -11936,13 +11972,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Переключение между подготовкой и просмотром нарезки" -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Пробел" - -msgid "Open actions speed dial" -msgstr "Открыть строку быстрых действий" - # Plater – это название библиотеки. Используется в меню горячих клавиш в качестве заголовка сочетаний клавиш, которые работают внутри пространства Plater. Как минимум на Windows не отображается. msgid "Plater" msgstr "Рабочая область" @@ -12207,8 +12236,8 @@ msgstr "Восстановление отменено" msgid "Copying of file %1% to %2% failed: %3%" msgstr "Не удалось скопировать файл %1% в %2%: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Перед обновлением профилей необходимо проверить несохранённые изменения." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Пакет профилей: " @@ -12216,6 +12245,12 @@ msgstr "Пакет профилей: " msgid " updated to " msgstr " обновлён до " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Перед обновлением профилей необходимо проверить несохранённые изменения." + msgid "Open G-code file:" msgstr "Выберите G-код файл:" @@ -13850,6 +13885,9 @@ msgstr "Ровный зигзаг" msgid "Concentric" msgstr "Эквидистанты" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Кривая Гильберта" @@ -13944,27 +13982,19 @@ msgid "Top surface fill order" msgstr "Направление печати" msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Направление печати спирали/эквидистант на верхних поверхностях. Позволяет управляемо распределять избыток материала.\n" -"• По умолчанию: использовать кратчайший путь.\n" -"• Наружу: от центра шаблона к краю модели.\n" -"• Внутрь: от края модели к центру шаблона." msgid "Bottom surface fill order" msgstr "Направление печати" msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Направление печати спирали/эквидистант на нижних поверхностях. Позволяет управляемо распределять избыток материала.\n" -"• По умолчанию: использовать кратчайший путь.\n" -"• Наружу: от центра шаблона к краю модели.\n" -"• Внутрь: от края модели к центру шаблона." msgid "Internal solid infill pattern" msgstr "Шаблон сплошного заполнения" @@ -22301,6 +22331,32 @@ msgstr "" "Предотвращение коробления материала\n" "Знаете ли вы, что при печати материалами, склонными к короблению, таких как ABS, повышение температуры подогреваемого стола может снизить эту вероятность?" +#~ msgid "Select the language" +#~ msgstr "Выбор языка" + +#~ msgid "Open actions speed dial" +#~ msgstr "Открыть строку быстрых действий" + +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Направление печати спирали/эквидистант на верхних поверхностях. Позволяет управляемо распределять избыток материала.\n" +#~ "• По умолчанию: использовать кратчайший путь.\n" +#~ "• Наружу: от центра шаблона к краю модели.\n" +#~ "• Внутрь: от края модели к центру шаблона." + +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Направление печати спирали/эквидистант на нижних поверхностях. Позволяет управляемо распределять избыток материала.\n" +#~ "• По умолчанию: использовать кратчайший путь.\n" +#~ "• Наружу: от центра шаблона к краю модели.\n" +#~ "• Внутрь: от края модели к центру шаблона." + #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "Для нативного отображения трансляции в Wayland требуется gtksink (плагин для GStreamer). Установите необходимый пакет плагинов и перезапустите OrcaSlicer." diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po index b9aa481d4c..e71aaaa56a 100644 --- a/localization/i18n/sv/OrcaSlicer_sv.po +++ b/localization/i18n/sv/OrcaSlicer_sv.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -2767,13 +2767,6 @@ msgstr "Det finns en uppdatering tillgänglig. Öppna dialogrutan för förinst msgid "%s has been removed." msgstr "%s har tagits bort." - -msgid "Select the language" -msgstr "Välj språk" - -msgid "Language" -msgstr "Språk" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2811,6 +2804,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Det gick inte att öppna dialogrutan Insticksmoduler (okänt fel)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "Terminal för insticksmoduler" @@ -5066,6 +5072,12 @@ msgstr "" "Det gick inte att kopiera den tillfälliga G-code-filen till utdatafilen. Kanske är SD-kortet skrivskyddat?\n" "Felmeddelande: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + # AI Translated #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." @@ -7169,6 +7181,9 @@ msgstr "Visa en kontur runt det markerade objektet i 3D-scenen." msgid "Preferences" msgstr "Inställningar" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -9692,7 +9707,6 @@ msgstr "Fortsätta?" msgid "Language selection" msgstr "Språkval" - msgid "Asia-Pacific" msgstr "Asien-Stillahavsområdet" @@ -9812,6 +9826,9 @@ msgstr "Sökväg till aktuell instans: " msgid "General" msgstr "Allmän" +msgid "Language" +msgstr "Språk" + msgid "Metric" msgstr "Metrisk" @@ -10052,6 +10069,12 @@ msgstr "Hantering av flera enheter" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Med det här alternativet aktiverat kan du skicka en uppgift till flera enheter samtidigt och hantera flera enheter." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + # AI Translated msgid "Pop up to select filament grouping mode" msgstr "Visa dialogruta för val av filamentgrupperingsläge" @@ -10851,6 +10874,9 @@ msgstr "Kopierar alla ärvda värden från den överordnade förinställningen t msgid "Detach from parent" msgstr "Koppla loss från överordnad" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Unik förinställning" @@ -12982,6 +13008,17 @@ msgstr "Visa/Dölj 3Dconnexion enheternas inställnings dialogruta" msgid "Switch table page" msgstr "Byt tabellsida" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Blanksteg" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Visa tangentbordets genvägs lista" @@ -13147,15 +13184,6 @@ msgstr "Tabb" msgid "Switch between Prepare/Preview" msgstr "Växla mellan Förbered/Förhandsgranska" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Blanksteg" - -# AI Translated -msgid "Open actions speed dial" -msgstr "Öppna snabbvalsmenyn för åtgärder" - msgid "Plater" msgstr "Plätering/Förgyllning" @@ -13454,8 +13482,8 @@ msgstr "Reparation avbruten" msgid "Copying of file %1% to %2% failed: %3%" msgstr "Kopierar fil %1% till %2% misslyckade: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Kontrollera ej sparade ändringar innan konfigureringen uppdateras." +msgid "Downloading new vendor profile(s): " +msgstr "" # AI Translated msgid "Configuration package: " @@ -13465,6 +13493,12 @@ msgstr "Konfigurationspaket: " msgid " updated to " msgstr " uppdaterat till " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Kontrollera ej sparade ändringar innan konfigureringen uppdateras." + msgid "Open G-code file:" msgstr "Öppna G-kod fil:" @@ -15218,6 +15252,9 @@ msgstr "Justerade Rätlinjig" msgid "Concentric" msgstr "Koncentrisk" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Hilbert kurvan" @@ -15313,29 +15350,21 @@ msgstr "" msgid "Top surface fill order" msgstr "Fyllordning för ovansidan" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Riktning i vilken ovansidor fylls när ett centrumbaserat mönster används (Koncentrisk, Arkimediska kordor, Oktagramspiral).\n" -"Utåt börjar i ytans mitt, så att överskottsmaterial trycks ut mot kanten där det syns minst. Inåt börjar vid kanten och slutar med de trånga kurvorna i mitten.\n" -"Standard använder ordning efter kortaste väg, vilken kan gå i endera riktningen." # AI Translated msgid "Bottom surface fill order" msgstr "Fyllordning för undersidan" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Riktning i vilken undersidor fylls när ett centrumbaserat mönster används (Koncentrisk, Arkimediska kordor, Oktagramspiral).\n" -"Inåt börjar varje yta med de bredare yttre kurvorna, vilket förbättrar det första lagrets vidhäftning på byggplattor där de trånga kurvorna i mitten kanske inte fastnar. Utåt börjar i mitten och trycker överskottsmaterial mot kanten.\n" -"Standard använder ordning efter kortaste väg, vilken kan gå i endera riktningen." msgid "Internal solid infill pattern" msgstr "Invändigt mönster för fyllning av solida ytor" @@ -24380,6 +24409,33 @@ msgstr "" "Undvik vridning\n" "Visste du att när du skriver ut material som är benägna att vrida, såsom ABS, kan en lämplig ökning av värmebäddens temperatur minska sannolikheten för vridning?" +#~ msgid "Select the language" +#~ msgstr "Välj språk" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "Öppna snabbvalsmenyn för åtgärder" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Riktning i vilken ovansidor fylls när ett centrumbaserat mönster används (Koncentrisk, Arkimediska kordor, Oktagramspiral).\n" +#~ "Utåt börjar i ytans mitt, så att överskottsmaterial trycks ut mot kanten där det syns minst. Inåt börjar vid kanten och slutar med de trånga kurvorna i mitten.\n" +#~ "Standard använder ordning efter kortaste väg, vilken kan gå i endera riktningen." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Riktning i vilken undersidor fylls när ett centrumbaserat mönster används (Koncentrisk, Arkimediska kordor, Oktagramspiral).\n" +#~ "Inåt börjar varje yta med de bredare yttre kurvorna, vilket förbättrar det första lagrets vidhäftning på byggplattor där de trånga kurvorna i mitten kanske inte fastnar. Utåt börjar i mitten och trycker överskottsmaterial mot kanten.\n" +#~ "Standard använder ordning efter kortaste väg, vilken kan gå i endera riktningen." + # AI Translated #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "Liveview i Wayland kräver GStreamers GTK-videosink. Installera insticksmodulen gtksink för GStreamer och starta sedan om OrcaSlicer." diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po index ee7430015e..9c0bab2947 100644 --- a/localization/i18n/th/OrcaSlicer_th.po +++ b/localization/i18n/th/OrcaSlicer_th.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: 2026-06-19 13:40+0700\n" "Last-Translator: Icezaza\n" "Language-Team: Thai\n" @@ -2456,13 +2456,6 @@ msgstr "มีอัปเดตพร้อมใช้งาน เปิด msgid "%s has been removed." msgstr "ลบ %s แล้ว" - -msgid "Select the language" -msgstr "เลือกภาษา" - -msgid "Language" -msgstr "ภาษา" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2500,6 +2493,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "ไม่สามารถเปิดกล่องโต้ตอบปลั๊กอินได้ (ข้อผิดพลาดที่ไม่รู้จัก)" +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "เทอร์มินัลปลั๊กอิน" @@ -4582,6 +4588,12 @@ msgstr "" "การคัดลอก G-code ชั่วคราวไปยังเอาต์พุต G-code ล้มเหลว บางทีการ์ด SD อาจถูกล็อคการเขียน?\n" "ข้อความแสดงข้อผิดพลาด: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "การคัดลอก G-code ชั่วคราวไปยังเอาต์พุต G-code ล้มเหลว อาจมีปัญหากับอุปกรณ์เป้าหมาย โปรดลองส่งออกอีกครั้งหรือใช้อุปกรณ์อื่น G-code เอาต์พุตที่เสียหายอยู่ที่ %1%.tmp" @@ -6482,6 +6494,9 @@ msgstr "แสดงเค้าร่างรอบๆ วัตถุที msgid "Preferences" msgstr "การตั้งค่า" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8756,7 +8771,6 @@ msgstr "ต้องการดำเนินการต่อหรือไ msgid "Language selection" msgstr "การเลือกภาษา" - msgid "Asia-Pacific" msgstr "เอเชียแปซิฟิก" @@ -8861,6 +8875,9 @@ msgstr "เส้นทางอินสแตนซ์ปัจจุบัน msgid "General" msgstr "ทั่วไป" +msgid "Language" +msgstr "ภาษา" + msgid "Metric" msgstr "เมตริก" @@ -9065,6 +9082,12 @@ msgstr "การจัดการอุปกรณ์หลายเครื msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "เมื่อเปิดใช้งานตัวเลือกนี้ คุณสามารถส่งงานไปยังอุปกรณ์หลายเครื่องพร้อมกันและจัดการอุปกรณ์หลายเครื่องได้" +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "ปรากฏขึ้นเพื่อเลือกโหมดการจัดกลุ่มเส้นพลาสติก" @@ -9776,6 +9799,9 @@ msgstr "คัดลอกค่าที่สืบทอดมาจากพ msgid "Detach from parent" msgstr "แยกออกจากพรีเซ็ตแม่" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "พรีเซ็ตอิสระ" @@ -11664,6 +11690,17 @@ msgstr "แสดง/ซ่อนกล่องโต้ตอบการต msgid "Switch table page" msgstr "สลับหน้าตาราง" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Space" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "แสดงรายการแป้นพิมพ์ลัด" @@ -11824,15 +11861,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "สลับระหว่างการเตรียม/ดูตัวอย่าง" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Space" - -# AI Translated -msgid "Open actions speed dial" -msgstr "เปิดสปีดไดอัลการดำเนินการ" - msgid "Plater" msgstr "เพลเตอร์" @@ -12100,8 +12128,8 @@ msgstr "ยกเลิกการซ่อมแล้ว" msgid "Copying of file %1% to %2% failed: %3%" msgstr "การคัดลอกไฟล์ %1% ถึง %2% ล้มเหลว: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "โปรดตรวจสอบการเปลี่ยนแปลงที่ยังไม่ได้บันทึกก่อนอัปเดตการกำหนดค่า" +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "แพคเกจการกำหนดค่า:" @@ -12109,6 +12137,12 @@ msgstr "แพคเกจการกำหนดค่า:" msgid " updated to " msgstr "อัปเดตเป็น" +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "โปรดตรวจสอบการเปลี่ยนแปลงที่ยังไม่ได้บันทึกก่อนอัปเดตการกำหนดค่า" + msgid "Open G-code file:" msgstr "เปิดไฟล์ G-code:" @@ -13674,6 +13708,9 @@ msgstr "จัดแนวเป็นเส้นตรง" msgid "Concentric" msgstr "ศูนย์กลาง" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "ฮิลเบิร์ต เคิร์ฟ" @@ -13763,29 +13800,21 @@ msgstr "" msgid "Top surface fill order" msgstr "ลำดับการเติมพื้นผิวด้านบน" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"ทิศทางที่พื้นผิวด้านบนถูกเติมเมื่อใช้ลวดลายแบบอิงจุดศูนย์กลาง (Concentric, Archimedean Chords, Octagram Spiral)\n" -"ออกด้านนอกเริ่มที่กึ่งกลางของพื้นผิว ดังนั้นวัสดุส่วนเกินจะถูกดันไปทางขอบซึ่งมองเห็นได้น้อยที่สุด เข้าด้านในเริ่มที่ขอบและจบด้วยเส้นโค้งแคบที่กึ่งกลาง\n" -"ค่าเริ่มต้นใช้การเรียงลำดับเส้นทางสั้นที่สุด ซึ่งอาจทำงานในทิศทางใดก็ได้" # AI Translated msgid "Bottom surface fill order" msgstr "ลำดับการเติมพื้นผิวด้านล่าง" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"ทิศทางที่พื้นผิวด้านล่างถูกเติมเมื่อใช้ลวดลายแบบอิงจุดศูนย์กลาง (Concentric, Archimedean Chords, Octagram Spiral)\n" -"เข้าด้านในเริ่มแต่ละพื้นผิวด้วยเส้นโค้งด้านนอกที่กว้างกว่า ซึ่งช่วยเพิ่มการยึดเกาะเลเยอร์แรกบนฐานพิมพ์ที่เส้นโค้งแคบตรงกลางอาจไม่ติด ออกด้านนอกเริ่มที่กึ่งกลาง โดยดันวัสดุส่วนเกินไปทางขอบ\n" -"ค่าเริ่มต้นใช้การเรียงลำดับเส้นทางสั้นที่สุด ซึ่งอาจทำงานในทิศทางใดก็ได้" msgid "Internal solid infill pattern" msgstr "รูปแบบไส้ในของแข็งภายใน" @@ -21680,6 +21709,33 @@ msgstr "" "หลีกเลี่ยงการบิดเบี้ยว\n" "คุณรู้หรือไม่ว่าเมื่อพิมพ์วัสดุที่มีแนวโน้มที่จะเกิดการบิดเบี้ยว เช่น ABS การเพิ่มอุณหภูมิฐานพิมพ์อย่างเหมาะสมสามารถลดความน่าจะเป็นของการบิดเบี้ยวได้" +#~ msgid "Select the language" +#~ msgstr "เลือกภาษา" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "เปิดสปีดไดอัลการดำเนินการ" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "ทิศทางที่พื้นผิวด้านบนถูกเติมเมื่อใช้ลวดลายแบบอิงจุดศูนย์กลาง (Concentric, Archimedean Chords, Octagram Spiral)\n" +#~ "ออกด้านนอกเริ่มที่กึ่งกลางของพื้นผิว ดังนั้นวัสดุส่วนเกินจะถูกดันไปทางขอบซึ่งมองเห็นได้น้อยที่สุด เข้าด้านในเริ่มที่ขอบและจบด้วยเส้นโค้งแคบที่กึ่งกลาง\n" +#~ "ค่าเริ่มต้นใช้การเรียงลำดับเส้นทางสั้นที่สุด ซึ่งอาจทำงานในทิศทางใดก็ได้" + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "ทิศทางที่พื้นผิวด้านล่างถูกเติมเมื่อใช้ลวดลายแบบอิงจุดศูนย์กลาง (Concentric, Archimedean Chords, Octagram Spiral)\n" +#~ "เข้าด้านในเริ่มแต่ละพื้นผิวด้วยเส้นโค้งด้านนอกที่กว้างกว่า ซึ่งช่วยเพิ่มการยึดเกาะเลเยอร์แรกบนฐานพิมพ์ที่เส้นโค้งแคบตรงกลางอาจไม่ติด ออกด้านนอกเริ่มที่กึ่งกลาง โดยดันวัสดุส่วนเกินไปทางขอบ\n" +#~ "ค่าเริ่มต้นใช้การเรียงลำดับเส้นทางสั้นที่สุด ซึ่งอาจทำงานในทิศทางใดก็ได้" + #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "Native Wayland liveview ต้องใช้ GStreamer GTK video sink โปรดติดตั้งปลั๊กอิน gtksink สำหรับ GStreamer จากนั้นรีสตาร์ท OrcaSlicer" diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index 0cf9d57412..864bd15610 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: 2026-08-21 23:18+0300\n" "Last-Translator: GlauTech\n" "Language-Team: \n" @@ -2480,13 +2480,6 @@ msgstr "Kullanılabilir bir güncelleme var. Güncellemek için ön ayar paketi msgid "%s has been removed." msgstr "%s kaldırıldı." - -msgid "Select the language" -msgstr "Dili seçin" - -msgid "Language" -msgstr "Dil" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2524,6 +2517,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Eklentiler iletişim kutusu açılamadı (bilinmeyen hata)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "Eklenti Terminali" @@ -4641,6 +4647,12 @@ msgstr "" "Geçici G-code'un çıkış G-code'a kopyalanması başarısız oldu. Belki SD kart yazma kilitlidir.\n" "Hata mesajı: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "Geçici G-code'un çıkış G-code'a kopyalanması başarısız oldu. Hedef cihazda sorun olabilir, lütfen tekrar dışa aktarmayı veya farklı bir cihaz kullanmayı deneyin. Bozuk çıktı G-code %1%.tmp konumunda." @@ -6550,6 +6562,9 @@ msgstr "3D sahnede seçilen nesnenin etrafındaki ana hatları göster." msgid "Preferences" msgstr "Tercihler" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8858,7 +8873,6 @@ msgstr "Devam etmek istiyor musun?" msgid "Language selection" msgstr "Dil seçimi" - msgid "Asia-Pacific" msgstr "Asya Pasifik" @@ -8963,6 +8977,9 @@ msgstr "Mevcut Örnek Yolu: " msgid "General" msgstr "Genel" +msgid "Language" +msgstr "Dil" + msgid "Metric" msgstr "Metrik" @@ -9170,6 +9187,12 @@ msgstr "Çoklu cihaz yönetimi" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Bu seçenek etkinleştirildiğinde, aynı anda birden fazla cihaza bir görev gönderebilir ve birden fazla cihazı yönetebilirsiniz." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Filament gruplama modunu seçmek için açılır pencere" @@ -9920,6 +9943,9 @@ msgstr "Üst ön ayardan devralınan tüm değerleri bu ön ayara kopyalar ve ü msgid "Detach from parent" msgstr "Ebeveynden ayrıl" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Bağımsız ön ayar" @@ -11850,6 +11876,17 @@ msgstr "3Dconnexion cihazları ayarları iletişim kutusunu Göster/Gizle" msgid "Switch table page" msgstr "Tablo sayfasını değiştir" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Boşluk" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Klavye kısayolları listesini göster" @@ -12012,15 +12049,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Hazırlama/Önizleme arasında geçiş yap" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Boşluk" - -# AI Translated -msgid "Open actions speed dial" -msgstr "Eylem hızlı erişim menüsünü aç" - msgid "Plater" msgstr "Plakacı" @@ -12295,8 +12323,8 @@ msgstr "Onarım iptal edildi" msgid "Copying of file %1% to %2% failed: %3%" msgstr "%1% dosyasının %2% dosyasına kopyalanması başarısız oldu: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Yapılandırma güncellemelerinden önce kaydedilmemiş değişiklikleri kontrol etmeniz gerekir." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Yapılandırma paketi: " @@ -12304,6 +12332,12 @@ msgstr "Yapılandırma paketi: " msgid " updated to " msgstr " güncellendi " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Yapılandırma güncellemelerinden önce kaydedilmemiş değişiklikleri kontrol etmeniz gerekir." + msgid "Open G-code file:" msgstr "G-code dosyasını açın:" @@ -13922,6 +13956,9 @@ msgstr "Hizalanmış doğrusal" msgid "Concentric" msgstr "Konsantrik" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Hilbert eğrisi" @@ -14011,29 +14048,21 @@ msgstr "" msgid "Top surface fill order" msgstr "Üst yüzey doldurma sırası" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Merkez tabanlı bir desen (Konsantrik, Arşimet akorları, Sekizgen spiral) kullanılırken üst yüzeylerin doldurulma yönü.\n" -"Dışarı, yüzeyin merkezinden başlar; böylece fazla malzeme en az göründüğü kenara doğru itilir. İçeri kenardan başlar ve merkezdeki dar kavislerle biter.\n" -"Varsayılan, her iki yönde de ilerleyebilen en kısa yol sıralamasını kullanır." # AI Translated msgid "Bottom surface fill order" msgstr "Alt yüzey doldurma sırası" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Merkez tabanlı bir desen (Konsantrik, Arşimet akorları, Sekizgen spiral) kullanılırken alt yüzeylerin doldurulma yönü.\n" -"İçeri, her yüzeye daha geniş dış kavislerle başlar; bu da merkezdeki dar kavislerin yapışmayabileceği yapı plakalarında ilk katman yapışmasını iyileştirir. Dışarı merkezden başlar ve fazla malzemeyi kenara doğru iter.\n" -"Varsayılan, her iki yönde de ilerleyebilen en kısa yol sıralamasını kullanır." msgid "Internal solid infill pattern" msgstr "İç katı dolgu deseni" @@ -22135,6 +22164,33 @@ msgstr "" "Eğilmeyi önleyin\n" "ABS gibi bükülmeye yatkın malzemelere baskı yaparken, ısıtma yatağı sıcaklığının uygun şekilde arttırılmasının bükülme olasılığını azaltabileceğini biliyor muydunuz?" +#~ msgid "Select the language" +#~ msgstr "Dili seçin" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "Eylem hızlı erişim menüsünü aç" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Merkez tabanlı bir desen (Konsantrik, Arşimet akorları, Sekizgen spiral) kullanılırken üst yüzeylerin doldurulma yönü.\n" +#~ "Dışarı, yüzeyin merkezinden başlar; böylece fazla malzeme en az göründüğü kenara doğru itilir. İçeri kenardan başlar ve merkezdeki dar kavislerle biter.\n" +#~ "Varsayılan, her iki yönde de ilerleyebilen en kısa yol sıralamasını kullanır." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Merkez tabanlı bir desen (Konsantrik, Arşimet akorları, Sekizgen spiral) kullanılırken alt yüzeylerin doldurulma yönü.\n" +#~ "İçeri, her yüzeye daha geniş dış kavislerle başlar; bu da merkezdeki dar kavislerin yapışmayabileceği yapı plakalarında ilk katman yapışmasını iyileştirir. Dışarı merkezden başlar ve fazla malzemeyi kenara doğru iter.\n" +#~ "Varsayılan, her iki yönde de ilerleyebilen en kısa yol sıralamasını kullanır." + # AI Translated #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "Yerel Wayland canlı görüntüsü, GStreamer GTK video alıcısını gerektirir. Lütfen GStreamer için gtksink eklentisini yükleyin ve ardından OrcaSlicer'ı yeniden başlatın." diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po index ec9e97bae0..5c352f52dd 100644 --- a/localization/i18n/uk/OrcaSlicer_uk.po +++ b/localization/i18n/uk/OrcaSlicer_uk.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: orcaslicerua\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: 2026-07-17 16:25+0300\n" "Last-Translator: Andrij Mizyk <andm1zyk@proton.me>\n" "Language-Team: Ukrainian\n" @@ -2424,13 +2424,6 @@ msgstr "Доступне оновлення. Відкрийте вікно на msgid "%s has been removed." msgstr "%s вилучено." - -msgid "Select the language" -msgstr "Вибрати мову" - -msgid "Language" -msgstr "Мова" - #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." msgstr "Не вдалося перемкнути Orca Slicer на мову %s." @@ -2465,6 +2458,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Не вдалося відкрити діалогове вікно плагінів (невідома помилка)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "Термінал плагіна" @@ -4577,6 +4583,12 @@ msgstr "" "Не вдалося скопіювати тимчасовий G-код у місцезнаходження вихідного файлу G-коду. Чи може ваша SD карта захищена від запису?\n" "Повідомлення про помилку: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "Не вдалося скопіювати тимчасовий G-код у вихідний G-код. Можливо, проблема з цільовим пристроєм, спробуйте експортувати ще раз або використати інший пристрій. Пошкоджений вихідний G-код - %1% .tmp." @@ -6519,6 +6531,9 @@ msgstr "Показувати контур навколо виділеного о msgid "Preferences" msgstr "Налаштування" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8872,7 +8887,6 @@ msgstr "Ви хочете продовжувати?" msgid "Language selection" msgstr "Вибір мови" - msgid "Asia-Pacific" msgstr "Азіатсько-Тихоокеанський регіон" @@ -8980,6 +8994,9 @@ msgstr "Шлях Поточної Інсталяції: " msgid "General" msgstr "Загальні" +msgid "Language" +msgstr "Мова" + msgid "Metric" msgstr "Метрика" @@ -9188,6 +9205,12 @@ msgstr "Керування кількома пристроями" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "З цією опцією ввімкненою, ви можете відправляти завдання на кілька пристроїв одночасно та керувати декількома пристроями." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + # AI Translated msgid "Pop up to select filament grouping mode" msgstr "Показувати вікно вибору режиму групування філаментів" @@ -9924,6 +9947,9 @@ msgstr "Копіює в цей пресет усі значення, успад msgid "Detach from parent" msgstr "Відʼєднати від батьківського" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Незалежний пресет" @@ -11912,6 +11938,17 @@ msgstr "Показати/приховати діалог налаштувань msgid "Switch table page" msgstr "Перемкнути сторінку таблиці" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Пробіл" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Показати список клавіш" @@ -12074,15 +12111,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Переключення між Підготовка/Попередній перегляд" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Пробіл" - -# AI Translated -msgid "Open actions speed dial" -msgstr "Відкрити панель швидких дій" - msgid "Plater" msgstr "Тарілка" @@ -12353,9 +12381,8 @@ msgstr "Ремонт скасовано" msgid "Copying of file %1% to %2% failed: %3%" msgstr "Копіювання %1% у %2% не вдалося: %3%" -# AI Translated -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Будь ласка, перевірте незбережені зміни перед оновленням конфігурації." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Пакет конфігурації: " @@ -12363,6 +12390,13 @@ msgstr "Пакет конфігурації: " msgid " updated to " msgstr " оновлено до " +msgid "Failed to download vendor profile(s): " +msgstr "" + +# AI Translated +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Будь ласка, перевірте незбережені зміни перед оновленням конфігурації." + msgid "Open G-code file:" msgstr "Відкрити файл G-коду:" @@ -14018,6 +14052,9 @@ msgstr "Вирівняний прямолінійний" msgid "Concentric" msgstr "Концентричний" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Крива Гільберта" @@ -14106,29 +14143,21 @@ msgstr "" msgid "Top surface fill order" msgstr "Порядок заповнення верхньої поверхні" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Напрямок, у якому заповнюються верхні поверхні при використанні шаблону з центром (Концентричний, Хорди Архімеда, Спіральна октограма).\n" -"Назовні починає з центру поверхні, тож надлишок матеріалу виштовхується до краю, де він найменш помітний. Усередину починає з краю та завершується щільними кривими в центрі.\n" -"Типово використовується впорядкування за найкоротшим шляхом, яке може йти в будь-якому напрямку." # AI Translated msgid "Bottom surface fill order" msgstr "Порядок заповнення нижньої поверхні" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Напрямок, у якому заповнюються нижні поверхні при використанні шаблону з центром (Концентричний, Хорди Архімеда, Спіральна октограма).\n" -"Усередину починає кожну поверхню з ширших зовнішніх кривих, що покращує зчеплення першого шару на столах, де щільні криві в центрі можуть не прилипати. Назовні починає з центру, виштовхуючи надлишок матеріалу до краю.\n" -"Типово використовується впорядкування за найкоротшим шляхом, яке може йти в будь-якому напрямку." msgid "Internal solid infill pattern" msgstr "Шаблон внутрішнього суцільного заповнення" @@ -22296,6 +22325,33 @@ msgstr "" "Уникнення деформації\n" "Чи знаєте ви, що при друку матеріалами, схильними до деформації, такими як ABS, відповідне підвищення температури столу може зменшити ймовірність деформації?" +#~ msgid "Select the language" +#~ msgstr "Вибрати мову" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "Відкрити панель швидких дій" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Напрямок, у якому заповнюються верхні поверхні при використанні шаблону з центром (Концентричний, Хорди Архімеда, Спіральна октограма).\n" +#~ "Назовні починає з центру поверхні, тож надлишок матеріалу виштовхується до краю, де він найменш помітний. Усередину починає з краю та завершується щільними кривими в центрі.\n" +#~ "Типово використовується впорядкування за найкоротшим шляхом, яке може йти в будь-якому напрямку." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Напрямок, у якому заповнюються нижні поверхні при використанні шаблону з центром (Концентричний, Хорди Архімеда, Спіральна октограма).\n" +#~ "Усередину починає кожну поверхню з ширших зовнішніх кривих, що покращує зчеплення першого шару на столах, де щільні криві в центрі можуть не прилипати. Назовні починає з центру, виштовхуючи надлишок матеріалу до краю.\n" +#~ "Типово використовується впорядкування за найкоротшим шляхом, яке може йти в будь-якому напрямку." + # AI Translated #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "Нативний перегляд у Wayland потребує відеоприймача GStreamer GTK. Встановіть плагін gtksink для GStreamer, а потім перезапустіть OrcaSlicer." diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po index a80f47cfc1..dedb1d33cb 100644 --- a/localization/i18n/vi/OrcaSlicer_vi.po +++ b/localization/i18n/vi/OrcaSlicer_vi.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: 2025-10-02 17:43+0700\n" "Last-Translator: \n" "Language-Team: hainguyen.ts13@gmail.com\n" @@ -2568,13 +2568,6 @@ msgstr "Có bản cập nhật khả dụng. Hãy mở hộp thoại gói cài msgid "%s has been removed." msgstr "%s đã bị xóa." - -msgid "Select the language" -msgstr "Chọn ngôn ngữ" - -msgid "Language" -msgstr "Ngôn ngữ" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2612,6 +2605,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "Không thể mở hộp thoại Plugin (lỗi không xác định)." +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "Terminal plugin" @@ -4834,6 +4840,12 @@ msgstr "" "Sao chép G-code tạm thời sang G-code đầu ra thất bại. Có thể thẻ SD bị khóa ghi?\n" "Thông báo lỗi: %1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "Sao chép G-code tạm thời sang G-code đầu ra thất bại. Có thể có vấn đề với thiết bị đích, vui lòng thử xuất lại hoặc dùng thiết bị khác. G-code đầu ra bị hỏng ở %1%.tmp." @@ -6871,6 +6883,9 @@ msgstr "Hiện đường viền xung quanh vật thể đã chọn trong cảnh msgid "Preferences" msgstr "Tùy chọn" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -9307,7 +9322,6 @@ msgstr "Bạn có muốn tiếp tục?" msgid "Language selection" msgstr "Chọn ngôn ngữ" - msgid "Asia-Pacific" msgstr "Châu Á-Thái Bình Dương" @@ -9423,6 +9437,9 @@ msgstr "Đường dẫn phiên bản hiện tại: " msgid "General" msgstr "Chung" +msgid "Language" +msgstr "Ngôn ngữ" + msgid "Metric" msgstr "Hệ mét" @@ -9651,6 +9668,12 @@ msgstr "Quản lý nhiều thiết bị" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "Với tùy chọn này được bật, bạn có thể gửi tác vụ đến nhiều thiết bị cùng lúc và quản lý nhiều thiết bị." +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + # AI Translated msgid "Pop up to select filament grouping mode" msgstr "Hiện cửa sổ để chọn chế độ nhóm filament" @@ -10437,6 +10460,9 @@ msgstr "Sao chép tất cả các giá trị kế thừa từ preset cha vào pr msgid "Detach from parent" msgstr "Tách khỏi vật thể cha" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "Preset độc lập" @@ -12465,6 +12491,17 @@ msgstr "Hiển thị/Ẩn hộp thoại cài đặt thiết bị 3Dconnexion" msgid "Switch table page" msgstr "Chuyển trang bảng" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Space" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "Hiển thị danh sách phím tắt" @@ -12631,15 +12668,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "Chuyển đổi giữa Chuẩn bị/Xem trước" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Space" - -# AI Translated -msgid "Open actions speed dial" -msgstr "Mở vòng quay thao tác nhanh" - msgid "Plater" msgstr "Bàn in" @@ -12925,8 +12953,8 @@ msgstr "Sửa chữa đã hủy" msgid "Copying of file %1% to %2% failed: %3%" msgstr "Sao chép file %1% sang %2% thất bại: %3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "Cần kiểm tra các thay đổi chưa lưu trước khi cập nhật cấu hình." +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "Gói cấu hình: " @@ -12934,6 +12962,12 @@ msgstr "Gói cấu hình: " msgid " updated to " msgstr " đã cập nhật lên " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "Cần kiểm tra các thay đổi chưa lưu trước khi cập nhật cấu hình." + msgid "Open G-code file:" msgstr "Mở file G-code:" @@ -14585,6 +14619,9 @@ msgstr "Thẳng hàng căn chỉnh" msgid "Concentric" msgstr "Đồng tâm" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "Đường cong Hilbert" @@ -14674,29 +14711,21 @@ msgstr "" msgid "Top surface fill order" msgstr "Thứ tự lấp bề mặt trên" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Hướng lấp các bề mặt trên khi dùng mẫu dựa trên tâm (Đồng tâm, Dây cung Archimedes, Xoắn ốc bát giác).\n" -"Ra ngoài bắt đầu từ tâm bề mặt, nhờ đó vật liệu dư bị đẩy về phía mép nơi ít nhìn thấy nhất. Vào trong bắt đầu từ mép và kết thúc bằng các đường cong hẹp ở tâm.\n" -"Mặc định dùng thứ tự đường đi ngắn nhất, có thể chạy theo hướng bất kỳ." # AI Translated msgid "Bottom surface fill order" msgstr "Thứ tự lấp bề mặt dưới" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Hướng lấp các bề mặt dưới khi dùng mẫu dựa trên tâm (Đồng tâm, Dây cung Archimedes, Xoắn ốc bát giác).\n" -"Vào trong bắt đầu mỗi bề mặt bằng các đường cong ngoài rộng hơn, giúp cải thiện độ bám lớp đầu tiên trên những bàn in mà các đường cong hẹp ở tâm có thể không dính. Ra ngoài bắt đầu từ tâm, đẩy vật liệu dư về phía mép.\n" -"Mặc định dùng thứ tự đường đi ngắn nhất, có thể chạy theo hướng bất kỳ." msgid "Internal solid infill pattern" msgstr "Mẫu infill đặc bên trong" @@ -23019,6 +23048,33 @@ msgstr "" "Tránh cong vênh\n" "Bạn có biết rằng khi in vật liệu dễ cong vênh như ABS, tăng nhiệt độ bàn nóng một cách thích hợp có thể giảm xác suất cong vênh không?" +#~ msgid "Select the language" +#~ msgstr "Chọn ngôn ngữ" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "Mở vòng quay thao tác nhanh" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Hướng lấp các bề mặt trên khi dùng mẫu dựa trên tâm (Đồng tâm, Dây cung Archimedes, Xoắn ốc bát giác).\n" +#~ "Ra ngoài bắt đầu từ tâm bề mặt, nhờ đó vật liệu dư bị đẩy về phía mép nơi ít nhìn thấy nhất. Vào trong bắt đầu từ mép và kết thúc bằng các đường cong hẹp ở tâm.\n" +#~ "Mặc định dùng thứ tự đường đi ngắn nhất, có thể chạy theo hướng bất kỳ." + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "Hướng lấp các bề mặt dưới khi dùng mẫu dựa trên tâm (Đồng tâm, Dây cung Archimedes, Xoắn ốc bát giác).\n" +#~ "Vào trong bắt đầu mỗi bề mặt bằng các đường cong ngoài rộng hơn, giúp cải thiện độ bám lớp đầu tiên trên những bàn in mà các đường cong hẹp ở tâm có thể không dính. Ra ngoài bắt đầu từ tâm, đẩy vật liệu dư về phía mép.\n" +#~ "Mặc định dùng thứ tự đường đi ngắn nhất, có thể chạy theo hướng bất kỳ." + # AI Translated #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "Xem trực tiếp trên Wayland thuần cần GStreamer GTK video sink. Vui lòng cài đặt plugin gtksink cho GStreamer, sau đó khởi động lại OrcaSlicer." diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po index faa3419ae5..cfacff0458 100644 --- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po +++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Slic3rPE\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: 2026-06-11 12:37-0300\n" "Last-Translator: Handle <mail@bysb.net>\n" "Language-Team: \n" @@ -2361,13 +2361,6 @@ msgstr "有更新可用。打开预设包对话框进行更新。" msgid "%s has been removed." msgstr "%s 已被移除。" - -msgid "Select the language" -msgstr "选择语言" - -msgid "Language" -msgstr "语言" - #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." msgstr "切换 Orca Slicer 语言到 %s 失败。" @@ -2400,6 +2393,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "无法打开插件对话框(未知错误)。" +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + msgid "Plugin Terminal" msgstr "插件终端" @@ -4437,6 +4443,12 @@ msgstr "" "将临时 G-Code 复制到输出 G-Code 失败。也许 SD 卡被写锁定了?\n" "错误消息:%1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "将临时 G-Code 复制到输出 G-Code 失败。目标设备可能有问题,请再次尝试导出或使用其他设备。损坏的输出 G-Code 在 %1%.tmp。" @@ -6337,6 +6349,9 @@ msgstr "在3D场景中显示选中对象的轮廓" msgid "Preferences" msgstr "偏好设置" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8585,7 +8600,6 @@ msgstr "是否继续?" msgid "Language selection" msgstr "语言选择" - msgid "Asia-Pacific" msgstr "亚太" @@ -8683,6 +8697,9 @@ msgstr "当前实例路径" msgid "General" msgstr "常规" +msgid "Language" +msgstr "语言" + msgid "Metric" msgstr "公制(Metric)" @@ -8887,6 +8904,12 @@ msgstr "多设备管理" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "启用此选项后,您可以同时向多个设备发送任务并管理多个设备。" +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "弹出选择耗材丝分组模式" @@ -9598,6 +9621,9 @@ msgstr "将父预设继承的所有数值复制到当前预设,并解除继承 msgid "Detach from parent" msgstr "与父级分离" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "独立预设" @@ -11453,6 +11479,17 @@ msgstr "显示/隐藏 3Dconnexion设备的设置对话框" msgid "Switch table page" msgstr "切换标签页" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Space" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "显示键盘快捷键列表" @@ -11620,15 +11657,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "准备/预览之间的切换" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Space" - -# AI Translated -msgid "Open actions speed dial" -msgstr "打开快捷操作盘" - msgid "Plater" msgstr "准备" @@ -11896,8 +11924,8 @@ msgstr "修复被取消" msgid "Copying of file %1% to %2% failed: %3%" msgstr "从%1%拷贝文件到%2%失败:%3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "需要在配置更新之前检查没有保存的参数修改。" +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "配置包:" @@ -11905,6 +11933,12 @@ msgstr "配置包:" msgid " updated to " msgstr "更新到" +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "需要在配置更新之前检查没有保存的参数修改。" + msgid "Open G-code file:" msgstr "打开G-code文件:" @@ -13431,6 +13465,9 @@ msgstr "直线排列" msgid "Concentric" msgstr "同心" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "希尔伯特曲线" @@ -13524,29 +13561,21 @@ msgstr "" msgid "Top surface fill order" msgstr "顶面填充顺序" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"使用基于中心的图案(同心、阿基米德和弦、八角螺旋)时填充顶面的方向。\n" -"向外从表面中心开始,因此多余的材料会被推向边缘等最不显眼的位置。向内从边缘开始,并以中心处的紧密曲线结束。\n" -"默认使用最短路径排序,可能沿任一方向进行。" # AI Translated msgid "Bottom surface fill order" msgstr "底面填充顺序" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"使用基于中心的图案(同心、阿基米德和弦、八角螺旋)时填充底面的方向。\n" -"向内以较宽的外侧曲线开始每个表面,这可以改善在热床上的首层附着,因为中心处的紧密曲线可能无法粘牢。向外从中心开始,将多余的材料推向边缘。\n" -"默认使用最短路径排序,可能沿任一方向进行。" msgid "Internal solid infill pattern" msgstr "内部实心填充图案" @@ -21428,6 +21457,33 @@ msgstr "" "避免翘曲\n" "您知道吗?打印ABS这类易翘曲材料时,适当提高热床温度可以降低翘曲的概率。" +#~ msgid "Select the language" +#~ msgstr "选择语言" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "打开快捷操作盘" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "使用基于中心的图案(同心、阿基米德和弦、八角螺旋)时填充顶面的方向。\n" +#~ "向外从表面中心开始,因此多余的材料会被推向边缘等最不显眼的位置。向内从边缘开始,并以中心处的紧密曲线结束。\n" +#~ "默认使用最短路径排序,可能沿任一方向进行。" + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "使用基于中心的图案(同心、阿基米德和弦、八角螺旋)时填充底面的方向。\n" +#~ "向内以较宽的外侧曲线开始每个表面,这可以改善在热床上的首层附着,因为中心处的紧密曲线可能无法粘牢。向外从中心开始,将多余的材料推向边缘。\n" +#~ "默认使用最短路径排序,可能沿任一方向进行。" + #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "原生 Wayland 实时画面需要 GStreamer GTK 视频接收器。请安装 GStreamer 的 gtksink 插件,然后重启 OrcaSlicer。" diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po index 8f17cfdc5d..fc24dcfaa2 100644 --- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po +++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-03 10:27+0800\n" +"POT-Creation-Date: 2026-09-11 12:33+0800\n" "PO-Revision-Date: 2025-11-28 13:48-0600\n" "Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n" "Language-Team: \n" @@ -2425,13 +2425,6 @@ msgstr "有可用的更新。請開啟預設組合對話框進行更新。" msgid "%s has been removed." msgstr "%s 已移除。" - -msgid "Select the language" -msgstr "選擇語言" - -msgid "Language" -msgstr "語言" - # AI Translated #, c-format, boost-format msgid "Switching Orca Slicer to language %s failed." @@ -2469,6 +2462,19 @@ msgstr "" msgid "Failed to open the Plugins dialog (unknown error)." msgstr "無法開啟外掛對話框(未知錯誤)。" +msgid "Plugins refreshed." +msgstr "" + +#, c-format, boost-format +msgid "Failed to refresh plugins: %s" +msgstr "" + +msgid "Select plugin package" +msgstr "" + +msgid "Plugin files (*.py;*.whl)|*.py;*.whl" +msgstr "" + # AI Translated msgid "Plugin Terminal" msgstr "外掛終端機" @@ -4550,6 +4556,12 @@ msgstr "" "錯誤訊息:%1%將臨時的 G-code 複製到輸出的 G-code 失敗 ,也許 SD 卡寫入被鎖定?\n" "錯誤訊息:%1%" +#, boost-format +msgid "" +"Copying of the temporary G-code to the output G-code failed.\n" +"Error message: %1%" +msgstr "" + #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." msgstr "將臨時 G-code 複製到輸出 G-code 時失敗。目標裝置可能存在問題,請嘗試再次匯出或使用不同的裝置。損壞的 G-code 已輸出為 %1%.tmp。將臨時 G-code 複製到輸出 G-code 時失敗。目標裝置可能存在問題,請嘗試再次匯出或使用不同的裝置。損壞的 G-code 已輸出為 %1%.tmp。" @@ -6467,6 +6479,9 @@ msgstr "在 3D 場景中顯示選定物件的輪廓" msgid "Preferences" msgstr "偏好設定" +msgid "Open speed dial..." +msgstr "" + # AI Translated msgctxt "Menu" msgid "Edit" @@ -8751,7 +8766,6 @@ msgstr "是否繼續?" msgid "Language selection" msgstr "語言選擇" - msgid "Asia-Pacific" msgstr "亞太" @@ -8856,6 +8870,9 @@ msgstr "目前實例路徑:" msgid "General" msgstr "一般" +msgid "Language" +msgstr "語言" + msgid "Metric" msgstr "公制" @@ -9060,6 +9077,12 @@ msgstr "多臺裝置管理" msgid "With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices." msgstr "啟用時可以同時傳送到並管理多個機臺。" +msgid "Open the Speed Dial with the Space key" +msgstr "" + +msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "彈出視窗選擇線材分組模式" @@ -9771,6 +9794,9 @@ msgstr "將父配置繼承的所有數值複製到目前的配置,並解除繼 msgid "Detach from parent" msgstr "從父預設分離" +msgid "Save without parent" +msgstr "" + # AI Translated msgid "Unique preset" msgstr "獨立配置" @@ -11661,6 +11687,17 @@ msgstr "顯示/隱藏 3Dconnexion 裝置的設定對話框" msgid "Switch table page" msgstr "切換表單頁面" +# AI Translated +msgctxt "Keyboard Shortcut" +msgid "Space" +msgstr "Space" + +msgid "Open speed dial" +msgstr "" + +msgid "Run a Speed Dial favourite (while the Speed Dial is open)" +msgstr "" + msgid "Show keyboard shortcuts list" msgstr "顯示鍵盤快速鍵清單" @@ -11825,15 +11862,6 @@ msgstr "Tab" msgid "Switch between Prepare/Preview" msgstr "在準備/預覽模式之中切換" -# AI Translated -msgctxt "Keyboard Shortcut" -msgid "Space" -msgstr "Space" - -# AI Translated -msgid "Open actions speed dial" -msgstr "開啟操作快捷選單" - msgid "Plater" msgstr "準備" @@ -12100,8 +12128,8 @@ msgstr "修復被取消" msgid "Copying of file %1% to %2% failed: %3%" msgstr "從 %1% 複製檔案到 %2% 失敗:%3%" -msgid "Please check any unsaved changes before updating the configuration." -msgstr "在設定更新之前需要檢查未儲存的設定變更。" +msgid "Downloading new vendor profile(s): " +msgstr "" msgid "Configuration package: " msgstr "設定檔:" @@ -12109,6 +12137,12 @@ msgstr "設定檔:" msgid " updated to " msgstr "更新到 " +msgid "Failed to download vendor profile(s): " +msgstr "" + +msgid "Please check any unsaved changes before updating the configuration." +msgstr "在設定更新之前需要檢查未儲存的設定變更。" + msgid "Open G-code file:" msgstr "開啟 G-code 檔案:" @@ -13646,6 +13680,9 @@ msgstr "直線排列" msgid "Concentric" msgstr "同心" +msgid "Spiral Inset" +msgstr "" + msgid "Hilbert Curve" msgstr "希爾伯特曲線" @@ -13735,29 +13772,21 @@ msgstr "" msgid "Top surface fill order" msgstr "頂面填充順序" -# AI Translated msgid "" -"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which top surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"使用以中心為基礎的圖樣(同心、阿基米德弦線、八角星螺旋)時,頂面填充的方向。\n" -"「向外」從表面中心開始,因此多餘的材料會被推向最不明顯的邊緣。「向內」從邊緣開始,並以中心的緊密曲線結束。\n" -"預設使用最短路徑排序,方向可能為任一種。" # AI Translated msgid "Bottom surface fill order" msgstr "底面填充順序" -# AI Translated msgid "" -"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +"Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"使用以中心為基礎的圖樣(同心、阿基米德弦線、八角星螺旋)時,底面填充的方向。\n" -"「向內」讓每個表面從較寬的外側曲線開始,可改善中心緊密曲線可能無法附著的列印板上的第一層附著。「向外」從中心開始,將多餘的材料推向邊緣。\n" -"預設使用最短路徑排序,方向可能為任一種。" msgid "Internal solid infill pattern" msgstr "內部實心填充圖案" @@ -21642,6 +21671,33 @@ msgstr "" "避免翹曲\n" "您知道嗎?當列印容易翹曲的材料(如 ABS)時,適當提高熱床溫度可以降低翹曲的機率。" +#~ msgid "Select the language" +#~ msgstr "選擇語言" + +# AI Translated +#~ msgid "Open actions speed dial" +#~ msgstr "開啟操作快捷選單" + +# AI Translated +#~ msgid "" +#~ "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "使用以中心為基礎的圖樣(同心、阿基米德弦線、八角星螺旋)時,頂面填充的方向。\n" +#~ "「向外」從表面中心開始,因此多餘的材料會被推向最不明顯的邊緣。「向內」從邊緣開始,並以中心的緊密曲線結束。\n" +#~ "預設使用最短路徑排序,方向可能為任一種。" + +# AI Translated +#~ msgid "" +#~ "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" +#~ "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" +#~ "Default uses shortest-path ordering, which may run in either direction." +#~ msgstr "" +#~ "使用以中心為基礎的圖樣(同心、阿基米德弦線、八角星螺旋)時,底面填充的方向。\n" +#~ "「向內」讓每個表面從較寬的外側曲線開始,可改善中心緊密曲線可能無法附著的列印板上的第一層附著。「向外」從中心開始,將多餘的材料推向邊緣。\n" +#~ "預設使用最短路徑排序,方向可能為任一種。" + #~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." #~ msgstr "原生 Wayland 即時檢視需要 GStreamer GTK 視訊接收器。請為 GStreamer 安裝 gtksink 外掛程式,然後重新啟動 OrcaSlicer。" diff --git a/resources/web/data/text.js b/resources/web/data/text.js index 95342acdaf..1a17c5ccdb 100644 --- a/resources/web/data/text.js +++ b/resources/web/data/text.js @@ -118,6 +118,36 @@ var LangText = { orca10: "Not connected", orca11: "Connected", orca12: "Note: When Stealth Mode is enabled, your user profiles will not be backed up to Orca Cloud.", + sd_search: "Search actions", + sd_clear: "Clear", + sd_search_n: "Search %s actions", + sd_showing: "Showing", + sd_of: "of", + sd_actions: "actions", + sd_recent: "recent", + sd_matches: "matches", + sd_tabs: "tabs", + sd_no_match: "No actions match", + sd_total: "Total", + sd_no_actions: "No actions yet", + sd_no_tabs_match: "No tabs match", + sd_no_tabs: "No tabs", + sd_go_to_pct: "Go to %s% of the layer range", + sd_enter_pct: "Enter a layer percentage (0-100)", + sd_go_layer_ph: "Go to layer % (0-100)", + sd_go_tab_ph: "Go to tab", + sd_favs_full: "Favourites are full", + sd_max: "max", + sd_pin_fav: "Pin to favourites (Ctrl+B)", + sd_unpin_fav: "Unpin from favourites (Ctrl+B)", + sd_remove_fav: "Remove from favourites", + sd_favourite: "Favourite", + sd_move_left: "Move left", + sd_move_right: "Move right", + sd_unpin: "Unpin", + sd_mode_advanced: "Advanced", + sd_mode_expert: "Expert", + sd_mode_develop: "Developer", }, ca_ES: { t1: "Benvingut a Orca Slicer", diff --git a/resources/web/dialog/SpeedDial/index.html b/resources/web/dialog/SpeedDial/index.html index 95a34b377a..a6f9e753a6 100644 --- a/resources/web/dialog/SpeedDial/index.html +++ b/resources/web/dialog/SpeedDial/index.html @@ -7,6 +7,7 @@ <link rel="stylesheet" href="./style.css" /> <link rel="stylesheet" type="text/css" href="../css/theme.css" /> <script type="text/javascript" src="../../include/globalapi.js"></script> + <script type="text/javascript" src="../../data/text.js"></script> <script src="../../js/fuzzy-search.js"></script> <script src="./speeddial.js"></script> </head> diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 9045262ea3..2e61165559 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -2,9 +2,9 @@ // node vm can exercise the pure helpers (searchActions / filterTabs / actionLabel / nextSel / commandList). // ---- state (populated by the C++ bridge via window.HandleStudio) ---- -var ACTIONS = []; // [{id,title,source,group,input,shortcut}], already frecency-sorted by C++ +var ACTIONS = []; // [{id,title,source,group,input,icon,mode}], already frecency-sorted by C++ var FAVS = []; // [id...] -var RECENTS = []; // [{id,title,source,group,input,shortcut}] - last-N launched +var RECENTS = []; // [{id,title,source,group,input,icon,mode}] - last-N launched var query = ""; var sel = { zone: "list", i: 0 }; // zone: 'list' | 'fav' var lastResizeHeight = 0; @@ -15,6 +15,26 @@ var matchIndex = {}; var USER_MODE = "simple"; var MODE_RANK = { simple: 0, advanced: 1, expert: 2, develop: 3 }; +// Search ranking weights: every contiguous match must outrank every fuzzy one regardless of field, +// and title must outrank group, which outranks source. +var SCORE_CONTIGUOUS = 100000; +var SCORE_TITLE = 2000; +var SCORE_GROUP = 1000; + +// Localized lookup for strings this page builds at runtime. text.js (loaded before this script) +// defines LangText; a missing entry falls back to the English literal. Extra args replace +// successive %s placeholders. +function T(key, fallback) { + var table = (typeof LangText !== "undefined" && LangText) || null; + var lang = "en"; + try { lang = localStorage.getItem(LANG_COOKIE_NAME) || "en"; } catch (e) {} + var s = table && table[lang] && table[lang][key] !== undefined ? table[lang][key] : + table && table.en && table.en[key] !== undefined ? table.en[key] : fallback; + for (var i = 2; i < arguments.length; i++) + s = s.replace("%s", arguments[i]); + return s; +} + // ---- windowed list render ---------------------------------------------------- // The command list is rendered in windows (append-on-scroll) so a huge settings pool doesn't build // the whole DOM per keystroke. Rows are exactly ROW_H tall (matches .row min-height 44px; see --row-h, @@ -36,11 +56,11 @@ var searchNeedle = ""; var phase = "commands"; var tabOptions = []; // [{id,title}] - notebook pages, fetched on entering the tab phase -// why: the fuzzy matcher (NormText/FuzzyRangesNorm/WholeWordRangesNorm) lives in shared +// why: the fuzzy matcher (NormText/FuzzyRangesNorm) lives in shared // ../../js/fuzzy-search.js, loaded before this script. Search is always case-insensitive. // element handles, assigned in OnInit (kept null so load-time touches no DOM) -var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, countEl = null, headEl = null; +var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, countEl = null; // ---- pure helpers (no DOM; unit-tested) ------------------------------------- // Pre-normalized haystacks, cached on the action object. The fold is length-preserving (1:1 per @@ -89,14 +109,20 @@ function fieldMatchScore(norm, wwRe) { return { score: 1000 - r[0][0] * 10 - gaps * 10, ranges: r, contiguous: r.length === 1 && len === searchNeedle.length }; } -// Combine the per-field match scores into one comparable value. Ranking tiers, strongest first: +// Combine the per-field match scores into one comparable value, or null when no field matched. +// Ranking tiers, strongest first: // tier (contiguous/perfect vs fuzzy) > field (title > group > source) > start/gaps. // The additive weights keep every contiguous match above every fuzzy one regardless of field. function scoreFields(t, g, s) { - var best = -1; - if (t) best = Math.max(best, (t.contiguous ? 100000 : 0) + 2000 + t.score); - if (g) best = Math.max(best, (g.contiguous ? 100000 : 0) + 1000 + g.score); - if (s) best = Math.max(best, (s.contiguous ? 100000 : 0) + s.score); + var best = null; + function consider(m, weight) { + if (!m) return; + var v = (m.contiguous ? SCORE_CONTIGUOUS : 0) + weight + m.score; + best = best === null ? v : Math.max(best, v); + } + consider(t, SCORE_TITLE); + consider(g, SCORE_GROUP); + consider(s, 0); return best; } @@ -125,7 +151,7 @@ function searchActions(actions, query) { var g = fieldMatchScore(groupNorm(a), wwRe); var s = fieldMatchScore(sourceNorm(a), wwRe); var score = scoreFields(t, g, s); - if (score < 0) continue; + if (score === null) continue; // Ranges are per-field against the ACTUAL text drawn: title for the row-name, and group (or // source when group is empty) for the eyebrow - so highlight offsets stay aligned to the label. matchIndex[a.id] = { @@ -198,7 +224,8 @@ function favIndexForDigit(d) { } function resultCountText(total, shown, query) { - return (query || "").trim() ? "Showing " + shown + " of " + total + " actions" : total + " actions"; + var n = total + " " + T("sd_actions", "actions"); + return (query || "").trim() ? T("sd_showing", "Showing") + " " + shown + " " + T("sd_of", "of") + " " + n : n; } // Display label for a notebook tab. Trim any stray whitespace; pages added with an empty title @@ -259,7 +286,7 @@ function commandList(actions, recents, query) { // 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) { if (sel.zone === "fav") return favIds[sel.i]; if (!actions || !actions.length) @@ -286,9 +313,9 @@ function needsModeSwitch(item, userMode) { // Short mode tag for an action that needs a switch, or "" when it is already available. function modeBadge(item, userMode) { if (!needsModeSwitch(item, userMode)) return ""; - if (item.mode === "develop") return "Developer"; - if (item.mode === "expert") return "Expert"; - if (item.mode === "advanced") return "Advanced"; + if (item.mode === "develop") return T("sd_mode_develop", "Developer"); + if (item.mode === "expert") return T("sd_mode_expert", "Expert"); + if (item.mode === "advanced") return T("sd_mode_advanced", "Advanced"); return ""; } @@ -334,12 +361,7 @@ function stateFromPayload(payload) { actions: payload.actions || [], favourites: payload.favourites || [], recent: payload.recent || [], - userMode: payload.user_mode || "simple", - query: "", - sel: { zone: "list", i: 0 }, - lastResizeHeight: 0, - phase: "commands", - tabOptions: [] + userMode: payload.user_mode || "simple" }; } @@ -395,19 +417,19 @@ window.HandleStudio = function (payload) { FAVS = next.favourites; RECENTS = next.recent; USER_MODE = next.userMode; - query = next.query; - sel = next.sel; - lastResizeHeight = next.lastResizeHeight; - phase = next.phase; - tabOptions = next.tabOptions; + // A fresh payload re-opens the main phase; C++ never rehydrates the transient phase/query state. + phase = "commands"; + tabOptions = []; + query = ""; + sel = { zone: "list", i: 0 }; + lastResizeHeight = 0; // why: builtKey caches phase|query so renderCommandsList can skip a rebuild on arrow-nav. // It survives a re-open (which never goes through exitPhase), so without a reset the cached // empty-query key would skip the rebuild and leave stale list content. builtKey = ""; - if (headEl) headEl.hidden = false; if (qEl) { qEl.value = ""; - qEl.placeholder = "Search " + ACTIONS.length + " actions"; + qEl.placeholder = T("sd_search_n", "Search %s actions", ACTIONS.length); syncClearButton(); } render({ resize: true, resetScroll: true }); @@ -422,7 +444,7 @@ window.HandleStudio = function (payload) { var fid = payload.id; if (fid && FAVS.indexOf(fid) !== -1) FAVS.splice(FAVS.indexOf(fid), 1); render({ resize: true, keepScroll: true }); - flashHint("Favourites are full (" + (payload.limit || K_FAV_LIMIT) + " max)"); + flashHint(T("sd_favs_full", "Favourites are full") + " (" + (payload.limit || K_FAV_LIMIT) + " " + T("sd_max", "max") + ")"); } }; @@ -485,7 +507,7 @@ function pinSvg(on) { function setPinState(pin, on) { pin.classList.toggle("on", on); pin.innerHTML = pinSvg(on); - pin.title = on ? "Unpin from favourites (Ctrl+B)" : "Pin to favourites (Ctrl+B)"; + pin.title = on ? T("sd_unpin_fav", "Unpin from favourites (Ctrl+B)") : T("sd_pin_fav", "Pin to favourites (Ctrl+B)"); } // ---- render ------------------------------------------------------------------ @@ -519,15 +541,16 @@ function renderFav() { var badge = document.createElement("span"); badge.className = "fav-slot"; badge.textContent = slot; - badge.title = slot === "0" ? "Favourite 10 (Alt+0)" : "Favourite " + slot + " (Alt+" + slot + ")"; + badge.title = slot === "0" ? T("sd_favourite", "Favourite") + " 10 (Alt+0)" : + T("sd_favourite", "Favourite") + " " + slot + " (Alt+" + slot + ")"; tile.appendChild(badge); } // 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.title = T("sd_remove_fav", "Remove from favourites"); + unpin.setAttribute("aria-label", T("sd_remove_fav", "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); @@ -567,9 +590,9 @@ function showFavMenu(x, y, id) { favMenuEl.innerHTML = ""; var favs = currentVisibleFavs(); var vi = favs.indexOf(id); - addFavMenuItem("Move left", vi > 0, function () { moveFav(id, -1); }); - addFavMenuItem("Move right", vi >= 0 && vi < favs.length - 1, function () { moveFav(id, 1); }); - addFavMenuItem("Unpin", true, function () { toggleFav(id); }); + addFavMenuItem(T("sd_move_left", "Move left"), vi > 0, function () { moveFav(id, -1); }); + addFavMenuItem(T("sd_move_right", "Move right"), vi >= 0 && vi < favs.length - 1, function () { moveFav(id, 1); }); + addFavMenuItem(T("sd_unpin", "Unpin"), true, function () { toggleFav(id); }); favMenuEl.hidden = false; favMenuEl.style.left = Math.max(0, Math.min(x, window.innerWidth - favMenuEl.offsetWidth - 4)) + "px"; favMenuEl.style.top = Math.max(0, Math.min(y, window.innerHeight - favMenuEl.offsetHeight - 4)) + "px"; @@ -598,52 +621,47 @@ function updateFavEyebrow(favs) { eyeEl.hidden = !a; } +// Row shell shared by action and tab rows: the row (selection class + aria label), the icon tile and +// the text column. Returns the pieces the caller fills in (eyebrow/name/badge/pin, handlers). +function beginRow(item, i, mono, ariaLabel) { + var row = document.createElement("div"); + row.className = "row" + (sel.zone === "list" && sel.i === i ? " sel" : ""); + row.setAttribute("aria-label", ariaLabel); + + var tile = document.createElement("div"); + tile.className = "tile"; + fillTile(tile, item, mono); + + var left = document.createElement("div"); + left.className = "row-left"; + var line = document.createElement("div"); + line.className = "row-line"; + left.appendChild(line); + row.appendChild(tile); + row.appendChild(left); + return { row: row, left: left, line: line }; +} + // A command/action row - used for search results, recents, and (because settings are actions now) // the setting options too. All rows are pinnable, so every row carries a bookmark. function renderActionRow(a, i) { var on = FAVS.indexOf(a.id) !== -1; - var row = document.createElement("div"); - row.className = "row" + (sel.zone === "list" && sel.i === i ? " sel" : ""); - row.setAttribute("aria-label", actionLabel(a, ACTIONS)); - - var tile = document.createElement("div"); - tile.className = "tile"; - fillTile(tile, a); - - var left = document.createElement("div"); - left.className = "row-left"; + var shell = beginRow(a, i, false, actionLabel(a, ACTIONS)); var mi = matchIndex[a.id]; // The eyebrow shows group when present, else source. Highlight with the ranges of whichever of the // two the eyebrow actually renders (so a "Recent Projects"/"Object" header match lights up like a // setting path does - the offsets are computed against the same string we are marking). var eyebrow = a.group || a.source; var eyebrowMatch = mi ? (mi.useEyebrowGroup ? mi.group : mi.source) : null; - var sourceEl = markedText("row-eyebrow", eyebrow, eyebrowMatch); - var line = document.createElement("div"); - line.className = "row-line"; - var name = markedText("row-name", a.title, mi ? mi.title : null); - line.appendChild(name); + shell.left.insertBefore(markedText("row-eyebrow", eyebrow, eyebrowMatch), shell.line); + shell.line.appendChild(markedText("row-name", a.title, mi ? mi.title : null)); var badge = modeBadge(a, USER_MODE); if (badge) { var tag = document.createElement("span"); tag.className = "row-mode"; tag.textContent = badge; - line.appendChild(tag); + shell.line.appendChild(tag); } - if (a.shortcut) { - var sc = document.createElement("div"); - sc.className = "row-sc"; - a.shortcut.split("+").forEach(function (k) { - var key = document.createElement("kbd"); - key.textContent = k; - sc.appendChild(key); - }); - line.appendChild(sc); - } - left.appendChild(sourceEl); - left.appendChild(line); - row.appendChild(tile); - row.appendChild(left); var pin = document.createElement("button"); pin.className = "pin"; @@ -651,11 +669,11 @@ function renderActionRow(a, i) { pin.onclick = function (ev) { ev.stopPropagation(); toggleFav(a.id); }; // why: two quick fav/unfav clicks must not dblclick-run the row pin.ondblclick = function (ev) { ev.stopPropagation(); }; - row.appendChild(pin); + shell.row.appendChild(pin); - row.onclick = function () { sel = { zone: "list", i: i }; render({ resize: true }); }; - row.ondblclick = function () { sel = { zone: "list", i: i }; activateEntry(a); }; - return row; + shell.row.onclick = function () { sel = { zone: "list", i: i }; render({ resize: true }); }; + shell.row.ondblclick = function () { sel = { zone: "list", i: i }; activateEntry(a); }; + return shell.row; } // Append rows [from, to) into listEl, always inserting before the bottom spacer so row order is preserved. @@ -731,6 +749,18 @@ function updatePins(list) { } } +// Replace the list with a single placeholder message (no matches / empty state). Shared by all phases. +function renderEmpty(text) { + listEl.innerHTML = ""; + spacerEl = null; + listEl.className = "dial-list empty"; + if (countEl) countEl.hidden = true; + var empty = document.createElement("div"); + empty.className = "dial-empty"; + empty.textContent = text; + listEl.appendChild(empty); +} + function renderCommandsList() { var list = currentList(); var total = list.length; @@ -742,14 +772,8 @@ function renderCommandsList() { matchIndex = {}; if (!total) { - listEl.innerHTML = ""; - spacerEl = null; - listEl.className = "dial-list empty"; - if (countEl) countEl.hidden = true; - var empty = document.createElement("div"); - empty.className = "dial-empty"; - empty.textContent = showList ? ("No actions match (Total: " + ACTIONS.length + ")") : "No actions yet"; - listEl.appendChild(empty); + renderEmpty(showList ? (T("sd_no_match", "No actions match") + " (" + T("sd_total", "Total") + ": " + ACTIONS.length + ")") + : T("sd_no_actions", "No actions yet")); renderEnd = 0; builtKey = buildKey() + "|0"; return; @@ -767,7 +791,7 @@ function renderCommandsList() { listEl.className = "dial-list"; if (countEl) { countEl.hidden = false; - countEl.textContent = showList ? resultCountText(ACTIONS.length, total, query) : total + " recent"; + countEl.textContent = showList ? resultCountText(ACTIONS.length, total, query) : total + " " + T("sd_recent", "recent"); } updateSelection(); updatePins(list); @@ -777,29 +801,14 @@ function renderCommandsList() { // tabTitle so pages added with an empty text (e.g. Home) still show a label. 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"; - fillTile(tile, t, true); - - var left = document.createElement("div"); - left.className = "row-left"; - var line = document.createElement("div"); - line.className = "row-line"; + var shell = beginRow(t, i, true, label); 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; + shell.line.appendChild(name); + shell.row.onclick = function () { sel = { zone: "list", i: i }; render({ resize: true }); }; + shell.row.ondblclick = function () { sel = { zone: "list", i: i }; jumpToTab(t); }; + return shell.row; } function renderTabList() { @@ -808,12 +817,7 @@ function renderTabList() { 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); + renderEmpty(q ? T("sd_no_tabs_match", "No tabs match") : T("sd_no_tabs", "No tabs")); return; } if (sel.zone === "list") @@ -821,20 +825,14 @@ function renderTabList() { listEl.className = "dial-list"; if (countEl) { countEl.hidden = false; - countEl.textContent = q ? list.length + " matches" : list.length + " tabs"; + countEl.textContent = q ? list.length + " " + T("sd_matches", "matches") : list.length + " " + T("sd_tabs", "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); + renderEmpty(q ? T("sd_go_to_pct", "Go to %s% of the layer range", q) : T("sd_enter_pct", "Enter a layer percentage (0-100)")); } function renderList() { @@ -936,7 +934,7 @@ function runSelected() { return; } var list = currentList(); - var id = selectedActionId(sel, list, currentVisibleFavs(), query); + var id = selectedActionId(sel, list, currentVisibleFavs()); if (id) activateEntry(byId(id)); } @@ -950,38 +948,43 @@ function runJumpToLayer(pct) { } function jumpToTab(t) { - SendMessage({ command: "go_to_tab", id: t.id, title: tabTitle(t) }); + var a = findActionByInput("tab"); + if (!a || !t) return; + SendMessage({ command: "run_action", id: a.id, title: a.title, param: t.id }); +} + +// Clear the shared query/cursor state when entering or leaving a phase. Callers set the +// phase-specific placeholder, then render. +function resetPhaseInput() { + query = ""; qEl.value = ""; sel = { zone: "list", i: 0 }; + syncClearButton(); } function enterPercentPhase() { - phase = "percent"; query = ""; qEl.value = ""; - sel = { zone: "list", i: 0 }; - qEl.placeholder = "Go to layer % (0-100)"; - syncClearButton(); + phase = "percent"; + resetPhaseInput(); + qEl.placeholder = T("sd_go_layer_ph", "Go to layer % (0-100)"); 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(); + phase = "tab"; tabOptions = []; + resetPhaseInput(); + qEl.placeholder = T("sd_go_tab_ph", "Go to tab"); render({ resize: true, resetScroll: true }); qEl.focus(); SendMessage({ command: "search_tabs" }); } function exitPhase() { - phase = "commands"; tabOptions = []; query = ""; qEl.value = ""; - if (headEl) headEl.hidden = false; - sel = { zone: "list", i: 0 }; + phase = "commands"; tabOptions = []; + resetPhaseInput(); // why: builtKey caches phase|query so renderCommandsList can skip a rebuild on arrow-nav/click. // It survives a second-phase exit (which never goes through exitPhase from the commands view), // so without a reset the cached empty-query key would skip the rebuild and leave stale content. builtKey = ""; - qEl.placeholder = "Search " + ACTIONS.length + " actions"; - syncClearButton(); + qEl.placeholder = T("sd_search_n", "Search %s actions", ACTIONS.length); render({ resize: true, resetScroll: true }); qEl.focus(); } @@ -991,13 +994,20 @@ function focusInput() { setTimeout(function () { if (qEl) qEl.focus(); }, 0); } // ---- init -------------------------------------------------------------------- function OnInit() { qEl = $("q"); listEl = $("list"); favEl = $("favBar"); clearEl = $("clear"); eyeEl = $("favEyebrow"); countEl = $("count"); - headEl = document.querySelector(".dial-head"); + // text.js's TranslatePage() targets jQuery `.trans` nodes; this page has none and defines its own + // `$`, so don't call it. Runtime strings go through T() instead. + qEl.placeholder = T("sd_search", "Search actions"); + qEl.setAttribute("aria-label", T("sd_search", "Search actions")); + if (clearEl) { + clearEl.title = T("sd_clear", "Clear"); + clearEl.setAttribute("aria-label", T("sd_clear", "Clear")); + } syncClearButton(); $("clear").onclick = function () { - query = ""; qEl.value = ""; sel = { zone: "list", i: 0 }; - render({ resize: true, resetScroll: true }); qEl.focus(); - syncClearButton(); + resetPhaseInput(); + render({ resize: true, resetScroll: true }); + qEl.focus(); }; qEl.addEventListener("input", function () { query = qEl.value; sel = { zone: "list", i: 0 }; syncClearButton(); @@ -1026,7 +1036,7 @@ function OnInit() { if (phase === "commands" && (e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === "b") { e.preventDefault(); - var id = selectedActionId(sel, currentList(), currentVisibleFavs(), query); + var id = selectedActionId(sel, currentList(), currentVisibleFavs()); if (id) toggleFav(id); return; } diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 4fe72aba11..7f08880510 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -109,6 +109,14 @@ ctx.searchActions(orientPool, "orient"); assert.deepEqual(ctx.matchIndex.ao.title, [[5, 11]], "a whole-word match highlights the full word, not a scattered fuzzy pick"); +// A fuzzy source-only match with a very late start still counts, even though its score is negative; +// the old `score < 0` sentinel mistook it for "no field matched" and dropped the action. +const negativePool = [ + { id: "n", title: "Unrelated", source: "o" + "x".repeat(200) + "rnt", group: "", input: "" } +]; +assert.equal(ctx.searchActions(negativePool, "ornt").length, 1, + "a low-score fuzzy match is not mistaken for no match"); + // commandList (the main-phase list) delegates to the ranked search for a typed query and returns // the mixed recents (no discrimination) for an empty query. const mixed = [ @@ -122,22 +130,22 @@ assert.deepEqual(ctx.commandList(mixed, mixed.slice(0, 1), "").map(function (a) // selectedActionId: resolves the active list (recents for an empty query, filtered list otherwise). assert.equal( - ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), [], ""), + ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), []), null, "Enter with an empty query and no recents must not resolve to an action the list never showed" ); assert.equal( - ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), [], "rep"), + ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), []), "0123456789abcdef", "a typed query resolves the list selection" ); assert.equal( - ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [duplicateActions[0]], ""), [], ""), + ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [duplicateActions[0]], ""), []), "0123456789abcdef", "Enter with an empty query resolves the recent entry" ); assert.equal( - ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"], ""), + ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"]), "fedcba9876543210", "favourites stay runnable with an empty query - the fav bar is always visible" ); diff --git a/resources/web/js/fuzzy-search.js b/resources/web/js/fuzzy-search.js index 66b2c7b520..22fa8491f9 100644 --- a/resources/web/js/fuzzy-search.js +++ b/resources/web/js/fuzzy-search.js @@ -19,18 +19,31 @@ function Norm(ch, caseSensitive) { return caseSensitive ? folded : folded.toLowerCase(); // case-sensitivity is the only toggle } -// Pre-normalize a whole haystack with the SAME per-char fold FuzzyRanges uses, so a caller can match -// it repeatedly against one cached string. The fold is 1:1 in length, so indices stay aligned to the -// ORIGINAL text - the highlight ranges that FuzzyRangesNorm returns slice the original correctly. -// Iterate by UTF-16 code unit (not Array.from code point) to mirror FuzzyRanges' own indexing exactly. +// Pre-normalize a whole haystack with a length-preserving fold, so a caller can match it repeatedly +// against one cached string. One input UTF-16 code unit always maps to one output code unit, so +// indices stay aligned to the ORIGINAL text - the highlight ranges that FuzzyRangesNorm returns slice +// the original correctly. Iterate by UTF-16 code unit (not Array.from code point) to mirror +// FuzzyRanges' own indexing exactly. function NormText(text, caseSensitive) { const src = text || ""; let out = ""; for (let i = 0; i < src.length; i++) - out += Norm(src[i], caseSensitive); + out += NormStable(src[i], caseSensitive); return out; } +// Length-preserving variant of Norm: NFD can expand a code unit (Hangul syllables become Jamo) or +// drop it (combining diacritics), and toLowerCase can expand one too (U+0130). Any of those would +// desync highlight offsets, so fall back to the original unit whenever the fold is not 1:1. +function NormStable(ch, caseSensitive) { + const folded = FoldChar(ch); + const stable = folded.length === 1 ? folded : ch; + if (caseSensitive) + return stable; + const lower = stable.toLowerCase(); + return lower.length === 1 ? lower : stable; +} + // Match a PRE-normalized haystack against a PRE-normalized needle (both produced by NormText with the // same caseSensitive flag). Skipping the per-character fold makes repeated matching (per keystroke over a // cached pool) cheap. Returns ranges in original coordinates, or null on no match. @@ -115,18 +128,3 @@ function WholeWordRanges(text, query, caseSensitive) { ranges.push([match.index, match.index + match[0].length]); return ranges.length > 0 ? ranges : null; } - -// Same whole-word (\b-bounded) match as WholeWordRanges, but against PRE-normalized haystack/needle -// (NormText output, so offsets stay length-aligned to the original text). Returns the first match as -// [[i, i+len]] in original coordinates, or null. Non-global so the caller can reuse one compiled regex -// across many fields without re-setting lastIndex. Skipping the per-char fold keeps the Speed Dial's -// per-keystroke scan over thousands of cached settings cheap. -function WholeWordRangesNorm(haystackNorm, needleNorm) { - const t = haystackNorm || ""; - const needle = needleNorm || ""; - if (!needle) - return null; - const re = new RegExp(`\\b${EscapeRegExp(needle)}\\b`); - const match = re.exec(t); - return match ? [[match.index, match.index + match[0].length]] : null; -} diff --git a/resources/web/js/fuzzy-search.test.js b/resources/web/js/fuzzy-search.test.js index 48d2352907..92cbf1c9f8 100644 --- a/resources/web/js/fuzzy-search.test.js +++ b/resources/web/js/fuzzy-search.test.js @@ -5,7 +5,7 @@ const vm = require("vm"), assert = require("assert"), fs = require("fs"); const ctx = {}; vm.createContext(ctx); vm.runInContext(fs.readFileSync(__dirname + "/fuzzy-search.js", "utf8"), ctx); -const { FoldChar, Norm, NormText, EscapeRegExp, FuzzyRanges, WholeWordRanges, FuzzyRangesNorm, WholeWordRangesNorm } = ctx; +const { FoldChar, Norm, NormText, EscapeRegExp, FuzzyRanges, WholeWordRanges, FuzzyRangesNorm } = ctx; // FoldChar / Norm: accents fold, case only folds when case-insensitive. assert.equal(FoldChar("é"), "e"); @@ -41,8 +41,11 @@ assert.deepEqual(FuzzyRangesNorm(NormText("Auto-Orient", false), NormText("orien assert.deepEqual(FuzzyRangesNorm(NormText("AutoOriented", false), NormText("orient", false)), [[4, 10]]); assert.equal(FuzzyRangesNorm(NormText("Measure", false), NormText("xyz", false)), null); // no subsequence -// WholeWordRangesNorm: \b-bounded literal against a pre-normalized haystack, offsets in original coords. -assert.deepEqual(WholeWordRangesNorm(NormText("Auto-Orient", false), NormText("orient", false)), [[5, 11]]); -assert.equal(WholeWordRangesNorm(NormText("AutoOriented", false), NormText("orient", false)), null); // inside a word +// NormText stays 1:1 with the original even when NFD expands (Hangul syllables) or drops (combining +// diacritics, U+0130) a code unit, so highlight ranges remain aligned to the original string. +assert.equal(NormText("각x", false).length, "각x".length); +assert.equal(NormText("e\u0301x", false).length, "e\u0301x".length); +assert.equal(NormText("\u0130x", false).length, "\u0130x".length); +assert.equal(FuzzyRangesNorm(NormText("각abcdeXfgh", false), NormText("X", false))[0][0], 6); console.log("ok"); diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index d1ce814b23..a64fe9f705 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -141,9 +141,6 @@ std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std: // ---- 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"; constexpr const char* kSettingPrefix = "orca_setting"; constexpr const char* kPlateGotoPrefix = "orca_plate_goto"; constexpr const char* kRecentProjectPrefix = "orca_recent_project"; @@ -212,29 +209,28 @@ struct SettingAction : AppAction } }; -// A built-in command action. Thin value: identity + presentation come from the NativeCommands -// catalog, and run() routes back to it - the catalog is the single source of truth for its -// behaviour. The id is keyed by the stable catalog key (NOT the display title), so a rename or a -// UI-language switch never re-keys the action; the title is display-only. -struct CommandAction : AppAction +// Seed one action's persisted state (favourite flag + frecency counters) from an already-parsed +// stats blob and capped favourite list. Shared by the dynamic materialisers. +void seed_from(const nlohmann::json& stats, const std::vector<std::string>& favs, const std::string& id, AppAction& a) { - static std::unique_ptr<CommandAction> make(const NativeCommand& c) { return std::unique_ptr<CommandAction>(new CommandAction(c)); } - - std::string command_key; - - AppActionRunResult run(const std::string& param) const override { return NativeCommands::run(command_key, param); } - -private: - explicit CommandAction(const NativeCommand& c) - : AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, c.title, kOrcaSourceKey, kOrcaSourceName) - , command_key(c.key) - { - this->kind = AppActionKind::Command; - this->group = c.group; - this->input = c.input; - this->icon = c.icon; + a.favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); + if (auto it = stats.find(id); it != stats.end() && it->is_object()) { + a.count = it->value("count", 0); + a.last = it->value("last", 0LL); } -}; +} + +// Drop actions whose id starts with `prefix` but that were not seen in this pass (a stale materialisation). +void drop_stale(std::unordered_map<std::string, std::shared_ptr<AppAction>>& actions, const char* prefix, + const std::unordered_set<std::string>& seen) +{ + for (auto it = actions.begin(); it != actions.end();) { + if (it->first.rfind(prefix, 0) == 0 && !seen.count(it->first)) + it = actions.erase(it); + else + ++it; + } +} // A dynamic "Go to Plate N" action, one per live plate, rebuilt on every snapshot() (so a // rename/move immediately shows up). id is keyed by plate index, NOT the display title, so @@ -343,10 +339,10 @@ void ActionRegistry::init() // 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. The catalog lives in NativeCommands - the registry - // only materialises thin CommandAction values from it. + // by prefix, so this is order-independent. The catalog (and its thin AppAction adapter) lives in + // NativeCommands; the registry only stores and dispatches the result. for (const NativeCommand& c : NativeCommands::catalog()) - upsert(CommandAction::make(c)); + upsert(NativeCommands::make_action(c)); } void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange change) @@ -429,6 +425,14 @@ void ActionRegistry::seed_state(AppAction& a) const } } +void ActionRegistry::load_persisted(nlohmann::json& stats, std::vector<std::string>& favs) const +{ + stats = read_section("stats", nlohmann::json::object()); + if (!stats.is_object()) + stats = nlohmann::json::object(); + favs = favourite_ids(); +} + // ---- read surface ----------------------------------------------------------- const AppAction* ActionRegistry::by_id(const std::string& id) const @@ -529,10 +533,9 @@ void ActionRegistry::materialize_setting_actions() // Load the persisted per-action state ONCE (not per-option) so a re-materialised setting keeps // its recency/favourite; mirroring seed_state but amortised over the whole option set. - nlohmann::json stats = read_section("stats", nlohmann::json::object()); - if (!stats.is_object()) - stats = nlohmann::json::object(); - const std::vector<std::string> favs = favourite_ids(); + nlohmann::json stats; + std::vector<std::string> favs; + load_persisted(stats, favs); std::unordered_set<std::string> seen; for (const Search::Option& opt : options) { @@ -568,11 +571,7 @@ void ActionRegistry::materialize_setting_actions() } } - action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); - if (auto it = stats.find(id); it != stats.end() && it->is_object()) { - action->count = it->value("count", 0); - action->last = it->value("last", 0LL); - } + seed_from(stats, favs, id, *action); auto const action_id = action->id(); auto const app_action = std::shared_ptr<AppAction>(std::move(action)); m_actions.insert_or_assign(action_id, app_action); @@ -580,12 +579,7 @@ void ActionRegistry::materialize_setting_actions() // Drop SettingActions whose option no longer exists in the current configs (e.g. the printer // technology / UI mode changed). Non-setting actions are untouched. - for (auto it = m_actions.begin(); it != m_actions.end();) { - if (it->first.rfind(kSettingPrefix, 0) == 0 && !seen.count(it->first)) - it = m_actions.erase(it); - else - ++it; - } + drop_stale(m_actions, kSettingPrefix, seen); } void ActionRegistry::materialize_plate_actions() @@ -597,21 +591,15 @@ void ActionRegistry::materialize_plate_actions() Plater* plater = wxTheApp ? wxGetApp().plater() : nullptr; if (!plater || plater->printer_technology() != ptFFF || plater->only_gcode_mode()) { // Drop any stale plate actions (e.g. the printer technology switched to SLA). - for (auto it = m_actions.begin(); it != m_actions.end();) { - if (it->first.rfind(kPlateGotoPrefix, 0) == 0) - it = m_actions.erase(it); - else - ++it; - } + drop_stale(m_actions, kPlateGotoPrefix, {}); return; } // Persisted per-action state, read ONCE (mirrors materialize_setting_actions) so a relisted // "Go to Plate N" keeps its recency/favourite when the plate is renamed - the id is index-keyed. - nlohmann::json stats = read_section("stats", nlohmann::json::object()); - if (!stats.is_object()) - stats = nlohmann::json::object(); - const std::vector<std::string> favs = favourite_ids(); + nlohmann::json stats; + std::vector<std::string> favs; + load_persisted(stats, favs); const std::vector<PartPlate*>& list = plater->get_partplate_list().get_plate_list(); std::unordered_set<std::string> seen; @@ -629,24 +617,15 @@ void ActionRegistry::materialize_plate_actions() if (!name.empty()) title += " (" + name + ")"; - auto action = std::make_unique<PlateAction>(int(i), title, kOrcaSourceName); - action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); - if (auto it = stats.find(id); it != stats.end() && it->is_object()) { - action->count = it->value("count", 0); - action->last = it->value("last", 0LL); - } + auto action = std::make_unique<PlateAction>(int(i), title, kOrcaSourceName); + seed_from(stats, favs, id, *action); auto const action_id = action->id(); auto const app_action = std::shared_ptr<AppAction>(std::move(action)); m_actions.insert_or_assign(action_id, app_action); } // Drop plate actions whose index no longer exists (a plate was deleted / moved to the front). - for (auto it = m_actions.begin(); it != m_actions.end();) { - if (it->first.rfind(kPlateGotoPrefix, 0) == 0 && !seen.count(it->first)) - it = m_actions.erase(it); - else - ++it; - } + drop_stale(m_actions, kPlateGotoPrefix, seen); } void ActionRegistry::materialize_recent_project_actions() @@ -655,10 +634,9 @@ void ActionRegistry::materialize_recent_project_actions() // Persisted per-action state, read ONCE (mirrors materialize_plate_actions) so a relisted recent // project keeps its recency/favourite when the recents list reorders - the id is path-keyed. - nlohmann::json stats = read_section("stats", nlohmann::json::object()); - if (!stats.is_object()) - stats = nlohmann::json::object(); - const std::vector<std::string> favs = favourite_ids(); + nlohmann::json stats; + std::vector<std::string> favs; + load_persisted(stats, favs); // app_config stores recents oldest-first; the palette shows newest-first. std::vector<std::string> recents = wxGetApp().app_config->get_recent_projects(); @@ -680,24 +658,15 @@ void ActionRegistry::materialize_recent_project_actions() if (title.empty()) title = path; - auto action = std::make_unique<RecentProjectAction>(path, std::move(title), path); - action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end(); - if (auto it = stats.find(id); it != stats.end() && it->is_object()) { - action->count = it->value("count", 0); - action->last = it->value("last", 0LL); - } + auto action = std::make_unique<RecentProjectAction>(path, std::move(title), path); + seed_from(stats, favs, id, *action); auto const action_id = action->id(); auto const app_action = std::shared_ptr<AppAction>(std::move(action)); m_actions.insert_or_assign(action_id, app_action); } // Drop recent-project actions whose file no longer exists / was removed from the recents list. - for (auto it = m_actions.begin(); it != m_actions.end();) { - if (it->first.rfind(kRecentProjectPrefix, 0) == 0 && !seen.count(it->first)) - it = m_actions.erase(it); - else - ++it; - } + drop_stale(m_actions, kRecentProjectPrefix, seen); } bool ActionRegistry::should_ask(const std::string& id) const @@ -753,7 +722,6 @@ nlohmann::json ActionRegistry::snapshot() {"group", a->group}, {"input", a->input}, {"icon", a->icon}, - {"shortcut", ""}, {"mode", mode_key(a->required_mode)}}); }; @@ -816,7 +784,7 @@ nlohmann::json ActionRegistry::tab_options() const if (id.empty()) continue; out.push_back({{"id", id.ToStdString()}, - {"title", notebook->GetPageText(i).ToStdString()}, + {"title", notebook->GetPageLabel(i).ToStdString()}, {"icon", notebook->GetPageIcon(i)}}); } return out; diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index cc73d3ad5c..3c71317eed 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -114,6 +114,12 @@ private: std::string m_source_name; // display name of the action's source }; +// Stable identity/display name of the built-in ("OrcaSlicer") action source. Shared by the native +// command catalog and the dynamically materialised setting/plate/recent actions so every built-in +// action re-keys together. +inline constexpr const char* kOrcaSourceKey = "orca"; +inline constexpr const char* kOrcaSourceName = "OrcaSlicer"; + // True when a setting at `setting_mode` cannot be edited in `current_mode` and the UI must switch // first. Developer settings are handled as a separate prompt by the Speed Dial. inline bool requires_mode_switch(ConfigOptionMode setting_mode, ConfigOptionMode current_mode) @@ -128,15 +134,17 @@ std::vector<std::string> cap_favourites(const std::vector<std::string>& ids, siz // Self-contained sink and single owner of runnable actions for the app session. // // Workflow: -// 1. init() (once, UI thread) subscribes to the plugin loader and enumerates the -// current script capabilities into actions. +// 1. init() (once, UI thread) subscribes to the plugin loader and enumerates the current script +// capabilities into actions, then materialises the static built-ins from the NativeCommands catalog. // 2. Loader load/unload callbacks route through refresh_source()/refresh_capability(), // which upsert()/remove() actions. The registry keeps the only action list and // restores persisted user state as actions arrive. -// 3. Consumers use by_id(), snapshot(), and run() without knowing the source. +// 3. Dynamic built-in families (settings, plates, recent projects) are re-materialised at the top of +// snapshot(), because their membership follows live state (the current configs, plate list, recents). +// 4. Consumers use by_id(), snapshot(), and run() without knowing the source. // -// note: there is exactly one source (script plugins), so it lives inline here rather -// than behind a polymorphic source interface. +// note: the static catalog lives in NativeCommands; the registry owns the pool, persistence and +// dispatch, and materialises the dynamic families inline rather than behind a source interface. class ActionRegistry { public: @@ -180,16 +188,20 @@ public: nlohmann::json snapshot(); // "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. + // as [{id,title,icon},...], using the page's real label (not the compact-blanked button text). + // 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: void seed_state(AppAction& a) const; // favourite/stats from config AppAction* find(const std::string& id); + // Read the persisted stats blob + capped favourite list once for a materialisation pass. + void load_persisted(nlohmann::json& stats, std::vector<std::string>& favs) const; + // (Re)materialise the current visible config settings as SettingActions from the live // searcher (respecting printer-tech + user-mode + visibility filtering), removing stale ones. // Called at the top of snapshot() so the palette always reflects the current configs. diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 4cd014952a..f9fbb463a6 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2616,6 +2616,10 @@ void GUI_App::init_app_config() } #endif // _WIN32 } + // Speed Dial opens on a bare Space from any page by default. Seed the flag so Preferences and the + // MainFrame shortcut read the same value; an existing config (true or false) is left untouched. + if (app_config->get("enable_speed_dial").empty()) + app_config->set_bool("enable_speed_dial", true); set_logging_level(Slic3r::level_string_to_boost(app_config->get("log_severity_level"))); } @@ -8163,6 +8167,22 @@ void GUI_App::save_mode(const /*ConfigOptionMode*/int mode) update_mode(); } +void GUI_App::set_mode(ConfigOptionMode mode) +{ + const bool was_developer = app_config->get_bool("developer_mode"); + if (was_developer) + app_config->set_bool("developer_mode", false); + save_mode(mode); + if (was_developer) + app_config->save(); +} + +void GUI_App::enable_developer_mode() +{ + app_config->set_bool("developer_mode", true); + update_mode(); +} + // Update view mode according to selected menu void GUI_App::update_mode() { @@ -8316,17 +8336,32 @@ void GUI_App::refresh_plugins() // The metadata refresh blocks on disc discovery and a cloud round-trip, so run it on a worker // and report completion through the notification manager -- the speed dial needs no dialog. std::thread([]() { - refresh_plugin_metadata_blocking(/*fetch_cloud=*/true); - wxTheApp->CallAfter([]() { + wxString error; + try { + refresh_plugin_metadata_blocking(/*fetch_cloud=*/true); + } catch (const std::exception& ex) { + error = from_u8(ex.what()); + } catch (...) { + error = "Unknown error"; // plain literal: wx translation isn't safe off the UI thread + } + if (!wxTheApp) + return; + wxTheApp->CallAfter([error]() { if (wxGetApp().is_closing()) return; Plater* plater = wxGetApp().plater(); if (plater == nullptr) return; - plater->get_notification_manager()->push_notification( - NotificationType::CustomNotification, - NotificationManager::NotificationLevel::RegularNotificationLevel, - into_u8(_L("Plugins refreshed."))); + if (error.IsEmpty()) + plater->get_notification_manager()->push_notification( + NotificationType::CustomNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, + into_u8(_L("Plugins refreshed."))); + else + plater->get_notification_manager()->push_notification( + NotificationType::CustomNotification, + NotificationManager::NotificationLevel::ErrorNotificationLevel, + into_u8(wxString::Format(_L("Failed to refresh plugins: %s"), error))); }); }).detach(); } diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 5e8dbad245..d0c38b04dc 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -593,6 +593,11 @@ public: std::string get_saved_mode_str(); std::string get_mode_str(); void save_mode(const /*ConfigOptionMode*/int mode) ; + // Switch to `mode` from the Speed Dial: a developer-mode override hides the saved mode + // (get_mode returns comDevelop), so clear it first and persist the choice. + void set_mode(ConfigOptionMode mode); + // Turn the developer-mode override on and refresh the UI (used before jumping to a Developer setting). + void enable_developer_mode(); void update_mode(); void update_internal_development(); void show_ip_address_enter_dialog(wxString title = wxEmptyString); diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index aa9472c15c..be1f53ef97 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -199,8 +199,8 @@ void KBShortcutsDialog::fill_shortcuts() // Switch table page { ctrl + L("Tab"), L("Switch table page")}, // Open speed dial - { "Space", L("Open speed dial") }, - { alt + "1..9,0", L("Run a Speed Dial favourite") }, + { L_CONTEXT("Space", "Keyboard Shortcut"), L("Open speed dial") }, + { alt + "1..9,0", L("Run a Speed Dial favourite (while the Speed Dial is open)") }, //DEL #ifdef __APPLE__ {"fn+⌫", L("Delete Selected")}, diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 8ee09b4136..4681a67c29 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -45,6 +45,7 @@ #include "Preferences.hpp" #include "Widgets/Button.hpp" #include "Widgets/ProgressDialog.hpp" +#include "Widgets/StaticBox.hpp" #include "BindDialog.hpp" #include "../Utils/MacDarkMode.hpp" #include "../Utils/NetworkAgentFactory.hpp" @@ -109,17 +110,21 @@ namespace { // Space opens the speed dial, but it is the activation key for buttons, checkboxes and other // controls. CHAR_HOOK runs before the focused child, so only take Space when the focused window has -// no keyboard-activation meaning of its own. Canvases (GLCanvas3D) and panels are not wxControls and -// fall through to "open"; Notebook and wxWebView are wxControls that don't use Space, so allow them. +// no keyboard-activation meaning of its own. Canvases (GLCanvas3D) and panels are not controls and +// fall through to "open"; the Notebook itself does too, so Space still opens the dial on any page. bool focus_keeps_space(wxWindow* focus) { if (!focus) return false; if (dynamic_cast<wxTextEntryBase*>(focus)) return true; // typing a space into a text field + if (dynamic_cast<wxWebView*>(focus)) + return true; // web content scrolls and hosts its own text fields if (dynamic_cast<::Button*>(focus)) return true; // custom button: Space clicks it (it is a wxWindow, not a wxControl) - if (dynamic_cast<wxControl*>(focus) && !dynamic_cast<Notebook*>(focus) && !dynamic_cast<wxWebView*>(focus)) + if (dynamic_cast<StaticBox*>(focus)) + return true; // custom composites (ComboBox, SpinInput, ...) activate with Space and are wxWindow + if (dynamic_cast<wxControl*>(focus) && !dynamic_cast<Notebook*>(focus)) return true; // stock button/checkbox/choice/list/etc. keep Space return false; } @@ -724,8 +729,10 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ // Orca: open the speed dial from any page with a bare Space. Only when no modifier is held (so // editing shortcuts like Ctrl+Shift+Space in the canvas still reach it) and the focused window // doesn't use Space to activate itself (buttons, checkboxes, list/choice controls, text fields), - // so a bare Space there still clicks/toggles instead of being hijacked. - if (!evt.CmdDown() && !evt.ShiftDown() && !evt.AltDown() && evt.GetKeyCode() == WXK_SPACE) { + // so a bare Space there still clicks/toggles instead of being hijacked. Gated by a preference + // (default on) so users can hand Space back to the focused control entirely. + if (wxGetApp().app_config->get_bool("enable_speed_dial") && !evt.CmdDown() && !evt.ShiftDown() && + !evt.AltDown() && evt.GetKeyCode() == WXK_SPACE) { if (focus_keeps_space(wxWindow::FindFocus())) { evt.Skip(); // let the focused control keep Space return; @@ -4325,7 +4332,7 @@ void MainFrame::technology_changed() // update menu titles PrinterTechnology pt = plater()->printer_technology(); if (int id = m_menubar->FindMenu(pt == ptFFF ? _omitL("Material Settings") : _L("Filament settings")); id != wxNOT_FOUND) - m_menubar->SetMenuLabel(id, pt == ptFFF ? _omitL("Material Settings") : _L("Filament settings")); + m_menubar->SetMenuLabel(id, pt == ptSLA ? _omitL("Material Settings") : _L("Filament settings")); } // Opens the calibration wizard for `calib_kind`. Single source of truth for the wizard lifecycle: diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp index 3886f8eb11..b1f517e12e 100644 --- a/src/slic3r/GUI/NativeCommands.cpp +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -25,7 +25,7 @@ #include <cmath> #include <cstdlib> #include <exception> -#include <map> +#include <memory> #include <string> #include <tuple> #include <utility> @@ -120,120 +120,43 @@ AppActionRunResult calib_command(CalibKind kind) return {AppActionRunResult::Level::Success}; } -// Palette-only: developer mode overrides the saved mode (get_mode returns comDevelop), so choosing -// Simple/Advanced/Expert must clear it first. Mirrors Preferences: persist the flag, then update. -void select_mode(ConfigOptionMode mode) -{ - GUI_App& app = wxGetApp(); - const bool was_developer = app.app_config->get_bool("developer_mode"); - if (was_developer) - app.app_config->set_bool("developer_mode", false); - app.save_mode(mode); - if (was_developer) - app.app_config->save(); -} +constexpr const char* kCommandPrefix = "orca_command"; -// Tile pictogram per command: the SVG base name of the icon the matching GUI control already uses -// (menu/toolbar/sidebar). Absent key => blank tile. Keeping this as one table makes the curation -// reviewable and lets a test check every value resolves to a real file. -const std::map<std::string, std::string>& command_icons() +// Thin AppAction wrapper for one catalog entry: identity and presentation come from the catalog, +// run() routes back to it. The id is keyed by the stable catalog key (not the display title), so a +// rename or a UI-language switch never re-keys the action. +struct CommandAction : AppAction { - static const std::map<std::string, std::string> icons = { - // Slice & Export - {"slice_and_preview", "media_play"}, - {"export_gcode", "menu_export_gcode"}, - {"export_stl", "menu_export_stl"}, - {"export_stl_multi", "menu_export_stl"}, - {"export_sliced_file", "menu_export_sliced_file"}, - {"export_all_sliced_file", "menu_export_sliced_file"}, - {"export_toolpaths_obj", "menu_export_toolpaths"}, - {"export_config", "menu_export_config"}, - {"export_3mf", "menu_save"}, - {"export_drc_single", "menu_export_stl"}, - {"export_drc_multi", "menu_export_stl"}, - // Commands - {"load_project", "menu_open"}, - {"save_project", "menu_save"}, - {"save_project_as", "menu_save"}, - {"open_preferences", "cog"}, - {"go_to_layer", "height_range_layer"}, - // Mode: the sidebar mode toggle's own icon (ParamsPanel). - {"mode_simple", "advanced"}, - {"mode_advanced", "advanced"}, - {"mode_expert", "advanced"}, - {"toggle_developer_mode", "advanced"}, - // Calibration - {"calib_temperature", "calib_sf"}, - {"calib_max_volumetric", "calib_sf"}, - {"calib_pressure_advance", "calib_sf"}, - {"calib_flow_ratio", "calib_sf"}, - {"calib_retraction", "calib_sf"}, - {"calib_cornering", "calib_sf"}, - {"calib_input_shaping_freq", "calib_sf"}, - {"calib_input_shaping_damp", "calib_sf"}, - {"calib_vfa", "calib_sf"}, - // View - {"reset_window_layout", "toolbar_reset"}, - // Object - {"obj_delete", "menu_delete"}, - {"obj_delete_all", "menu_remove"}, - {"obj_mirror_x", "menu_mirror_x"}, - {"obj_mirror_y", "menu_mirror_y"}, - {"obj_mirror_z", "menu_mirror_z"}, - {"obj_split_objects", "menu_split_objects"}, - {"obj_split_parts", "menu_split_parts"}, - {"obj_drop", "toolbar_flatten"}, - {"obj_instances_up", "instance_add"}, - {"obj_instances_down", "instance_remove"}, - {"obj_arrange", "toolbar_arrange"}, - {"obj_orient", "toolbar_orient"}, - // Add Primitive - {"add_primitive_cube", "menu_obj_cube"}, - {"add_primitive_cylinder", "menu_obj_cylinder"}, - {"add_primitive_sphere", "menu_obj_sphere"}, - {"add_primitive_cone", "menu_obj_cone"}, - {"add_primitive_disc", "menu_obj_disc"}, - {"add_primitive_torus", "menu_obj_torus"}, - {"add_primitive_text", "menu_obj_text"}, - {"add_primitive_svg", "menu_obj_svg"}, - // Plate - {"plate_add", "toolbar_add_plate"}, - {"plate_duplicate", "menu_copy"}, - {"plate_delete", "menu_delete"}, - {"plate_rename", "plate_name_edit"}, - {"plate_toggle_lock", "lock_normal"}, - {"plate_goto", "go_next_plate"}, - // Printer / Presets - {"sync_ams", "ams_fila_sync"}, - {"sync_presets", "printer_sync_ok"}, - {"preset_bundle", "menu_edit_preset"}, - // Import - {"import_file", "menu_import"}, - {"import_zip_archive", "menu_import"}, - {"import_configs", "menu_import"}, - // Help - {"help_open_config_folder", "folder-closed"}, - {"help_tip_of_the_day", "info"}, - {"help_check_updates", "ams_refresh_normal"}, - {"help_about", "OrcaSlicer_about"}, - {"open_wiki", "link_wiki_img"}, - }; - return icons; -} + std::string command_key; + + AppActionRunResult run(const std::string& param) const override { return NativeCommands::run(command_key, param); } + + explicit CommandAction(const NativeCommand& c) + : AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, c.title, kOrcaSourceKey, kOrcaSourceName) + , command_key(c.key) + { + this->kind = AppActionKind::Command; + this->group = c.group; + this->input = c.input; + this->icon = c.icon; + } +}; std::vector<NativeCommand> build_command_catalog() { std::vector<NativeCommand> out; auto add = [&](std::string key, std::string title, std::string group, std::function<AppActionRunResult(const std::string&)> runner, - std::string input = {}) { - std::string icon; - if (auto it = command_icons().find(key); it != command_icons().end()) - icon = it->second; + std::string input = {}, std::string icon = {}) { out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(icon), std::move(runner)}); }; + // Presentation-first overload: keeps the tile icon next to the title/group it belongs to. + auto add_with_icon = [&](std::string key, std::string title, std::string group, std::string icon, + std::function<AppActionRunResult(const std::string&)> runner, std::string input = {}) { + add(std::move(key), std::move(title), std::move(group), std::move(runner), std::move(input), std::move(icon)); + }; // ---- Slice & Export ---- - add("slice_and_preview", _u8L("Slice and Preview"), _u8L("Slice & Export"), [](const std::string&) { + add_with_icon("slice_and_preview", _u8L("Slice and Preview"), _u8L("Slice & Export"), "media_play", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (plater) { plater->reslice(); @@ -244,8 +167,8 @@ std::vector<NativeCommand> build_command_catalog() return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add( - "go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"), + add_with_icon( + "go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"), "height_range_layer", [](const std::string& param) { Plater* plater = wxGetApp().plater(); if (plater) { @@ -258,47 +181,52 @@ std::vector<NativeCommand> build_command_catalog() }, "percent"); - // "go_to_tab" is two-phase: the palette collects the tab after activating it, so dispatch here - // is a no-op (the jump goes through the go_to_tab web command). + // "go_to_tab" is two-phase: the palette collects the tab after activating it, then hands the tab + // id back as `param` (same contract as go_to_layer's percent). add( "go_to_tab", _u8L("Go to tab..."), _u8L("Commands"), - [](const std::string&) { return AppActionRunResult{AppActionRunResult::Level::Success}; }, "tab"); + [](const std::string& param) { + if (MainFrame* mf = wxGetApp().mainframe; mf && !param.empty()) + mf->select_tab(from_u8(param)); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }, + "tab"); - add("load_project", _u8L("Load Project"), _u8L("Commands"), [](const std::string&) { + add_with_icon("load_project", _u8L("Load Project"), _u8L("Commands"), "menu_open", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->load_project(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("save_project", _u8L("Save Project"), _u8L("Commands"), [](const std::string&) { + add_with_icon("save_project", _u8L("Save Project"), _u8L("Commands"), "menu_save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->save_project(false); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("save_project_as", _u8L("Save Project As"), _u8L("Commands"), [](const std::string&) { + add_with_icon("save_project_as", _u8L("Save Project As"), _u8L("Commands"), "menu_save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->save_project(true); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("open_preferences", _u8L("Preferences"), _u8L("Commands"), [](const std::string&) { + add_with_icon("open_preferences", _u8L("Preferences"), _u8L("Commands"), "cog", [](const std::string&) { wxGetApp().open_preferences(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // ---- Mode ---- - add("mode_simple", _u8L("Mode: Simple"), _u8L("Mode"), [](const std::string&) { - select_mode(comSimple); + add_with_icon("mode_simple", _u8L("Mode: Simple"), _u8L("Mode"), "advanced", [](const std::string&) { + wxGetApp().set_mode(comSimple); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode"), [](const std::string&) { - select_mode(comAdvanced); + add_with_icon("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode"), "advanced", [](const std::string&) { + wxGetApp().set_mode(comAdvanced); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("mode_expert", _u8L("Mode: Expert"), _u8L("Mode"), [](const std::string&) { - select_mode(comExpert); + add_with_icon("mode_expert", _u8L("Mode: Expert"), _u8L("Mode"), "advanced", [](const std::string&) { + wxGetApp().set_mode(comExpert); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // Mirrors Preferences > Developer > Developer mode: flip the flag, persist, refresh the UI. - add("toggle_developer_mode", _u8L("Toggle Developer Mode"), _u8L("Mode"), [](const std::string&) { + add_with_icon("toggle_developer_mode", _u8L("Toggle Developer Mode"), _u8L("Mode"), "advanced", [](const std::string&) { GUI_App& app = wxGetApp(); const bool on = !app.app_config->get_bool("developer_mode"); app.app_config->set_bool("developer_mode", on); @@ -308,50 +236,50 @@ std::vector<NativeCommand> build_command_catalog() }); // ---- Export pipeline ---- - add("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), [](const std::string&) { + add_with_icon("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), "menu_export_gcode", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_gcode(false); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("export_stl", _u8L("Export STL"), _u8L("Slice & Export"), [](const std::string&) { + add_with_icon("export_stl", _u8L("Export STL"), _u8L("Slice & Export"), "menu_export_stl", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("export_3mf", _u8L("Export 3MF"), _u8L("Slice & Export"), [](const std::string&) { + add_with_icon("export_3mf", _u8L("Export 3MF"), _u8L("Slice & Export"), "menu_save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_core_3mf(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"), [](const std::string&) { + add_with_icon("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"), "menu_export_sliced_file", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_gcode_3mf(false); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), [](const std::string&) { + add_with_icon("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), "menu_export_sliced_file", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_gcode_3mf(true); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // ---- Calibration ---- - add("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), + add_with_icon("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), "calib_sf", [](const std::string&) { return calib_command(CalibKind::Temperature); }); - add("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"), + add_with_icon("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"), "calib_sf", [](const std::string&) { return calib_command(CalibKind::MaxVolumetric); }); - add("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"), + add_with_icon("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"), "calib_sf", [](const std::string&) { return calib_command(CalibKind::PressureAdvance); }); - add("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"), + add_with_icon("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"), "calib_sf", [](const std::string&) { return calib_command(CalibKind::FlowRatio); }); - add("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"), + add_with_icon("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"), "calib_sf", [](const std::string&) { return calib_command(CalibKind::Retraction); }); - add("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"), + add_with_icon("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"), "calib_sf", [](const std::string&) { return calib_command(CalibKind::Cornering); }); - add("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"), + add_with_icon("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"), "calib_sf", [](const std::string&) { return calib_command(CalibKind::InputShapingFreq); }); - add("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), + add_with_icon("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), "calib_sf", [](const std::string&) { return calib_command(CalibKind::InputShapingDamp); }); - add("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), [](const std::string&) { return calib_command(CalibKind::VFA); }); + add_with_icon("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), "calib_sf", [](const std::string&) { return calib_command(CalibKind::VFA); }); // ---- View ---- for (auto [key, dir, title] : @@ -386,39 +314,39 @@ std::vector<NativeCommand> build_command_catalog() plater->get_camera().select_next_type(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("reset_window_layout", _u8L("Reset Window Layout"), _u8L("View"), [](const std::string&) { + add_with_icon("reset_window_layout", _u8L("Reset Window Layout"), _u8L("View"), "toolbar_reset", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->reset_window_layout(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // ---- Object ---- - add("obj_delete", _u8L("Delete Selected"), _u8L("Object"), [](const std::string&) { + add_with_icon("obj_delete", _u8L("Delete Selected"), _u8L("Object"), "menu_delete", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->remove_selected(); }); }); - add("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"), [](const std::string&) { + add_with_icon("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"), "menu_remove", [](const std::string&) { return object_op( wxGetApp().plater(), [](Plater* p) { return p->can_delete_all(); }, [](Plater* p) { p->delete_all_objects_from_model(); }); }); - add("obj_mirror_x", _u8L("Mirror X"), _u8L("Object"), [](const std::string&) { + add_with_icon("obj_mirror_x", _u8L("Mirror X"), _u8L("Object"), "menu_mirror_x", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::X); }); }); - add("obj_mirror_y", _u8L("Mirror Y"), _u8L("Object"), [](const std::string&) { + add_with_icon("obj_mirror_y", _u8L("Mirror Y"), _u8L("Object"), "menu_mirror_y", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Y); }); }); - add("obj_mirror_z", _u8L("Mirror Z"), _u8L("Object"), [](const std::string&) { + add_with_icon("obj_mirror_z", _u8L("Mirror Z"), _u8L("Object"), "menu_mirror_z", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Z); }); }); - add("obj_split_objects", _u8L("Split to Objects"), _u8L("Object"), [](const std::string&) { + add_with_icon("obj_split_objects", _u8L("Split to Objects"), _u8L("Object"), "menu_split_objects", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_objects(); }, [](Plater* p) { p->split_object(true); }); }); - add("obj_split_parts", _u8L("Split to Parts"), _u8L("Object"), [](const std::string&) { + add_with_icon("obj_split_parts", _u8L("Split to Parts"), _u8L("Object"), "menu_split_parts", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_volumes(); }, [](Plater* p) { p->split_volume(); }); }); add("obj_center", _u8L("Center Selected on Plate"), _u8L("Object"), [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); }); }); - add("obj_drop", _u8L("Drop to Bed"), _u8L("Object"), [](const std::string&) { + add_with_icon("obj_drop", _u8L("Drop to Bed"), _u8L("Object"), "toolbar_flatten", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); }); }); add("obj_fit_volume", _u8L("Scale to Fit Print Volume"), _u8L("Object"), [](const std::string&) { @@ -426,24 +354,24 @@ std::vector<NativeCommand> build_command_catalog() wxGetApp().plater(), [](Plater* p) { return p->can_scale_to_print_volume(); }, [](Plater* p) { p->scale_selection_to_fit_print_volume(); }); }); - add("obj_instances_up", _u8L("Increase Instances"), _u8L("Object"), [](const std::string&) { + add_with_icon("obj_instances_up", _u8L("Increase Instances"), _u8L("Object"), "instance_add", [](const std::string&) { return object_op( wxGetApp().plater(), [](Plater* p) { return p->can_increase_instances(); }, [](Plater* p) { p->increase_instances(); }); }); - add("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object"), [](const std::string&) { + add_with_icon("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object"), "instance_remove", [](const std::string&) { return object_op( wxGetApp().plater(), [](Plater* p) { return p->can_decrease_instances(); }, [](Plater* p) { p->decrease_instances(); }); }); - add("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object"), [](const std::string&) { + add_with_icon("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object"), "toolbar_arrange", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->arrange(); }); }); - add("obj_orient", _u8L("Auto-Orient"), _u8L("Object"), [](const std::string&) { + add_with_icon("obj_orient", _u8L("Auto-Orient"), _u8L("Object"), "toolbar_orient", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); }); }); // ---- Add Primitive ---- (the Add > Add Primitive submenu; creates a new object) - auto add_primitive = [&](std::string key, std::string title, const char* type_name) { - add(std::move(key), std::move(title), _u8L("Add Primitive"), [type_name](const std::string&) { + auto add_primitive = [&](std::string key, std::string title, std::string icon, const char* type_name) { + add_with_icon(std::move(key), std::move(title), _u8L("Add Primitive"), std::move(icon), [type_name](const std::string&) { Plater* plater = wxGetApp().plater(); if (plater) { ensure_3d_view(plater); @@ -453,13 +381,13 @@ std::vector<NativeCommand> build_command_catalog() return AppActionRunResult{AppActionRunResult::Level::Success}; }); }; - add_primitive("add_primitive_cube", _u8L("Cube"), "Cube"); - add_primitive("add_primitive_cylinder", _u8L("Cylinder"), "Cylinder"); - add_primitive("add_primitive_sphere", _u8L("Sphere"), "Sphere"); - add_primitive("add_primitive_cone", _u8L("Cone"), "Cone"); - add_primitive("add_primitive_disc", _u8L("Disc"), "Disc"); - add_primitive("add_primitive_torus", _u8L("Torus"), "Torus"); - add("add_primitive_text", _u8L("Text"), _u8L("Add Primitive"), [](const std::string&) { + add_primitive("add_primitive_cube", _u8L("Cube"), "menu_obj_cube", "Cube"); + add_primitive("add_primitive_cylinder", _u8L("Cylinder"), "menu_obj_cylinder", "Cylinder"); + add_primitive("add_primitive_sphere", _u8L("Sphere"), "menu_obj_sphere", "Sphere"); + add_primitive("add_primitive_cone", _u8L("Cone"), "menu_obj_cone", "Cone"); + add_primitive("add_primitive_disc", _u8L("Disc"), "menu_obj_disc", "Disc"); + add_primitive("add_primitive_torus", _u8L("Torus"), "menu_obj_torus", "Torus"); + add_with_icon("add_primitive_text", _u8L("Text"), _u8L("Add Primitive"), "menu_obj_text", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (plater) { ensure_3d_view(plater); @@ -469,7 +397,7 @@ std::vector<NativeCommand> build_command_catalog() } return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("add_primitive_svg", _u8L("SVG"), _u8L("Add Primitive"), [](const std::string&) { + add_with_icon("add_primitive_svg", _u8L("SVG"), _u8L("Add Primitive"), "menu_obj_svg", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (plater) { ensure_3d_view(plater); @@ -493,7 +421,7 @@ std::vector<NativeCommand> build_command_catalog() } // ---- Plate ---- - add("plate_add", _u8L("Add Plate"), _u8L("Plate"), [](const std::string&) { + add_with_icon("plate_add", _u8L("Add Plate"), _u8L("Plate"), "toolbar_add_plate", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (!is_fff_plater(plater)) return plate_unavailable(); @@ -502,7 +430,7 @@ std::vector<NativeCommand> build_command_catalog() plater->add_plate(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("plate_duplicate", _u8L("Duplicate Plate"), _u8L("Plate"), [](const std::string&) { + add_with_icon("plate_duplicate", _u8L("Duplicate Plate"), _u8L("Plate"), "menu_copy", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (!is_fff_plater(plater)) return plate_unavailable(); @@ -511,7 +439,7 @@ std::vector<NativeCommand> build_command_catalog() plater->duplicate_plate(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("plate_delete", _u8L("Delete Plate"), _u8L("Plate"), [](const std::string&) { + add_with_icon("plate_delete", _u8L("Delete Plate"), _u8L("Plate"), "menu_delete", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (!is_fff_plater(plater)) return plate_unavailable(); @@ -520,7 +448,7 @@ std::vector<NativeCommand> build_command_catalog() plater->delete_plate(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("plate_rename", _u8L("Rename Plate"), _u8L("Plate"), [](const std::string&) { + add_with_icon("plate_rename", _u8L("Rename Plate"), _u8L("Plate"), "plate_name_edit", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (!is_fff_plater(plater)) return plate_unavailable(); @@ -531,7 +459,7 @@ std::vector<NativeCommand> build_command_catalog() curr->set_plate_name(dlg.get_plate_name().ToUTF8().data()); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("plate_toggle_lock", _u8L("Toggle Plate Lock"), _u8L("Plate"), [](const std::string&) { + add_with_icon("plate_toggle_lock", _u8L("Toggle Plate Lock"), _u8L("Plate"), "lock_normal", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (!is_fff_plater(plater)) return plate_unavailable(); @@ -541,7 +469,7 @@ std::vector<NativeCommand> build_command_catalog() plates.lock_plate(index, !plates.is_locked(index)); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("plate_goto", _u8L("Go to Plate"), _u8L("Plate"), [](const std::string& param) { + add_with_icon("plate_goto", _u8L("Go to Plate"), _u8L("Plate"), "go_next_plate", [](const std::string& param) { Plater* plater = wxGetApp().plater(); if (!is_fff_plater(plater)) return plate_unavailable(); @@ -559,7 +487,7 @@ std::vector<NativeCommand> build_command_catalog() }); // ---- Printer / device connection ---- - add("sync_ams", _u8L("Synchronize Filament List from AMS"), _u8L("Printer"), [](const std::string&) { + add_with_icon("sync_ams", _u8L("Synchronize Filament List from AMS"), _u8L("Printer"), "ams_fila_sync", [](const std::string&) { Plater* plater = wxGetApp().plater(); DeviceManager* dev = wxGetApp().getDeviceManager(); if (dev && dev->get_selected_machine() && plater) { @@ -570,11 +498,11 @@ std::vector<NativeCommand> build_command_catalog() }); // ---- Presets / cloud ---- - add("preset_bundle", _u8L("Open Preset Bundle"), _u8L("Presets"), [](const std::string&) { + add_with_icon("preset_bundle", _u8L("Open Preset Bundle"), _u8L("Presets"), "menu_edit_preset", [](const std::string&) { wxGetApp().open_presetbundledialog(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("sync_presets", _u8L("Sync Presets"), _u8L("Presets"), [](const std::string&) { + add_with_icon("sync_presets", _u8L("Sync Presets"), _u8L("Presets"), "printer_sync_ok", [](const std::string&) { if (!wxGetApp().is_user_login()) return AppActionRunResult{AppActionRunResult::Level::Info, _L("Sign in to sync presets.")}; wxGetApp().restart_sync_user_preset(); @@ -582,7 +510,7 @@ std::vector<NativeCommand> build_command_catalog() }); // ---- Import ---- - add("import_file", _u8L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), _u8L("Import"), [](const std::string&) { + add_with_icon("import_file", _u8L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), _u8L("Import"), "menu_import", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) { #ifdef __APPLE__ plater->add_model(); @@ -592,39 +520,39 @@ std::vector<NativeCommand> build_command_catalog() } return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("import_zip_archive", _u8L("Import ZIP Archive"), _u8L("Import"), [](const std::string&) { + add_with_icon("import_zip_archive", _u8L("Import ZIP Archive"), _u8L("Import"), "menu_import", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->import_zip_archive(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("import_configs", _u8L("Import Configs"), _u8L("Import"), [](const std::string&) { + add_with_icon("import_configs", _u8L("Import Configs"), _u8L("Import"), "menu_import", [](const std::string&) { if (MainFrame* mf = wxGetApp().mainframe) mf->load_config_file(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // ---- Export extras ---- - add("export_stl_multi", _u8L("Export All Objects as STLs"), _u8L("Export"), [](const std::string&) { + add_with_icon("export_stl_multi", _u8L("Export All Objects as STLs"), _u8L("Export"), "menu_export_stl", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(false, false, true); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("export_drc_single", _u8L("Export All Objects as DRC (one file)"), _u8L("Export"), [](const std::string&) { + add_with_icon("export_drc_single", _u8L("Export All Objects as DRC (one file)"), _u8L("Export"), "menu_export_stl", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(false, false, false, FT_DRC); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("export_drc_multi", _u8L("Export All Objects as DRCs"), _u8L("Export"), [](const std::string&) { + add_with_icon("export_drc_multi", _u8L("Export All Objects as DRCs"), _u8L("Export"), "menu_export_stl", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(false, false, true, FT_DRC); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("export_toolpaths_obj", _u8L("Export Toolpaths as OBJ"), _u8L("Export"), [](const std::string&) { + add_with_icon("export_toolpaths_obj", _u8L("Export Toolpaths as OBJ"), _u8L("Export"), "menu_export_toolpaths", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_toolpaths_to_obj(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("export_config", _u8L("Export Preset Bundle"), _u8L("Export"), [](const std::string&) { + add_with_icon("export_config", _u8L("Export Preset Bundle"), _u8L("Export"), "menu_export_config", [](const std::string&) { if (MainFrame* mf = wxGetApp().mainframe) mf->export_config(); return AppActionRunResult{AppActionRunResult::Level::Success}; @@ -639,7 +567,7 @@ std::vector<NativeCommand> build_command_catalog() wxGetApp().ShowUserGuide(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("help_open_config_folder", _u8L("Show Configuration Folder"), _u8L("Help"), [](const std::string&) { + add_with_icon("help_open_config_folder", _u8L("Show Configuration Folder"), _u8L("Help"), "folder-closed", [](const std::string&) { Slic3r::GUI::desktop_open_datadir_folder(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); @@ -652,7 +580,7 @@ std::vector<NativeCommand> build_command_catalog() dlg.ShowModal(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("help_tip_of_the_day", _u8L("Show Tip of the Day"), _u8L("Help"), [](const std::string&) { + add_with_icon("help_tip_of_the_day", _u8L("Show Tip of the Day"), _u8L("Help"), "info", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) { plater->get_dailytips()->open(); if (GLCanvas3D* canvas = plater->get_current_canvas3D()) @@ -660,15 +588,15 @@ std::vector<NativeCommand> build_command_catalog() } return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("help_check_updates", _u8L("Check for Updates"), _u8L("Help"), [](const std::string&) { + add_with_icon("help_check_updates", _u8L("Check for Updates"), _u8L("Help"), "ams_refresh_normal", [](const std::string&) { wxGetApp().check_new_version_sf(true, 1); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("help_about", _u8L("About OrcaSlicer"), _u8L("Help"), [](const std::string&) { + add_with_icon("help_about", _u8L("About OrcaSlicer"), _u8L("Help"), "OrcaSlicer_about", [](const std::string&) { Slic3r::GUI::about(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add("open_wiki", _u8L("Open Wiki"), _u8L("Help"), [](const std::string&) { + add_with_icon("open_wiki", _u8L("Open Wiki"), _u8L("Help"), "link_wiki_img", [](const std::string&) { wxLaunchDefaultBrowser("https://www.orcaslicer.com/wiki/", wxBROWSER_NEW_WINDOW); return AppActionRunResult{AppActionRunResult::Level::Success}; }); @@ -706,6 +634,11 @@ const std::vector<NativeCommand>& NativeCommands::catalog() return commands; } +std::unique_ptr<AppAction> NativeCommands::make_action(const NativeCommand& command) +{ + return std::make_unique<CommandAction>(command); +} + AppActionRunResult NativeCommands::run(const std::string& key, const std::string& param) { GUI_App& app = wxGetApp(); diff --git a/src/slic3r/GUI/NativeCommands.hpp b/src/slic3r/GUI/NativeCommands.hpp index 52143ed506..32ae6ea898 100644 --- a/src/slic3r/GUI/NativeCommands.hpp +++ b/src/slic3r/GUI/NativeCommands.hpp @@ -1,17 +1,18 @@ #pragma once #include <functional> +#include <memory> #include <string> #include <vector> -#include "ActionRegistry.hpp" // for AppActionRunResult +#include "ActionRegistry.hpp" // for AppAction / AppActionRunResult namespace Slic3r { namespace GUI { -// A built-in speed-dial command: identity + how to run it. The registry keeps commands as thin -// values (CommandAction) and routes run() here, so this catalog is the single source of truth for -// the behaviour (runner => an owner method), the presentation (title/group/input), and the tile -// pictogram (icon = an SVG base name under resources/images, "" for no icon). +// A built-in speed-dial command: identity + how to run it. make_action() wraps a value as a thin +// AppAction for the registry, so this catalog is the single source of truth for the behaviour +// (runner => an owner method), the presentation (title/group/input), and the tile pictogram +// (icon = an SVG base name under resources/images, "" for no icon). struct NativeCommand { std::string key; @@ -28,6 +29,11 @@ const std::vector<NativeCommand>& catalog(); // Dispatches `key` to its runner (unknown keys return a quiet Info). UI thread only. AppActionRunResult run(const std::string& key, const std::string& param = {}); + +// Materialises one catalog entry as a runnable AppAction. Keeps the catalog's identity, +// presentation and behaviour as the single source of truth; ActionRegistry only stores and +// dispatches the result. UI thread only. +std::unique_ptr<AppAction> make_action(const NativeCommand& command); } // namespace NativeCommands }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index a9eaffbc8d..7b69effacc 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -267,6 +267,12 @@ wxString ButtonsListCtrl::GetPageText(size_t n) const return btn->GetLabel(); } +// ORCA +wxString ButtonsListCtrl::GetPageLabel(size_t n) const +{ + return n < m_pageLabels.size() ? m_pageLabels[n] : wxString(); +} + // ORCA void ButtonsListCtrl::SetOverflowButton(wxWindow* button) { diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp index 6e166a337c..cc6ad91f21 100644 --- a/src/slic3r/GUI/Notebook.hpp +++ b/src/slic3r/GUI/Notebook.hpp @@ -33,8 +33,14 @@ public: void SetPageText(size_t n, const wxString& strText); void SetCompact(size_t n, bool compact); // ORCA wxString GetPageText(size_t n) const; + // ORCA: the full page label, unaffected by SetCompact() blanking the button text. + wxString GetPageLabel(size_t n) const; // Resource name the page was inserted with (empty for plugin pages, which pass a wxBitmap). - const std::string& GetPageIcon(size_t n) const { return m_pageIcons[n]; } + const std::string& GetPageIcon(size_t n) const + { + static const std::string empty; + return n < m_pageIcons.size() ? m_pageIcons[n] : empty; + } wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA // ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g. // an overflow indicator. Pass nullptr to remove it; ownership stays with the caller. @@ -244,6 +250,13 @@ public: return GetBtnsListCtrl()->GetPageText(n); } + // ORCA: the real page label. GetPageText() returns the button label, which SetCompact() blanks. + wxString GetPageLabel(size_t n) const + { + wxCHECK_MSG(n < GetPageCount(), wxString(), wxS("Invalid page")); + return GetBtnsListCtrl()->GetPageLabel(n); + } + // Resource icon name the page was inserted with; empty for pages added with a wxBitmap. std::string GetPageIcon(size_t n) const { diff --git a/src/slic3r/GUI/PluginsDialog.cpp b/src/slic3r/GUI/PluginsDialog.cpp index 4f97a7d089..8bee3919b2 100644 --- a/src/slic3r/GUI/PluginsDialog.cpp +++ b/src/slic3r/GUI/PluginsDialog.cpp @@ -543,15 +543,16 @@ bool install_local_plugin_package(const boost::filesystem::path& package_file, w }); timer->Start(100); - bool finished = false; - wxEventLoop loop; - auto on_finish = [&finished, &loop]() { - finished = true; - if (loop.IsRunning()) - loop.Exit(); + // finished/loop live on the heap: the worker's completion callback is posted to the UI loop + // and can still fire after this stack frame is gone, so it must not reference locals. + struct WaitState + { + bool finished = false; + wxEventLoop loop; }; + auto wait = std::make_shared<WaitState>(); - std::thread([state, package_file, on_finish]() mutable { + std::thread([state, package_file, wait]() mutable { std::string error; bool ok = false; try { @@ -570,11 +571,17 @@ bool install_local_plugin_package(const boost::filesystem::path& package_file, w state->ok = ok; state->error = std::move(error); } - wxTheApp->CallAfter(on_finish); + if (!wxTheApp) + return; + wxTheApp->CallAfter([wait]() { + wait->finished = true; + if (wait->loop.IsRunning()) + wait->loop.Exit(); + }); }).detach(); - if (!finished) - loop.Run(); + if (!wait->finished) + wait->loop.Run(); timer->Stop(); delete timer; diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 7d80147efa..c5f9bf1f89 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1723,6 +1723,11 @@ void PreferencesDialog::create_items() auto item_multi_machine = create_item_checkbox(_L("Multi device management"), _L("With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices."), "enable_multi_machine", _L("(Requires restart)")); g_sizer->Add(item_multi_machine); + auto item_speed_dial = create_item_checkbox(_L("Open the Speed Dial with the Space key"), + _L("When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page."), + "enable_speed_dial"); + g_sizer->Add(item_speed_dial); + #if 0 g_sizer->Add(create_item_title(_L("Filament Grouping")), 1, wxEXPAND); //temporarily disable it diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index aece404515..21aa8b7350 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -322,7 +322,9 @@ void OptionsSearcher::init(std::vector<InputInfo> input_values) void OptionsSearcher::apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode) { - if (options.empty()) return; + // options_all_modes is a separate consumer (the Speed Dial), so "nothing initialised yet" means + // both views are empty - the mode-filtered options can be empty while the all-modes view is not. + if (options.empty() && options_all_modes.empty()) return; options.erase(std::remove_if(options.begin(), options.end(), [type](Option opt) { return opt.type == type; }), options.end()); options_all_modes.erase(std::remove_if(options_all_modes.begin(), options_all_modes.end(), [type](Option opt) { return opt.type == type; }), diff --git a/src/slic3r/GUI/Search.hpp b/src/slic3r/GUI/Search.hpp index b32f2b11ca..bf2441e15c 100644 --- a/src/slic3r/GUI/Search.hpp +++ b/src/slic3r/GUI/Search.hpp @@ -156,10 +156,6 @@ public: void dlg_sys_color_changed(); void dlg_msw_rescale(); - // The full gated option set built by init() (after visibility/mode/printer-tech filtering). - // Used by the Speed Dial to materialise config settings as first-class actions. - const std::vector<Option>& all_options() const { return options; } - // Every option across all UI modes (Developer included), regardless of the current mode. // Used by the Speed Dial so it can list settings the user would have to switch mode to edit. const std::vector<Option>& all_modes_options() const { return options_all_modes; } diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index 88913b2136..af079d48e4 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -9,8 +9,6 @@ #include "Plater.hpp" #include "Widgets/WebViewHostDialog.hpp" -#include <libslic3r/AppConfig.hpp> - #include <algorithm> #include <wx/display.h> @@ -156,15 +154,7 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload) run_action(payload.value("id", ""), payload.value("title", ""), payload.value("param", "")); 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") + else if (command == "resize") resize_to_content(json_int_or(payload, "height", 0)); } @@ -218,8 +208,7 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti _L("Developer setting"), wxOK | wxCANCEL); if (dlg.ShowModal() != wxID_OK) return; - wxGetApp().app_config->set_bool("developer_mode", true); - wxGetApp().update_mode(); + wxGetApp().enable_developer_mode(); } else { RichMessageDialog dlg(wxGetApp().mainframe, wxString::Format(_L("\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?"), diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index 8c5e074a2e..59a81d21fa 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -29,14 +29,14 @@ public: } // namespace -TEST_CASE("AppAction composes a stable id from prefix:title:source_key", "[speeddial][actions]") +TEST_CASE("AppAction composes a stable id from prefix:title:source_key", "[ActionSource][SpeedDial]") { CHECK(AppAction::compose_id("test", "Action title", "src-key") == "test:Action title:src-key"); // source_key (not the display name) carries identity, so it is the third field. CHECK(AppAction::compose_id("script", "Do Thing", "pack.py") == "script:Do Thing:pack.py"); } -TEST_CASE("AppAction definitions are immutable after construction", "[speeddial][actions]") +TEST_CASE("AppAction definitions are immutable after construction", "[ActionSource][SpeedDial]") { using StringAccessor = const std::string& (AppAction::*) () const; @@ -52,7 +52,7 @@ TEST_CASE("AppAction definitions are immutable after construction", "[speeddial] CHECK(action.source_name() == "Action source"); } -TEST_CASE("ActionRegistry takes exclusive ownership of published actions", "[speeddial][actions]") +TEST_CASE("ActionRegistry takes exclusive ownership of published actions", "[ActionSource][SpeedDial]") { using ExpectedUpsert = void (ActionRegistry::*)(std::unique_ptr<AppAction>); @@ -61,7 +61,7 @@ TEST_CASE("ActionRegistry takes exclusive ownership of published actions", "[spe // A dynamic "Go to Plate N" action is keyed by plate index (not the display title), so renaming // a plate never re-keys it - the same contract as a setting action. -TEST_CASE("Go-to-plate actions are keyed by index, not title", "[speeddial][actions]") +TEST_CASE("Go-to-plate actions are keyed by index, not title", "[ActionSource][SpeedDial]") { CHECK(AppAction::compose_id("orca_plate_goto", "0", "orca") == "orca_plate_goto:0:orca"); CHECK(AppAction::compose_id("orca_plate_goto", "2", "orca") == "orca_plate_goto:2:orca"); @@ -69,7 +69,7 @@ TEST_CASE("Go-to-plate actions are keyed by index, not title", "[speeddial][acti // A dynamic "Open recent project" action is keyed by file path (not the display name), so renaming // a project or reordering the recents list never re-keys it - the same contract as a setting action. -TEST_CASE("Recent-project actions are keyed by path, not title", "[speeddial][actions]") +TEST_CASE("Recent-project actions are keyed by path, not title", "[ActionSource][SpeedDial]") { CHECK(AppAction::compose_id("orca_recent_project", "/a/b/project.3mf", "orca") == "orca_recent_project:/a/b/project.3mf:orca"); @@ -79,7 +79,7 @@ TEST_CASE("Recent-project actions are keyed by path, not title", "[speeddial][ac // A built-in command is keyed by its stable catalog key (not the localized display title), so a // rename or a UI-language switch never re-keys the action and its persisted favourite/stats survive. -TEST_CASE("Command actions are keyed by catalog key, not display title", "[speeddial][actions]") +TEST_CASE("Command actions are keyed by catalog key, not display title", "[ActionSource][SpeedDial]") { CHECK(AppAction::compose_id("orca_command", "save_project", "orca") == "orca_command:save_project:orca"); // The second field is the stable key, so distinct commands never collide. @@ -87,11 +87,42 @@ TEST_CASE("Command actions are keyed by catalog key, not display title", "[speed AppAction::compose_id("orca_command", "load_project", "orca")); } +// The real catalog -> action mapping keys by the stable catalog key and copies presentation from the +// catalog, so a rename or a UI-language switch never re-keys the action. +TEST_CASE("Command action construction keys by catalog key", "[ActionSource][SpeedDial]") +{ + const std::vector<Slic3r::GUI::NativeCommand>& commands = Slic3r::GUI::NativeCommands::catalog(); + REQUIRE_FALSE(commands.empty()); + const Slic3r::GUI::NativeCommand& c = commands.front(); + + std::unique_ptr<AppAction> action = Slic3r::GUI::NativeCommands::make_action(c); + REQUIRE(action != nullptr); + CHECK(action->id() == AppAction::compose_id("orca_command", c.key, "orca")); + CHECK(action->id() != AppAction::compose_id("orca_command", c.title, "orca")); + CHECK(action->title() == c.title); + CHECK(action->group == c.group); + CHECK(action->input == c.input); + CHECK(action->icon == c.icon); +} + +// Two-phase commands declare the input the palette must collect before they can run. +TEST_CASE("Two-phase commands declare their input phase", "[ActionSource][SpeedDial]") +{ + auto input_of = [](const std::string& key) -> std::string { + for (const auto& c : Slic3r::GUI::NativeCommands::catalog()) + if (c.key == key) + return c.input; + return {}; + }; + CHECK(input_of("go_to_layer") == "percent"); + CHECK(input_of("go_to_tab") == "tab"); +} + // The quick-launch cap must stay 10 to match the numbered Alt/Option+1..9,0 keys. The web palette // mirrors it as K_FAV_LIMIT (asserted in speeddial.test.js); the C++ side pins it here. static_assert(Slic3r::GUI::ActionRegistry::kFavLimit == 10, "kFavLimit must stay 10"); -TEST_CASE("Favourite lists are capped and deduped preserving order", "[speeddial][actions]") +TEST_CASE("Favourite lists are capped and deduped preserving order", "[ActionSource][SpeedDial]") { using Slic3r::GUI::cap_favourites; @@ -101,7 +132,7 @@ TEST_CASE("Favourite lists are capped and deduped preserving order", "[speeddial CHECK(cap_favourites({"a", "b"}, 0) == std::vector<std::string>{}); } -TEST_CASE("Native command catalog has unique keys and present titles", "[speeddial][actions]") +TEST_CASE("Native command catalog has unique keys and present titles", "[ActionSource][SpeedDial]") { const std::vector<Slic3r::GUI::NativeCommand>& commands = Slic3r::GUI::NativeCommands::catalog(); CHECK_FALSE(commands.empty()); @@ -118,7 +149,7 @@ TEST_CASE("Native command catalog has unique keys and present titles", "[speeddi // Every command's tile pictogram is the SVG the matching GUI control already uses; an absent icon // means a blank tile (like the tab picker). Guard representative names and that every non-empty // value resolves to a shipped file, so a rename/typo cannot leave broken images in the palette. -TEST_CASE("Native command icons resolve to shipped SVGs", "[speeddial][actions]") +TEST_CASE("Native command icons resolve to shipped SVGs", "[ActionSource][SpeedDial]") { const std::vector<Slic3r::GUI::NativeCommand>& commands = Slic3r::GUI::NativeCommands::catalog(); auto icon_of = [&commands](const std::string& key) -> const std::string* { @@ -159,7 +190,7 @@ TEST_CASE("Native command icons resolve to shipped SVGs", "[speeddial][actions]" // The Help-menu commands, wiki/YouTube links and the developer-mode toggle are part of the palette. // Guard their presence and that they stay grouped with their peers, so a catalog edit cannot drop // or scatter them. Groups are compared to the peer's own group to stay independent of translation. -TEST_CASE("Native command catalog includes the Help and developer-mode commands", "[speeddial][actions]") +TEST_CASE("Native command catalog includes the Help and developer-mode commands", "[ActionSource][SpeedDial]") { const std::vector<Slic3r::GUI::NativeCommand>& commands = Slic3r::GUI::NativeCommands::catalog(); auto find = [&commands](const std::string& key) -> const Slic3r::GUI::NativeCommand* { @@ -187,7 +218,7 @@ TEST_CASE("Native command catalog includes the Help and developer-mode commands" // Every "Add Primitive" item and shipped handy model has a palette command, grouped as in the Add // menu. Groups are compared to a peer's own group to stay independent of translation. -TEST_CASE("Native command catalog covers the Add menus", "[speeddial][actions]") +TEST_CASE("Native command catalog covers the Add menus", "[ActionSource][SpeedDial]") { const std::vector<Slic3r::GUI::NativeCommand>& commands = Slic3r::GUI::NativeCommands::catalog(); auto group_of = [&commands](const std::string& key) -> const std::string* { @@ -221,7 +252,7 @@ TEST_CASE("Native command catalog covers the Add menus", "[speeddial][actions]") // A setting whose mode is above the user's current mode must be prompted before it can be edited. // Developer settings (comDevelop) are above every non-developer mode, so they always prompt then. -TEST_CASE("Settings above the current mode require a switch", "[speeddial][actions]") +TEST_CASE("Settings above the current mode require a switch", "[ActionSource][SpeedDial]") { using Slic3r::GUI::requires_mode_switch; using Slic3r::comAdvanced; From 81458dae8678c71d56fae8102690a8f9d3607f23 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Fri, 11 Sep 2026 14:56:59 +0800 Subject: [PATCH 17/29] Add category headers. Alphabetical sorting when no search query. Recent count adjustments --- resources/web/data/text.js | 4 +- resources/web/dialog/SpeedDial/speeddial.js | 118 ++++++++++++++++-- .../web/dialog/SpeedDial/speeddial.test.js | 81 +++++++++--- resources/web/dialog/SpeedDial/style.css | 15 +++ src/libslic3r/AppConfig.cpp | 18 +++ src/libslic3r/AppConfig.hpp | 8 ++ src/slic3r/GUI/ActionRegistry.cpp | 10 +- src/slic3r/GUI/Preferences.cpp | 10 ++ tests/libslic3r/test_appconfig.cpp | 38 ++++++ 9 files changed, 271 insertions(+), 31 deletions(-) diff --git a/resources/web/data/text.js b/resources/web/data/text.js index 1a17c5ccdb..2f6682c051 100644 --- a/resources/web/data/text.js +++ b/resources/web/data/text.js @@ -124,7 +124,9 @@ var LangText = { sd_showing: "Showing", sd_of: "of", sd_actions: "actions", - sd_recent: "recent", + sd_recent: "Recent", + sd_plugins: "Plugins", + sd_other: "Other", sd_matches: "matches", sd_tabs: "tabs", sd_no_match: "No actions match", diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 2e61165559..2045d42224 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -1,10 +1,11 @@ // Speed Dial launcher page. Static-safe module: no DOM access at load time so a -// node vm can exercise the pure helpers (searchActions / filterTabs / actionLabel / nextSel / commandList). +// node vm can exercise the pure helpers (searchActions / filterTabs / actionLabel / nextSel / +// commandList / commandSections / actionCategory / groupActions). // ---- state (populated by the C++ bridge via window.HandleStudio) ---- -var ACTIONS = []; // [{id,title,source,group,input,icon,mode}], already frecency-sorted by C++ +var ACTIONS = []; // [{id,title,source,group,kind,input,icon,mode}], already frecency-sorted by C++ var FAVS = []; // [id...] -var RECENTS = []; // [{id,title,source,group,input,icon,mode}] - last-N launched +var RECENTS = []; // [{id,title,source,group,kind,input,icon,mode}] - last-N launched var query = ""; var sel = { zone: "list", i: 0 }; // zone: 'list' | 'fav' var lastResizeHeight = 0; @@ -47,6 +48,14 @@ var renderEnd = 0; var builtKey = ""; // phase|query|listLen - when it changes, rows are rebuilt from the first window var spacerEl = null; // the trailing height spacer, always the last child of listEl +// Section headers in the empty-query list ("Recent" then one per category). sectionStarts maps a flat +// action index to the header label that sits above it. sectionTotal/Rendered count headers so the +// bottom spacer reserves the same vertical space the not-yet-rendered headers will occupy. +var SECTION_H = 30; // MUST match .dial-section height (30px) +var sectionStarts = null; +var sectionTotal = 0; +var sectionRendered = 0; + // search-cache: the normalized (folded+lowercased) needle for the current query pass. var searchNeedle = ""; @@ -274,12 +283,75 @@ function fillTile(tile, a, mono) { tile.appendChild(img); } +// Category a row is grouped under in the empty-query list. Native commands and dynamic plate/recent +// actions carry a group; settings derive their top-level preset type from the source breadcrumb +// ("Process : Quality : Layers" -> "Process"); plugins all share one header. +function actionCategory(a) { + if (!a) return T("sd_other", "Other"); + if (a.kind === "plugin") return T("sd_plugins", "Plugins"); + if (a.group) return a.group; + var src = a.source || ""; + var sep = src.indexOf(" : "); + var cat = sep === -1 ? src : src.slice(0, sep); + return cat || T("sd_other", "Other"); +} + +// Stable-bucket actions by category, then order the groups alphabetically. Within a group the incoming +// (frecency) order is kept. Pure so the node-vm test can exercise grouping. +function groupActions(list) { + var buckets = Object.create(null); + var order = []; + (list || []).forEach(function (a) { + var c = actionCategory(a); + if (!buckets[c]) { buckets[c] = []; order.push(c); } + buckets[c].push(a); + }); + order.sort(function (x, y) { + var a = x.toLowerCase(), b = y.toLowerCase(); + return a < b ? -1 : a > b ? 1 : 0; + }); + var out = []; + order.forEach(function (c) { out = out.concat(buckets[c]); }); + return out; +} + // The active list for the main phase. A typed query ranks every action (commands/plugins/settings) -// by relevance; an empty query shows the recent list (recents are a mixed bag - no discrimination). +// by relevance; an empty query shows the recents first, then the rest grouped under category headers. +// The empty-query result is cached on the action/recents array identities so scrolling doesn't regroup. +var commandListCache = null; function commandList(actions, recents, query) { + var all = actions || []; if (shouldRenderActionList(query)) - return searchActions(actions || [], query); - return (recents || []).slice(0); + return searchActions(all, query); + var rec = recents || []; + if (commandListCache && commandListCache.actions === all && commandListCache.recents === rec) + return commandListCache.list; + var recentIds = {}; + for (var i = 0; i < rec.length; i++) + recentIds[rec[i].id] = true; + var rest = all.filter(function (a) { return !recentIds[a.id]; }); + var list = rec.concat(groupActions(rest)); + commandListCache = { actions: all, recents: rec, list: list }; + return list; +} + +// Section headers for the main list: "Recent" (when recents exist) then one per category. The list is +// already grouped, so a header is emitted whenever the category changes. A typed query has no headers. +// Returns {startIndex: label}, where startIndex is the flat list index the header sits above. +function commandSections(list, recentsLen, query) { + if (shouldRenderActionList(query) || !list || !list.length) + return null; + var sections = {}; + if (recentsLen > 0) + sections[0] = T("sd_recent", "Recent"); + var prev = null; + for (var i = recentsLen; i < list.length; i++) { + var c = actionCategory(list[i]); + if (i === recentsLen || c !== prev) + sections[i] = c; + prev = c; + } + return sections; } // Resolve the selection cursor {zone,i} to the action id it points at: fav zone indexes the @@ -676,16 +748,31 @@ function renderActionRow(a, i) { return shell.row; } -// Append rows [from, to) into listEl, always inserting before the bottom spacer so row order is preserved. +// Append rows [from, to) into listEl, always inserting before the bottom spacer so row order is +// preserved. A section header is inserted just before the first row of its section. function appendActionRows(list, from, to) { var spacer = spacerEl || ensureSpacer(); for (var i = from; i < to; i++) { + if (sectionStarts && sectionStarts[i] !== undefined) { + var header = document.createElement("div"); + header.className = "dial-section"; + header.textContent = sectionStarts[i]; + listEl.insertBefore(header, spacer); + sectionRendered++; + } var row = renderActionRow(list[i], i); row.setAttribute("data-idx", i); listEl.insertBefore(row, spacer); } } +// Set the section map for the current list and reset the rendered-header counters (a fresh build). +function setSections(sections) { + sectionStarts = sections || null; + sectionTotal = sectionStarts ? Object.keys(sectionStarts).length : 0; + sectionRendered = 0; +} + // Ensure the bottom spacer exists as the last child of listEl. It is (re)created on rebuild because // listEl.innerHTML="" destroys the old node. function ensureSpacer() { @@ -697,10 +784,13 @@ function ensureSpacer() { return spacerEl; } -// Size the spacer to the un-rendered tail so the scrollbar reflects the full match count. +// Size the spacer to the un-rendered tail so the scrollbar reflects the full match count. Pending +// section headers reserve their own height too, so the last rows stay reachable. function setBottomSpacer(total) { ensureSpacer(); - spacerEl.style.height = Math.max(0, total - renderEnd) * ROW_H + "px"; + var remainingRows = Math.max(0, total - renderEnd); + var remainingHeaders = Math.max(0, sectionTotal - sectionRendered); + spacerEl.style.height = (remainingRows * ROW_H + remainingHeaders * SECTION_H) + "px"; } // Reveal rows up to `upto` (an exclusive index), appending without rebuilding the whole list. Used by @@ -720,6 +810,7 @@ function rebuildCommandsList(list) { listEl.className = "dial-list"; ensureSpacer(); renderEnd = 0; + sectionRendered = 0; appendActionRows(list, 0, Math.min(list.length, K_ROWS)); renderEnd = Math.min(list.length, K_ROWS); setBottomSpacer(list.length); @@ -754,6 +845,7 @@ function renderEmpty(text) { listEl.innerHTML = ""; spacerEl = null; listEl.className = "dial-list empty"; + setSections(null); if (countEl) countEl.hidden = true; var empty = document.createElement("div"); empty.className = "dial-empty"; @@ -782,6 +874,8 @@ function renderCommandsList() { var key = buildKey() + "|" + total; if (key !== builtKey) { builtKey = key; + // Headers split the empty-query list into recents + category groups; typed queries have none. + setSections(showList ? null : commandSections(list, (RECENTS || []).length, query)); rebuildCommandsList(list); } else if (sel.i >= renderEnd) { // Arrow-nav walked past the rendered window - reveal enough to keep the selection visible. @@ -789,9 +883,11 @@ function renderCommandsList() { } listEl.className = "dial-list"; + // The empty-query list is labelled by its section headers instead. if (countEl) { - countEl.hidden = false; - countEl.textContent = showList ? resultCountText(ACTIONS.length, total, query) : total + " " + T("sd_recent", "recent"); + countEl.hidden = !showList; + if (showList) + countEl.textContent = resultCountText(ACTIONS.length, total, query); } updateSelection(); updatePins(list); diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 7f08880510..521705d68f 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -22,16 +22,16 @@ assert.equal( "duplicate labels should use the opaque id without interpreting its contents" ); -assert.equal(ctx.shouldRenderActionList(""), false, "an empty search keeps recent/empty list"); -assert.equal(ctx.shouldRenderActionList(" "), false, "whitespace-only search keeps recent/empty list"); +assert.equal(ctx.shouldRenderActionList(""), false, "an empty search shows the recents+pool list"); +assert.equal(ctx.shouldRenderActionList(" "), false, "whitespace-only search shows the recents+pool list"); 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"); +// commandList: an empty query shows recents first, then every other action; a typed query filters all. +assert.deepEqual(ctx.commandList(duplicateActions, [], ""), duplicateActions, + "empty query + no recents shows the whole action pool"); assert.deepEqual(ctx.commandList(duplicateActions, [duplicateActions[0]], ""), - [duplicateActions[0]], - "empty query shows the recent list"); + [duplicateActions[0], duplicateActions[1]], + "empty query shows the recent first, then the remaining actions"); assert.deepEqual(ctx.commandList(duplicateActions, [], "rep"), duplicateActions, "a typed query filters actions (both identical titles match) instead of showing recents"); @@ -117,22 +117,73 @@ const negativePool = [ assert.equal(ctx.searchActions(negativePool, "ornt").length, 1, "a low-score fuzzy match is not mistaken for no match"); +// actionCategory: a command/dynamic action's group is its category; a setting uses the top-level +// source segment; every plugin shares one header; a category-less action falls back to "Other". +assert.equal(ctx.actionCategory({ id: "c", group: "Help", source: "OrcaSlicer", kind: "command" }), "Help", + "a command's group is its category"); +assert.equal(ctx.actionCategory({ id: "s", group: "", source: "Process : Quality : Layers", kind: "command" }), "Process", + "a setting's category is the top-level source segment"); +assert.equal(ctx.actionCategory({ id: "s", group: "", source: "Filament : Cooling", kind: "command" }), "Filament", + "a Filament setting groups under Filament"); +assert.equal(ctx.actionCategory({ id: "plugin_script_action:Foo:bar.py", group: "", source: "Gcode Optimizer", kind: "plugin" }), "Plugins", + "every plugin shares one Plugins header"); +assert.equal(ctx.actionCategory({ id: "x", group: "", source: "", kind: "command" }), "Other", + "a category-less action falls back to Other"); + +// groupActions: bucket by category, order the groups alphabetically, keep the incoming order within +// each group (the pool arrives frecency-sorted). +const groupPool = [ + { id: "q1", title: "Q1", source: "Quality", group: "Quality", kind: "command" }, + { id: "h1", title: "H1", source: "OrcaSlicer", group: "Help", kind: "command" }, + { id: "p1", title: "P1", source: "Process : A", group: "", kind: "command" }, + { id: "h2", title: "H2", source: "OrcaSlicer", group: "Help", kind: "command" } +]; +assert.deepEqual(ctx.groupActions(groupPool).map(function (a) { return a.id; }), ["h1", "h2", "p1", "q1"], + "groups are alphabetical and each group keeps its incoming order"); + // commandList (the main-phase list) delegates to the ranked search for a typed query and returns -// the mixed recents (no discrimination) for an empty query. +// recents + the category-grouped pool for an empty query. const mixed = [ - { id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", input: "" }, - { id: "set", title: "Sparse Infill Density", source: "Quality", group: "Quality", input: "" } + { id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", kind: "command", input: "" }, + { id: "set", title: "Sparse Infill Density", source: "Quality", group: "Quality", kind: "command", input: "" } ]; assert.equal(ctx.commandList(mixed, [], "sli")[0].id, "cmd", "a typed query keeps the relevance-ranked action list (best match first)"); -assert.deepEqual(ctx.commandList(mixed, mixed.slice(0, 1), "").map(function (a) { return a.id; }), ["cmd"], - "an empty query shows the mixed recents list verbatim"); +assert.deepEqual(ctx.commandList(mixed, mixed.slice(0, 1), "").map(function (a) { return a.id; }), ["cmd", "set"], + "an empty query shows the recents first and de-dupes them out of the tail"); +assert.deepEqual(ctx.commandList(mixed, [], "").map(function (a) { return a.id; }), ["cmd", "set"], + "empty query + no recents shows the whole action pool"); +assert.deepEqual(ctx.commandList(mixed, [mixed[1]], "").map(function (a) { return a.id; }), ["set", "cmd"], + "the recent is hoisted above the alphabetically-ordered groups"); -// selectedActionId: resolves the active list (recents for an empty query, filtered list otherwise). +// commandSections: "Recent" (when recents exist) plus one header per category in the grouped list; +// a typed query or an empty list yields no headers. Uses a computed grouped list so the recents +// hoist and the category ordering are exercised together. +const sectionPool = [ + { id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", kind: "command" }, + { id: "help", title: "Shortcuts", source: "OrcaSlicer", group: "Help", kind: "command" }, + { id: "set", title: "Infill", source: "Quality", group: "Quality", kind: "command" } +]; +const sectionList = ctx.commandList(sectionPool, [sectionPool[0]], ""); +assert.deepEqual(sectionList.map(function (a) { return a.id; }), ["cmd", "help", "set"], + "recents are hoisted, then the rest is grouped alphabetically (Commands, Help, Quality)"); +assert.deepEqual(ctx.commandSections(sectionList, 1, ""), { 0: "Recent", 1: "Help", 2: "Quality" }, + "recents + grouped actions get one header per category"); +assert.deepEqual(ctx.commandSections([sectionPool[0]], 1, ""), { 0: "Recent" }, + "a list that is all recents gets only the Recent header"); +assert.deepEqual(ctx.commandSections(ctx.commandList(sectionPool, [], ""), 0, ""), + { 0: "Commands", 1: "Help", 2: "Quality" }, + "with no recents the grouped list still gets category headers"); +assert.equal(ctx.commandSections(sectionList, 1, "sli"), null, + "a typed query has no section headers"); +assert.equal(ctx.commandSections([], 0, ""), null, + "an empty list has no section headers"); + +// selectedActionId: resolves the active list (recents+pool for an empty query, filtered list otherwise). assert.equal( ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), []), - null, - "Enter with an empty query and no recents must not resolve to an action the list never showed" + "0123456789abcdef", + "Enter with an empty query resolves the first action in the recents+pool list" ); assert.equal( ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), []), diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index 3aea0cf15a..6aaca682d9 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -279,6 +279,21 @@ body { overflow-y: hidden; } +/* Section header in the empty-query list ("Recent" then one per category). Its 30px height MUST match + SECTION_H in speeddial.js, which reserves header space in the windowed-list bottom spacer. */ +.dial-section { + height: 30px; + display: flex; + align-items: center; + padding: 0 8px; + font-size: 10px; + font-weight: 600; + letter-spacing: .04em; + text-transform: uppercase; + color: var(--muted, var(--orca-muted, #6b7280)); + opacity: .85; +} + /* Bottom spacer for the windowed command list: fills the un-rendered tail so the scrollbar reflects the full match count, while only a near-viewport window of rows exists in the DOM. */ .dial-spacer-bottom { diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 4c6eefa470..d1f488e523 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -286,6 +286,8 @@ void AppConfig::set_defaults() // The getter already defaults, parses and clamps; write back what it resolves to. set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(get_plugin_pages_visible_count())); + set(SETTING_SPEED_DIAL_RECENT_COUNT, std::to_string(get_speed_dial_recent_count())); + if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty()) set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false); @@ -1664,6 +1666,22 @@ int AppConfig::get_plugin_pages_visible_count() const return std::clamp(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX); } +int AppConfig::get_speed_dial_recent_count() const +{ + std::string value = get(SETTING_SPEED_DIAL_RECENT_COUNT); + if (value.empty()) + return SPEED_DIAL_RECENT_COUNT_DEFAULT; + + int recent_count = SPEED_DIAL_RECENT_COUNT_DEFAULT; + try { + recent_count = std::stoi(value); + } + catch (...) { + return SPEED_DIAL_RECENT_COUNT_DEFAULT; + } + return std::clamp(recent_count, SPEED_DIAL_RECENT_COUNT_MIN, SPEED_DIAL_RECENT_COUNT_MAX); +} + std::vector<std::string> AppConfig::get_skipped_network_versions() const { std::vector<std::string> result; diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index c799502993..8e3305105a 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -46,6 +46,11 @@ using namespace nlohmann; #define PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT 5 #define PLUGIN_PAGES_VISIBLE_COUNT_MAX 10 +#define SETTING_SPEED_DIAL_RECENT_COUNT "speed_dial_recent_count" +#define SPEED_DIAL_RECENT_COUNT_MIN 0 +#define SPEED_DIAL_RECENT_COUNT_DEFAULT 5 +#define SPEED_DIAL_RECENT_COUNT_MAX 10 + #if defined(_WIN32) || defined(_WIN64) #define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09" #else @@ -394,6 +399,9 @@ public: // dropdown on the last tab. int get_plugin_pages_visible_count() const; + // Number of recently launched actions shown at the top of the Speed Dial; 0 hides them. + int get_speed_dial_recent_count() const; + std::vector<std::string> get_skipped_network_versions() const; void add_skipped_network_version(const std::string& version); bool is_network_version_skipped(const std::string& version) const; diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index a64fe9f705..b44d73ddc1 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -720,6 +720,7 @@ nlohmann::json ActionRegistry::snapshot() {"title", a->title()}, {"source", a->source_name()}, {"group", a->group}, + {"kind", a->kind == AppActionKind::Plugin ? "plugin" : "command"}, {"input", a->input}, {"icon", a->icon}, {"mode", mode_key(a->required_mode)}}); @@ -744,8 +745,9 @@ nlohmann::json ActionRegistry::snapshot() write_section("favourite_actions", nlohmann::json(live_favs)); nlohmann::json favourites(live_favs); - // Recent = the last-N launched actions by recency (only actions with a run history). - constexpr size_t kRecentLimit = 5; + // Recent = the last-N launched actions by recency (only actions with a run history). N is a + // user preference; 0 hides recents without affecting the frecency order below. + const size_t recent_limit = size_t(wxGetApp().app_config->get_speed_dial_recent_count()); std::vector<const AppAction*> recent; for (const auto& entry : m_actions) if (entry.second->last > 0) @@ -755,8 +757,8 @@ nlohmann::json ActionRegistry::snapshot() return a->last > b->last; return a->id() < b->id(); }); - if (recent.size() > kRecentLimit) - recent.resize(kRecentLimit); + if (recent.size() > recent_limit) + recent.resize(recent_limit); nlohmann::json recent_json = nlohmann::json::array(); for (const AppAction* a : recent) recent_json.push_back(action_to_json(a)); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index c5f9bf1f89..54f117f245 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1728,6 +1728,16 @@ void PreferencesDialog::create_items() "enable_speed_dial"); g_sizer->Add(item_speed_dial); + auto item_speed_dial_recents = create_item_spinctrl( + _L("Recent actions"), + "", + _L("actions"), + _L("How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions."), + SETTING_SPEED_DIAL_RECENT_COUNT, + SPEED_DIAL_RECENT_COUNT_MIN, + SPEED_DIAL_RECENT_COUNT_MAX); + g_sizer->Add(item_speed_dial_recents); + #if 0 g_sizer->Add(create_item_title(_L("Filament Grouping")), 1, wxEXPAND); //temporarily disable it diff --git a/tests/libslic3r/test_appconfig.cpp b/tests/libslic3r/test_appconfig.cpp index 59f9d11808..d3811c9827 100644 --- a/tests/libslic3r/test_appconfig.cpp +++ b/tests/libslic3r/test_appconfig.cpp @@ -43,3 +43,41 @@ TEST_CASE("AppConfig network version helpers", "[AppConfig]") { REQUIRE(config.is_network_version_skipped("02.01.01.52")); } } + +TEST_CASE("AppConfig Speed Dial recent count defaults, clamps and parses", "[AppConfig]") { + AppConfig config; + + SECTION("unset falls back to the default") { + REQUIRE(config.get_speed_dial_recent_count() == SPEED_DIAL_RECENT_COUNT_DEFAULT); + } + + SECTION("zero disables recents") { + config.set(SETTING_SPEED_DIAL_RECENT_COUNT, "0"); + REQUIRE(config.get_speed_dial_recent_count() == 0); + } + + SECTION("a value in range is returned as-is") { + config.set(SETTING_SPEED_DIAL_RECENT_COUNT, "7"); + REQUIRE(config.get_speed_dial_recent_count() == 7); + } + + SECTION("the maximum is kept") { + config.set(SETTING_SPEED_DIAL_RECENT_COUNT, "10"); + REQUIRE(config.get_speed_dial_recent_count() == SPEED_DIAL_RECENT_COUNT_MAX); + } + + SECTION("values above the maximum clamp down") { + config.set(SETTING_SPEED_DIAL_RECENT_COUNT, "42"); + REQUIRE(config.get_speed_dial_recent_count() == SPEED_DIAL_RECENT_COUNT_MAX); + } + + SECTION("negative values clamp up to 0") { + config.set(SETTING_SPEED_DIAL_RECENT_COUNT, "-3"); + REQUIRE(config.get_speed_dial_recent_count() == 0); + } + + SECTION("garbage falls back to the default") { + config.set(SETTING_SPEED_DIAL_RECENT_COUNT, "abc"); + REQUIRE(config.get_speed_dial_recent_count() == SPEED_DIAL_RECENT_COUNT_DEFAULT); + } +} From 70a2b3a814d7c6efebee12f15e838ba57e8fbbca Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Fri, 11 Sep 2026 15:43:46 +0800 Subject: [PATCH 18/29] Code cleanup, dedup, update unit tests --- resources/web/dialog/SpeedDial/speeddial.js | 89 +++++- .../web/dialog/SpeedDial/speeddial.test.js | 21 ++ resources/web/dialog/SpeedDial/style.css | 21 -- src/slic3r/GUI/ActionRegistry.cpp | 4 +- src/slic3r/GUI/MainFrame.cpp | 4 +- src/slic3r/GUI/PluginsDialog.cpp | 78 ++--- src/slic3r/GUI/PluginsDialog.hpp | 289 ++++++++++-------- src/slic3r/GUI/Search.cpp | 25 +- tests/slic3rutils/test_action_source.cpp | 10 + 9 files changed, 316 insertions(+), 225 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 2045d42224..3dcbbd4fd1 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -1,6 +1,14 @@ // Speed Dial launcher page. Static-safe module: no DOM access at load time so a // node vm can exercise the pure helpers (searchActions / filterTabs / actionLabel / nextSel / -// commandList / commandSections / actionCategory / groupActions). +// commandList / commandSections / actionCategory / groupActions / favDigitFromEvent / spacerHeight). +// +// Cross-boundary contracts (keep in sync; the C++ side pins its half in tests): +// - favourite cap 10 -> ActionRegistry::kFavLimit (K_FAV_LIMIT here) +// - mode rank simple<advanced<expert<develop -> ConfigOptionMode order (MODE_RANK here) +// - action.mode token -> ActionRegistry::mode_key / SpeedDialDialog::mode_label +// - action.input "percent"/"tab" -> NativeCommands catalog (phases handled in activateEntry) +// - action.icon SVG base name -> AppAction::icon / resources/images/<name>.svg +// - action list is frecency-sorted -> ActionRegistry::snapshot() // ---- state (populated by the C++ bridge via window.HandleStudio) ---- var ACTIONS = []; // [{id,title,source,group,kind,input,icon,mode}], already frecency-sorted by C++ @@ -45,7 +53,7 @@ function T(key, fallback) { var K_ROWS = 50; var ROW_H = 44; var renderEnd = 0; -var builtKey = ""; // phase|query|listLen - when it changes, rows are rebuilt from the first window +var builtKey = ""; // phase|query|total - when it changes, rows are rebuilt from the first window [0, K_ROWS) var spacerEl = null; // the trailing height spacer, always the last child of listEl // Section headers in the empty-query list ("Recent" then one per category). sectionStarts maps a flat @@ -200,6 +208,12 @@ function revealTarget(total, fromIndex, size) { return Math.min(total, Math.max(0, fromIndex) + size); } +// Pure: the bottom spacer's height - the un-rendered row tail plus any not-yet-rendered section +// headers, so the scrollbar reflects the full list and the last rows stay reachable. +function spacerHeight(total, rendered, totalSections, renderedSections) { + return Math.max(0, total - rendered) * ROW_H + Math.max(0, totalSections - renderedSections) * SECTION_H; +} + // buildKey: the command-list signature that decides whether rows must be rebuilt (new search / phase) // or just have their selection refreshed in place (arrow-nav / click). Cheap to compute. function buildKey() { return phase + "|" + (query || "").trim(); } @@ -232,6 +246,13 @@ function favIndexForDigit(d) { return -1; } +// Physical digit for a keydown event. Use e.code so macOS Option+digit (which composes to a symbol +// in e.key, e.g. Alt+1 -> "¡") still maps to the intended slot; fall back to e.key elsewhere. +function favDigitFromEvent(e) { + var m = /^(?:Digit|Numpad)([0-9])$/.exec((e && e.code) || ""); + return m ? m[1] : ((e && e.key) || ""); +} + function resultCountText(total, shown, query) { var n = total + " " + T("sd_actions", "actions"); return (query || "").trim() ? T("sd_showing", "Showing") + " " + shown + " " + T("sd_of", "of") + " " + n : n; @@ -319,10 +340,22 @@ function groupActions(list) { // by relevance; an empty query shows the recents first, then the rest grouped under category headers. // The empty-query result is cached on the action/recents array identities so scrolling doesn't regroup. var commandListCache = null; +// Typed-query result cache, keyed on the pool identity + query. searchActions populates the +// module-level matchIndex/searchNeedle; a hit restores both so a repeated call (keydown + input, +// or a scroll tick) skips the whole scan instead of recomputing it. +var searchCache = null; function commandList(actions, recents, query) { var all = actions || []; - if (shouldRenderActionList(query)) - return searchActions(all, query); + if (shouldRenderActionList(query)) { + if (searchCache && searchCache.actions === all && searchCache.query === query) { + matchIndex = searchCache.matchIndex; + searchNeedle = searchCache.needle; + return searchCache.list; + } + var found = searchActions(all, query); + searchCache = { actions: all, query: query, list: found, matchIndex: matchIndex, needle: searchNeedle }; + return found; + } var rec = recents || []; if (commandListCache && commandListCache.actions === all && commandListCache.recents === rec) return commandListCache.list; @@ -408,15 +441,35 @@ function modeFilterFromQuery(query) { return found; } +// Precomputed label parts for one action pool, keyed on the pool's array identity. The fold pass is +// O(N); doing it here instead of inside every actionLabel() call keeps row rendering O(rows), not O(rows*N). +var labelCache = null; +function labelParts(actions) { + if (labelCache && labelCache.actions === actions) + return labelCache; + var sig = {}, count = {}; + (actions || []).forEach(function (o) { + var s = foldLabel(o.title) + "|" + foldLabel(o.source || o.group || ""); + sig[o.id] = s; + count[s] = (count[s] || 0) + 1; + }); + labelCache = { actions: actions, sig: sig, count: count }; + return labelCache; +} + // 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. function actionLabel(action, actions) { var label = action.title + " from " + prettySource(action.source || action.group || ""); if (actions && actions.length) { - var mine = foldLabel(action.title) + "|" + foldLabel(action.source || action.group || ""); - var clash = actions.some(function (o) { - return o.id !== action.id && foldLabel(o.title) + "|" + foldLabel(o.source || o.group || "") === mine; - }); + var cache = labelParts(actions); + var mine = cache.sig[action.id]; + // mine is undefined only for an action outside the cached pool (e.g. a transient row); scan then. + var clash = mine !== undefined ? cache.count[mine] > 1 : + actions.some(function (o) { + return o.id !== action.id && foldLabel(o.title) + "|" + foldLabel(o.source || o.group || "") === + foldLabel(action.title) + "|" + foldLabel(action.source || action.group || ""); + }); if (clash) label += " (" + action.id + ")"; } @@ -788,13 +841,14 @@ function ensureSpacer() { // section headers reserve their own height too, so the last rows stay reachable. function setBottomSpacer(total) { ensureSpacer(); - var remainingRows = Math.max(0, total - renderEnd); - var remainingHeaders = Math.max(0, sectionTotal - sectionRendered); - spacerEl.style.height = (remainingRows * ROW_H + remainingHeaders * SECTION_H) + "px"; + spacerEl.style.height = spacerHeight(total, renderEnd, sectionTotal, sectionRendered) + "px"; } // Reveal rows up to `upto` (an exclusive index), appending without rebuilding the whole list. Used by // the scroll handler (viewport + overscan) and by arrow-nav that runs off the end of the current window. +// The window is append-only and row indices are absolute, so jumping the selection to the very last row +// (ArrowUp wrap with no fav bar) necessarily materializes the whole list; the label/search caches above +// keep that one-off cost linear rather than quadratic. function revealTo(list, upto) { var need = Math.min(list.length, upto); if (need <= renderEnd) @@ -893,8 +947,9 @@ function renderCommandsList() { updatePins(list); } -// A tab row: no pin/unpin (tabs aren't pinnable), placeholder tile (tabs have no pictogram). Uses -// tabTitle so pages added with an empty text (e.g. Home) still show a label. +// A tab row: no pin/unpin (tabs aren't pinnable), and a tile that shows the page icon when the +// notebook has one (plugin pages often don't). Uses tabTitle so pages added with an empty text +// (e.g. Home) still show a label. function renderTabRow(t, i) { var label = tabTitle(t); var shell = beginRow(t, i, true, label); @@ -911,6 +966,7 @@ function renderTabList() { var q = (query || "").trim(); var list = currentList(); listEl.innerHTML = ""; + spacerEl = null; // the tab list has no windowed spacer; rebuildCommandsList recreates it if (!list.length) { renderEmpty(q ? T("sd_no_tabs_match", "No tabs match") : T("sd_no_tabs", "No tabs")); @@ -990,7 +1046,10 @@ function flashHint(text) { hint.textContent = text; launcher.insertBefore(hint, launcher.firstChild); setTimeout(function () { - if (hint && hint.parentNode) hint.parentNode.removeChild(hint); + if (hint && hint.parentNode) { + hint.parentNode.removeChild(hint); + requestResize(); // reclaim the hint's height so the popup doesn't stay tall + } }, 2500); } @@ -1139,7 +1198,7 @@ function OnInit() { // Quick-launch a numbered favourite: Alt/Option + digit (0 = the 10th). Only in the // commands phase, where the pinned bar is shown. if (phase === "commands" && e.altKey && !e.ctrlKey && !e.metaKey) { - var slotIdx = favIndexForDigit(e.key); + var slotIdx = favIndexForDigit(favDigitFromEvent(e)); var favIds = currentVisibleFavs(); if (slotIdx >= 0 && slotIdx < favIds.length) { e.preventDefault(); diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 521705d68f..85a0bcd064 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -216,6 +216,15 @@ assert.equal(ctx.favSlotForIndex(10), null, "index 10 is beyond the cap"); assert.equal(ctx.favSlotForIndex(-1), null, "negative index is not a slot"); assert.equal(ctx.K_FAV_LIMIT, 10, "the slot count matches the quick-launch cap"); +// favDigitFromEvent: prefer the physical code (so macOS Option+digit still maps even though e.key +// is the composed symbol), and fall back to e.key for keyboards/synthetic events without a code. +assert.equal(ctx.favDigitFromEvent({ code: "Digit1", key: "¡" }), "1", "Digit1 wins over a composed key"); +assert.equal(ctx.favDigitFromEvent({ code: "Digit0", key: "0" }), "0", "Digit0 is a physical digit"); +assert.equal(ctx.favDigitFromEvent({ code: "Numpad7", key: "7" }), "7", "numpad digits count"); +assert.equal(ctx.favDigitFromEvent({ code: "", key: "3" }), "3", "a missing code falls back to key"); +assert.equal(ctx.favDigitFromEvent({ key: "a" }), "a", "non-digit input is passed through (maps to -1)"); +assert.equal(ctx.favDigitFromEvent(null), "", "a null event yields no digit"); + // nextSel: arrow-nav wrapping. Down wraps at the list bottom to the first row; Up wraps at the // list top to the last row ONLY when there's no fav bar above (else it goes to the fav bar). assert.deepEqual(ctx.nextSel({ zone: "list", i: 2 }, "ArrowDown", 3, 0), { zone: "list", i: 0 }, @@ -245,6 +254,14 @@ assert.equal(ctx.revealTarget(100, -5, 50), 50, "negative start is clamped to th assert.equal(ctx.revealTarget(200, 50, 100), 150, "a scroll viewpoint reveals a window past the current rows"); assert.equal(ctx.revealTarget(10, 0, 50), 10, "a list shorter than one window stays fully materialized"); +// spacerHeight: un-rendered rows (44px) plus un-rendered section headers (30px), never negative. +assert.equal(ctx.spacerHeight(100, 50, 0, 0), 50 * 44, "the tail rows reserve their full height"); +assert.equal(ctx.spacerHeight(100, 100, 0, 0), 0, "a fully-rendered list needs no spacer"); +assert.equal(ctx.spacerHeight(100, 50, 3, 1), 50 * 44 + 2 * 30, "pending section headers reserve their height too"); +assert.equal(ctx.spacerHeight(10, 0, 2, 0), 10 * 44 + 2 * 30, "a short list still reserves its headers"); +assert.equal(ctx.spacerHeight(0, 0, 0, 0), 0, "an empty list has no spacer"); +assert.equal(ctx.spacerHeight(10, 20, 0, 5), 0, "over-rendered counters clamp to zero"); + // visibleFavourites: the quick-bar drops pins whose action no longer exists (plugin unloaded, // command removed) and collapses duplicate ids, keeping the persisted pin order. assert.deepEqual(ctx.visibleFavourites(["a", "b", "c"], [{ id: "a" }, { id: "b" }]), @@ -274,6 +291,10 @@ assert.equal(ctx.needsModeSwitch({ mode: "advanced" }, "advanced"), false, "an A assert.equal(ctx.needsModeSwitch({ mode: "develop" }, "develop"), false, "a Developer setting is not gated in Developer mode"); assert.equal(ctx.needsModeSwitch({}, "simple"), false, "a command with no mode is never gated"); +// MODE_RANK must match the C++ ConfigOptionMode order (comSimple < comAdvanced < comExpert < comDevelop). +assert.deepEqual(ctx.MODE_RANK, { simple: 0, advanced: 1, expert: 2, develop: 3 }, + "mode rank matches the C++ ConfigOptionMode order"); + // modeBadge: the tag text for gated settings, empty once the setting is available. assert.equal(ctx.modeBadge({ mode: "advanced" }, "simple"), "Advanced", "Advanced badge text"); assert.equal(ctx.modeBadge({ mode: "expert" }, "simple"), "Expert", "Expert badge text"); diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index 6aaca682d9..c9532d8dab 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -382,12 +382,6 @@ body { border-radius: 2px; } -.row-sc { - flex: 0 0 auto; - display: inline-flex; - gap: 3px; -} - /* Mode tag on settings above the user's current mode (Advanced/Expert/Developer). */ .row-mode { flex: 0 0 auto; @@ -400,21 +394,6 @@ body { white-space: nowrap; } -kbd { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 18px; - height: 18px; - padding: 0 5px; - font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; - font-size: 11px; - color: var(--muted, var(--orca-muted, #6b7280)); - background: rgba(127, 127, 127, .12); - border: 1px solid var(--border, var(--orca-border, #ddd)); - border-radius: 4px; -} - .pin { flex: 0 0 auto; width: 24px; diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index b44d73ddc1..fb98b96a17 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -178,7 +178,7 @@ struct SettingAction : AppAction { std::string opt_key; Preset::Type type; - std::wstring category; // localized category, forwarded to jump_to_option + std::wstring category; // English category, forwarded to jump_to_option (it localizes) static std::string id_for(const std::string& opt_key, Preset::Type type) { return std::string(kSettingPrefix) + ":" + opt_key + ":" + std::to_string(int(type)); } @@ -556,7 +556,7 @@ void ActionRegistry::materialize_setting_actions() // title = the option leaf name (last label segment); group stays empty so the source path // (above) is the single display/search breadcrumb rather than being duplicated. auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, boost::nowide::narrow(label_w), std::string(), - opt.category_local, boost::nowide::narrow(path), opt.mode); + opt.category, boost::nowide::narrow(path), opt.mode); // Tile pictogram = the icon of the setting's own group header (e.g. Advanced -> param_advanced), // the one shown next to it in the page. Fall back to the page/category icon for groups diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 4681a67c29..828c698ce1 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -737,7 +737,9 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ evt.Skip(); // let the focused control keep Space return; } - wxGetApp().open_speed_dial(); + // Defer out of the native key-event stack: open_speed_dial() may create a WebView and + // run script, the same window work the codebase avoids doing on native callbacks. + this->CallAfter([this] { 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; } diff --git a/src/slic3r/GUI/PluginsDialog.cpp b/src/slic3r/GUI/PluginsDialog.cpp index 8bee3919b2..5b1e203c17 100644 --- a/src/slic3r/GUI/PluginsDialog.cpp +++ b/src/slic3r/GUI/PluginsDialog.cpp @@ -528,68 +528,37 @@ bool install_local_plugin_package(const boost::filesystem::path& package_file, w { struct Result { - std::mutex mutex; - bool ok = false; + std::mutex mutex; + bool ok = false; std::string error; }; auto state = std::make_shared<Result>(); - wxProgressDialog* progress = new wxProgressDialog(_L("Installing plugin"), _L("Installing plugin") + ": " + package_name, - 100, parent, wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME); - wxTimer* timer = new wxTimer(); - timer->Bind(wxEVT_TIMER, [progress](wxTimerEvent&) { - if (progress) - progress->Pulse(); - }); - timer->Start(100); - - // finished/loop live on the heap: the worker's completion callback is posted to the UI loop - // and can still fire after this stack frame is gone, so it must not reference locals. - struct WaitState - { - bool finished = false; - wxEventLoop loop; - }; - auto wait = std::make_shared<WaitState>(); - - std::thread([state, package_file, wait]() mutable { - std::string error; - bool ok = false; - try { - ok = PluginManager::instance().install_plugin(package_file, error); - } catch (const std::exception& ex) { - error = ex.what(); - } catch (...) { - error = "Unknown error"; - } - if (ok) { - // Reflect the new package in discovery/cloud metadata without blocking the caller. - try { refresh_plugin_metadata_blocking(kUseCurrentCloudMeta); } catch (...) {} - } - { + detail::run_wait_with_progress( + [state, package_file]() { + std::string error; + bool ok = false; + try { + ok = PluginManager::instance().install_plugin(package_file, error); + } catch (const std::exception& ex) { + error = ex.what(); + } catch (...) { + error = "Unknown error"; + } + if (ok) { + // Reflect the new package in discovery/cloud metadata without blocking the caller. + try { refresh_plugin_metadata_blocking(kUseCurrentCloudMeta); } catch (...) {} + } std::lock_guard<std::mutex> lock(state->mutex); state->ok = ok; state->error = std::move(error); - } - if (!wxTheApp) - return; - wxTheApp->CallAfter([wait]() { - wait->finished = true; - if (wait->loop.IsRunning()) - wait->loop.Exit(); - }); - }).detach(); - - if (!wait->finished) - wait->loop.Run(); - - timer->Stop(); - delete timer; - progress->Destroy(); + }, + parent, _L("Installing plugin"), _L("Installing plugin") + ": " + package_name, 100, + wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME, /*alive=*/nullptr, /*restore=*/{}); std::lock_guard<std::mutex> lock(state->mutex); - installed = state->ok; - error = std::move(state->error); + installed = state->ok; + error = std::move(state->error); } if (!installed) { @@ -980,6 +949,9 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path) const boost::filesystem::path package_file(package_path); wxString message; const bool installed = install_local_plugin_package(package_file, this, message); + // The helper's overwrite prompt and progress dialog can push this webview behind; re-raise it + // once, after both have closed (the speed-dial path parents to the mainframe instead). + restore_z_order(); // The shared helper reports a user-cancelled overwrite with an empty message: stay silent. if (message.IsEmpty()) { diff --git a/src/slic3r/GUI/PluginsDialog.hpp b/src/slic3r/GUI/PluginsDialog.hpp index 2c4a5925d9..0aee7b2734 100644 --- a/src/slic3r/GUI/PluginsDialog.hpp +++ b/src/slic3r/GUI/PluginsDialog.hpp @@ -52,6 +52,168 @@ void open_plugin_hub(); // confirmation; on a user-cancelled overwrite it is empty; on failure it carries the reason. bool install_local_plugin_package(const boost::filesystem::path& package_file, wxWindow* parent, wxString& message); +namespace detail { + +// Shared worker + modal-progress machinery: pulse a progress dialog while `run` executes on a +// detached worker, then run `on_finish` back on the UI thread. `alive`, when non-null, gates both +// the pulse and `on_finish` so a worker outliving its dialog can't touch freed windows; pass null +// for a dialog-independent caller. `restore` runs after the progress dialog is destroyed and before +// `on_finish`, so a webview host can re-raise itself. `finish_after_dialog_destroyed` still calls +// `on_finish` (without touching the dialog) when the host died, so a waiting loop can exit. +template<typename Run, typename OnFinish> +void run_off_thread_with_progress(Run&& run, + OnFinish&& on_finish, + wxWindow* parent, + const wxString& title, + const wxString& message, + int maximum, + int style, + std::shared_ptr<std::atomic<bool>> alive, + bool finish_after_dialog_destroyed, + std::function<void()> restore) +{ + wxProgressDialog* progress = new wxProgressDialog(title, message, maximum, parent, style); + wxTimer* timer = new wxTimer(); + + timer->Bind(wxEVT_TIMER, [alive, progress, message](wxTimerEvent&) { + if ((!alive || alive->load(std::memory_order_acquire)) && progress) + progress->Pulse(message); + }); + + timer->Start(100); + + std::thread([alive, + progress, + timer, + run = std::forward<Run>(run), + on_finish = std::forward<OnFinish>(on_finish), + finish_after_dialog_destroyed, + restore = std::move(restore)]() mutable { + try { + run(); + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed: " << ex.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed with an unknown exception"; + } + + if (wxTheApp == nullptr) + return; + + wxTheApp->CallAfter([alive, + progress, + timer, + on_finish = std::move(on_finish), + finish_after_dialog_destroyed, + restore = std::move(restore)]() mutable { + timer->Stop(); + delete timer; + + if (!alive || alive->load(std::memory_order_acquire)) { + progress->Destroy(); + if (restore) + restore(); + on_finish(); + } else if (finish_after_dialog_destroyed) { + on_finish(); + } + }); + }).detach(); +} + +// Wait for a worker behind a progress dialog, returning its result (or rethrowing). The waiting +// loop stays responsive because it pumps the event loop the worker posts its completion into. +template<typename Run> +std::invoke_result_t<std::decay_t<Run>&> run_wait_with_progress(Run&& run, + wxWindow* parent, + const wxString& title, + const wxString& message, + int maximum, + int style, + std::shared_ptr<std::atomic<bool>> alive, + std::function<void()> restore) +{ + using Result = std::invoke_result_t<std::decay_t<Run>&>; + + bool finished = false; + wxEventLoop loop; + auto on_finish = [&finished, &loop]() { + finished = true; + if (loop.IsRunning()) + loop.Exit(); + }; + + if constexpr (std::is_void_v<Result>) { + struct WaitState + { + std::mutex mutex; + std::exception_ptr exception; + }; + + auto state = std::make_shared<WaitState>(); + run_off_thread_with_progress( + [run = std::forward<Run>(run), state]() mutable { + try { + run(); + } catch (...) { + std::lock_guard<std::mutex> lock(state->mutex); + state->exception = std::current_exception(); + } + }, + on_finish, parent, title, message, maximum, style, std::move(alive), /*finish_after_dialog_destroyed=*/true, std::move(restore)); + + if (!finished) + loop.Run(); + + std::exception_ptr exception; + { + std::lock_guard<std::mutex> lock(state->mutex); + exception = state->exception; + } + if (exception) + std::rethrow_exception(exception); + } else { + using StoredResult = std::decay_t<Result>; + struct WaitState + { + std::mutex mutex; + std::optional<StoredResult> result; + std::exception_ptr exception; + }; + + auto state = std::make_shared<WaitState>(); + run_off_thread_with_progress( + [run = std::forward<Run>(run), state]() mutable { + try { + StoredResult result = run(); + std::lock_guard<std::mutex> lock(state->mutex); + state->result.emplace(std::move(result)); + } catch (...) { + std::lock_guard<std::mutex> lock(state->mutex); + state->exception = std::current_exception(); + } + }, + on_finish, parent, title, message, maximum, style, std::move(alive), /*finish_after_dialog_destroyed=*/true, std::move(restore)); + + if (!finished) + loop.Run(); + + std::optional<StoredResult> result; + std::exception_ptr exception; + { + std::lock_guard<std::mutex> lock(state->mutex); + if (state->result) + result.emplace(std::move(*state->result)); + exception = state->exception; + } + if (exception) + std::rethrow_exception(exception); + return std::move(*result); + } +} + +} // namespace detail + class PluginsDialog : public Slic3r::GUI::WebViewHostDialog { public: @@ -128,53 +290,8 @@ private: int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE, bool finish_after_dialog_destroyed = false) { - const auto alive = m_alive; - wxProgressDialog* progress = new wxProgressDialog(title, message, maximum, this, style); - wxTimer* timer = new wxTimer(); - - timer->Bind(wxEVT_TIMER, [alive, progress, message](wxTimerEvent&) { - if (alive->load(std::memory_order_acquire) && progress) - progress->Pulse(message); - }); - - timer->Start(100); - - std::thread([this, - alive, - progress, - timer, - run = std::forward<Run>(run), - on_finish = std::forward<OnFinish>(on_finish), - finish_after_dialog_destroyed]() mutable { - try { - run(); - } catch (const std::exception& ex) { - BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed: " << ex.what(); - } catch (...) { - BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed with an unknown exception"; - } - - if (wxTheApp == nullptr) - return; - - wxTheApp->CallAfter([this, - alive, - progress, - timer, - on_finish = std::move(on_finish), - finish_after_dialog_destroyed]() mutable { - timer->Stop(); - delete timer; - - if (alive->load(std::memory_order_acquire)) { - progress->Destroy(); - restore_z_order(); - on_finish(); - } else if (finish_after_dialog_destroyed) { - on_finish(); - } - }); - }).detach(); + detail::run_off_thread_with_progress(std::forward<Run>(run), std::forward<OnFinish>(on_finish), this, title, message, maximum, style, + m_alive, finish_after_dialog_destroyed, [this] { restore_z_order(); }); } template<typename Run> @@ -184,83 +301,7 @@ private: int maximum = 100, int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE) { - using Result = std::invoke_result_t<std::decay_t<Run>&>; - - bool finished = false; - wxEventLoop loop; - auto on_finish = [&finished, &loop]() { - finished = true; - if (loop.IsRunning()) - loop.Exit(); - }; - - if constexpr (std::is_void_v<Result>) { - struct WaitState - { - std::mutex mutex; - std::exception_ptr exception; - }; - - auto state = std::make_shared<WaitState>(); - run_with_dialog( - [run = std::forward<Run>(run), state]() mutable { - try { - run(); - } catch (...) { - std::lock_guard<std::mutex> lock(state->mutex); - state->exception = std::current_exception(); - } - }, - on_finish, title, message, maximum, style, true); - - if (!finished) - loop.Run(); - - std::exception_ptr exception; - { - std::lock_guard<std::mutex> lock(state->mutex); - exception = state->exception; - } - if (exception) - std::rethrow_exception(exception); - } else { - using StoredResult = std::decay_t<Result>; - struct WaitState - { - std::mutex mutex; - std::optional<StoredResult> result; - std::exception_ptr exception; - }; - - auto state = std::make_shared<WaitState>(); - run_with_dialog( - [run = std::forward<Run>(run), state]() mutable { - try { - StoredResult result = run(); - std::lock_guard<std::mutex> lock(state->mutex); - state->result.emplace(std::move(result)); - } catch (...) { - std::lock_guard<std::mutex> lock(state->mutex); - state->exception = std::current_exception(); - } - }, - on_finish, title, message, maximum, style, true); - - if (!finished) - loop.Run(); - - std::optional<StoredResult> result; - std::exception_ptr exception; - { - std::lock_guard<std::mutex> lock(state->mutex); - if (state->result) - result.emplace(std::move(*state->result)); - exception = state->exception; - } - if (exception) - std::rethrow_exception(exception); - return std::move(*result); - } + return detail::run_wait_with_progress(std::forward<Run>(run), this, title, message, maximum, style, m_alive, [this] { restore_z_order(); }); } std::function<void()> m_open_terminal_dlg_fn; diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index 21aa8b7350..16ec2051a9 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -143,17 +143,24 @@ void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type ty } } -inline void OptionsSearcher::sort_options() +void OptionsSearcher::sort_options() { - std::sort(options.begin(), options.end(), [](const Option &o1, const Option &o2) { return o1.label < o2.label; }); - Option * last = nullptr; - for (auto& opt : options) { - if (last && last->label == opt.label && last->group == opt.group && last->type == opt.type && last->category != opt.category) { - last->multi_category = true; - opt.multi_category = true; + // Both views are label-sorted and multi_category-marked. They are separate consumers (the sidebar + // search and the Speed Dial); keeping them in sync here prevents the all-modes view from silently + // diverging in order or flags. + auto sort_and_mark = [](std::vector<Option> &v) { + std::sort(v.begin(), v.end(), [](const Option &o1, const Option &o2) { return o1.label < o2.label; }); + Option *last = nullptr; + for (auto &opt : v) { + if (last && last->label == opt.label && last->group == opt.group && last->type == opt.type && last->category != opt.category) { + last->multi_category = true; + opt.multi_category = true; + } + last = &opt; } - last = &opt; - } + }; + sort_and_mark(options); + sort_and_mark(options_all_modes); } // Mark a string using ColorMarkerStart and ColorMarkerEnd symbols diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index 59a81d21fa..02e5205fe6 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -118,6 +118,16 @@ TEST_CASE("Two-phase commands declare their input phase", "[ActionSource][SpeedD CHECK(input_of("go_to_tab") == "tab"); } +// The input token vocabulary is a JS<->C++ contract (speeddial.js dispatches "percent"/"tab"). +// A typo here would leave a command that never enters its second phase, so pin the allowed set. +TEST_CASE("Command input tokens stay in the known vocabulary", "[ActionSource][SpeedDial]") +{ + for (const auto& c : Slic3r::GUI::NativeCommands::catalog()) { + INFO(c.key << " input=" << c.input); + CHECK((c.input.empty() || c.input == "percent" || c.input == "tab")); + } +} + // The quick-launch cap must stay 10 to match the numbered Alt/Option+1..9,0 keys. The web palette // mirrors it as K_FAV_LIMIT (asserted in speeddial.test.js); the C++ side pins it here. static_assert(Slic3r::GUI::ActionRegistry::kFavLimit == 10, "kFavLimit must stay 10"); From c57b74731e37550444f0218457396be512150201 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Fri, 11 Sep 2026 16:38:14 +0800 Subject: [PATCH 19/29] Tooltip bottom bar for process/filament/printer settings --- .github/workflows/unit_tests.yml | 6 --- resources/web/data/text.js | 2 + resources/web/dialog/SpeedDial/index.html | 1 + resources/web/dialog/SpeedDial/speeddial.js | 53 ++++++++++++++++++- .../web/dialog/SpeedDial/speeddial.test.js | 16 ++++++ resources/web/dialog/SpeedDial/style.css | 45 ++++++++++++++++ src/slic3r/GUI/ActionRegistry.cpp | 10 +++- src/slic3r/GUI/ActionRegistry.hpp | 4 ++ src/slic3r/GUI/OptionsGroup.cpp | 6 +++ src/slic3r/GUI/OptionsGroup.hpp | 6 ++- src/slic3r/GUI/Search.cpp | 22 ++++++-- src/slic3r/GUI/Search.hpp | 7 +++ src/slic3r/GUI/SpeedDialDialog.cpp | 12 +++++ src/slic3r/GUI/SpeedDialDialog.hpp | 1 + tests/slic3rutils/test_action_source.cpp | 15 ++++++ 15 files changed, 191 insertions(+), 15 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index e05a927a03..5f54f6581d 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -49,12 +49,6 @@ jobs: cmakeVersion: "~4.3.0" # use most recent 4.3.x version useLocalCache: true useCloudCache: true - - name: Run web dialog JS tests - timeout-minutes: 5 - shell: bash - run: | - node resources/web/js/fuzzy-search.test.js - node resources/web/dialog/SpeedDial/speeddial.test.js - name: Unpackage and Run Unit Tests timeout-minutes: 20 shell: bash diff --git a/resources/web/data/text.js b/resources/web/data/text.js index 2f6682c051..0c803c0ada 100644 --- a/resources/web/data/text.js +++ b/resources/web/data/text.js @@ -150,6 +150,8 @@ var LangText = { sd_mode_advanced: "Advanced", sd_mode_expert: "Expert", sd_mode_develop: "Developer", + sd_wiki: "Wiki", + sd_no_wiki: "No wiki page for this action", }, ca_ES: { t1: "Benvingut a Orca Slicer", diff --git a/resources/web/dialog/SpeedDial/index.html b/resources/web/dialog/SpeedDial/index.html index a6f9e753a6..0137a2fcf2 100644 --- a/resources/web/dialog/SpeedDial/index.html +++ b/resources/web/dialog/SpeedDial/index.html @@ -28,6 +28,7 @@ <div class="dial-count" id="count" hidden></div> </div> <div class="dial-list" id="list"></div> + <div class="dial-detail" id="detail" hidden></div> </div> </body> </html> diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 3dcbbd4fd1..d5615bd3f7 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -8,6 +8,7 @@ // - action.mode token -> ActionRegistry::mode_key / SpeedDialDialog::mode_label // - action.input "percent"/"tab" -> NativeCommands catalog (phases handled in activateEntry) // - action.icon SVG base name -> AppAction::icon / resources/images/<name>.svg +// - action.desc/wiki -> AppAction::tooltip / help_url (footer detail strip) // - action list is frecency-sorted -> ActionRegistry::snapshot() // ---- state (populated by the C++ bridge via window.HandleStudio) ---- @@ -77,7 +78,7 @@ var tabOptions = []; // [{id,title}] - notebook pages, fetched on entering // ../../js/fuzzy-search.js, loaded before this script. Search is always case-insensitive. // element handles, assigned in OnInit (kept null so load-time touches no DOM) -var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, countEl = null; +var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, countEl = null, detailEl = null; // ---- pure helpers (no DOM; unit-tested) ------------------------------------- // Pre-normalized haystacks, cached on the action object. The fold is length-preserving (1:1 per @@ -400,6 +401,11 @@ function selectedActionId(sel, actions, favIds) { return a && a.id; } +function actionHasWiki(a) { return !!(a && a.wiki); } + +// Whether the action has anything for the footer strip to show (a description or a wiki link). +function actionHasDetail(a) { return !!(a && ((a.desc && a.desc.length) || a.wiki)); } + function foldLabel(s) { return String(s || "").toLowerCase().replace(/[^a-z0-9]+/g, ""); } // Title-case a source for display: "GCODE OPTIMIZER"/"iRoNiNg pRo" -> "Gcode Optimizer"/"Ironing Pro". @@ -996,9 +1002,43 @@ function renderList() { renderCommandsList(); } +// The action the footer describes: the current selection resolved through the active list/fav bar. +function currentDetailAction() { + if (phase !== "commands") return null; + var id = selectedActionId(sel, currentList(), currentVisibleFavs()); + return id ? byId(id) : null; +} + +// Footer detail strip: the selected action's description plus, when it has a wiki page, a link that +// opens it (same path as F1). Shown only when the highlighted action has something to say, so +// selecting a command with no description hides the strip. +function renderDetail() { + if (!detailEl) return; + var a = currentDetailAction(); + var show = phase === "commands" && actionHasDetail(a); + detailEl.hidden = !show; + detailEl.innerHTML = ""; + if (!show) return; + if (a && a.desc) { + var desc = document.createElement("div"); + desc.className = "detail-desc"; + desc.textContent = a.desc; + detailEl.appendChild(desc); + } + if (a && a.wiki) { + var link = document.createElement("button"); + link.type = "button"; + link.className = "detail-wiki"; + link.textContent = T("sd_wiki", "Wiki") + " (F1)"; + link.onclick = function (ev) { ev.stopPropagation(); SendMessage({ command: "open_wiki", id: a.id }); }; + detailEl.appendChild(link); + } +} + function render(opts) { renderFav(); renderList(); + renderDetail(); // Pin toggles don't move the selection, so they pass keepScroll to avoid snapping the list // back to a row that is currently off-screen. if (!(opts && opts.keepScroll)) @@ -1148,7 +1188,7 @@ function focusInput() { setTimeout(function () { if (qEl) qEl.focus(); }, 0); } // ---- init -------------------------------------------------------------------- function OnInit() { - qEl = $("q"); listEl = $("list"); favEl = $("favBar"); clearEl = $("clear"); eyeEl = $("favEyebrow"); countEl = $("count"); + qEl = $("q"); listEl = $("list"); favEl = $("favBar"); clearEl = $("clear"); eyeEl = $("favEyebrow"); countEl = $("count"); detailEl = $("detail"); // text.js's TranslatePage() targets jQuery `.trans` nodes; this page has none and defines its own // `$`, so don't call it. Runtime strings go through T() instead. qEl.placeholder = T("sd_search", "Search actions"); @@ -1187,6 +1227,15 @@ function OnInit() { document.addEventListener("keydown", function (e) { if (favMenuEl && !favMenuEl.hidden && e.key === "Escape") { e.preventDefault(); hideFavMenu(); return; } + // F1 opens the selected setting's wiki page. Settings without one flash a hint instead. + if (e.key === "F1") { + e.preventDefault(); + if (phase !== "commands") return; + var help = currentDetailAction(); + if (actionHasWiki(help)) SendMessage({ command: "open_wiki", id: help.id }); + else flashHint(T("sd_no_wiki", "No wiki page for this action")); + return; + } // Pin/unpin the highlighted action: Ctrl/Cmd+B. Commands phase only (tabs/percent aren't pinnable). if (phase === "commands" && (e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === "b") { diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 85a0bcd064..38f7c343ae 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -334,4 +334,20 @@ assert.equal(ctx.searchActions(modePool, "retraction")[0].id, "a3", assert.equal(ctx.searchActions([{ id: "both", title: "Advanced", source: "Quality", group: "", mode: "advanced" }], "advanced").length, 1, "a setting that both matches text and requires the mode appears exactly once"); +// actionHasWiki: the footer's wiki link/F1 path is offered only when the action carries a wiki flag. +assert.equal(ctx.actionHasWiki({ id: "x", wiki: true }), true, "a wiki-flagged setting offers the wiki action"); +assert.equal(ctx.actionHasWiki({ id: "x", wiki: false }), false, "a setting without a wiki path offers nothing"); +assert.equal(ctx.actionHasWiki({ id: "x" }), false, "a missing wiki field offers nothing"); +assert.equal(ctx.actionHasWiki(null), false, "no action selected offers nothing"); + +// actionHasDetail: the footer strip appears only when the highlighted action has a description or +// wiki link; selecting a plain command hides it. +assert.equal(ctx.actionHasDetail({ id: "a", desc: "Layer height" }), true, "a description shows the footer"); +assert.equal(ctx.actionHasDetail({ id: "a", wiki: true }), true, "a wiki link shows the footer"); +assert.equal(ctx.actionHasDetail({ id: "a", desc: "Layer height", wiki: true }), true, "both show the footer"); +assert.equal(ctx.actionHasDetail({ id: "a", desc: "" }), false, "an empty description hides the footer"); +assert.equal(ctx.actionHasDetail({ id: "a", desc: "", wiki: false }), false, "empty description and false wiki hide the footer"); +assert.equal(ctx.actionHasDetail({ id: "a" }), false, "an action with neither hides the footer"); +assert.equal(ctx.actionHasDetail(null), false, "no selected action hides the footer"); + console.log("ok"); diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index c9532d8dab..8afc33d98c 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -436,3 +436,48 @@ body { color: var(--muted, var(--orca-muted, #6b7280)); text-align: center; } + +/* Footer detail strip: the selected action's description plus its wiki link. Auto-sizes to the + content; the description is clamped below. */ +.dial-detail { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-top: 1px solid var(--border, var(--orca-border, #ddd)); + overflow: hidden; +} + +.dial-detail[hidden] { + display: none; +} + +.detail-desc { + flex: 1 1 auto; + min-width: 0; + font-size: 11px; + line-height: 1.4; + color: var(--muted, var(--orca-muted, #6b7280)); + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + overflow-wrap: anywhere; +} + +.detail-wiki { + flex: 0 0 auto; + border: 0; + padding: 0; + background: transparent; + font: inherit; + font-size: 11px; + color: var(--main-color, var(--orca-accent, #009688)); + cursor: pointer; + white-space: nowrap; +} + +.detail-wiki:hover { + text-decoration: underline; +} diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index fb98b96a17..3770d6fb68 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -6,6 +6,7 @@ #include "MainFrame.hpp" #include "NativeCommands.hpp" #include "Notebook.hpp" +#include "OptionsGroup.hpp" #include "Plater.hpp" #include "Search.hpp" #include "Tab.hpp" @@ -571,6 +572,11 @@ void ActionRegistry::materialize_setting_actions() } } + // Footer description + wiki affordance; only settings whose row declared a wiki path have one. + action->tooltip = opt.tooltip; + if (!opt.wiki_path.empty()) + action->help_url = into_u8(OptionsGroup::get_url(opt.wiki_path)); + seed_from(stats, favs, id, *action); auto const action_id = action->id(); auto const app_action = std::shared_ptr<AppAction>(std::move(action)); @@ -723,7 +729,9 @@ nlohmann::json ActionRegistry::snapshot() {"kind", a->kind == AppActionKind::Plugin ? "plugin" : "command"}, {"input", a->input}, {"icon", a->icon}, - {"mode", mode_key(a->required_mode)}}); + {"mode", mode_key(a->required_mode)}, + {"desc", a->tooltip}, + {"wiki", !a->help_url.empty()}}); }; nlohmann::json actions = nlohmann::json::array(); diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index 3c71317eed..026e1bcee5 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -83,6 +83,10 @@ struct AppAction // Settings mode required to edit this action (SettingActions only). The palette prompts before // running an action whose mode is above the user's current mode. comSimple for everything else. ConfigOptionMode required_mode = comSimple; + // Description shown in the Speed Dial's footer strip (SettingActions: the localized tooltip). + std::string tooltip; + // Full wiki URL, when the action has one (SettingActions whose row declared a label_path). + std::string help_url; virtual ~AppAction() = default; // Re-resolves + runs (UI thread). `param` carries an optional per-run argument for diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 9930e6d872..2fc41ba90b 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -244,6 +244,12 @@ void OptionsGroup::append_line(const Line& line) { m_lines.emplace_back(line); + // Record each option's wiki path (Line::label_path) so the Speed Dial can offer an "open wiki" + // affordance for it. Settings tabs only; the searcher already exists by the time tabs are built. + if (m_use_custom_ctrl && !line.label_path.empty()) + for (const auto& opt : line.get_options()) + wxGetApp().sidebar().get_searcher().set_path(opt.opt_id, static_cast<Preset::Type>(config_type()), line.label_path); + if (line.full_width && (line.widget != nullptr || !line.get_extra_widgets().empty())) return; diff --git a/src/slic3r/GUI/OptionsGroup.hpp b/src/slic3r/GUI/OptionsGroup.hpp index b20d16aca9..921ac62f15 100644 --- a/src/slic3r/GUI/OptionsGroup.hpp +++ b/src/slic3r/GUI/OptionsGroup.hpp @@ -250,6 +250,10 @@ protected: virtual void back_to_initial_value(const std::string& opt_key) {} virtual void back_to_sys_value(const std::string& opt_key) {} + // Preset::Type of a settings group; -1 for groups not tied to a preset. Used by append_line to + // register each option's wiki path with the searcher. Overridden by ConfigOptionsGroup. + virtual int config_type() const { return -1; } + public: static wxString get_url(const std::string& path_end); static bool launch_browser(const std::string& path_end); @@ -273,7 +277,7 @@ public: OptionsGroup(parent, wxEmptyString, wxEmptyString, true, nullptr) {} const wxString& config_category() const throw() { return m_config_category; } - int config_type() const throw() { return m_config_type; } + int config_type() const throw() override { return m_config_type; } const t_opt_map& opt_map() const throw() { return m_opt_map; } void set_config_category_and_type(const wxString &category, int type) { m_config_category = category; m_config_type = type; } diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index 16ec2051a9..916d93a40c 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -85,7 +85,7 @@ static std::string get_key(const std::string &opt_key, Preset::Type type) { retu void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode) { - auto emplace = [this, type](std::vector<Option> &dst, const std::string &key, const wxString &label, ConfigOptionMode opt_mode) { + auto emplace = [this, type](std::vector<Option> &dst, const std::string &key, const wxString &label, ConfigOptionMode opt_mode, std::string tooltip) { const GroupAndCategory &gc = groups_and_categories[key]; if (gc.group.IsEmpty() || gc.category.IsEmpty()) return; @@ -101,7 +101,7 @@ void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type ty if (!label.IsEmpty()) dst.emplace_back(Option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(), gc.group.ToStdWstring(), _(gc.group).ToStdWstring(), into_u8(gc.icon), gc.category.ToStdWstring(), GUI::Tab::translate_category(gc.category, type).ToStdWstring(), - false, opt_mode}); + false, opt_mode, std::move(tooltip), gc.path}); }; for (std::string opt_key : config->keys()) { @@ -131,8 +131,8 @@ void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type ty std::string key = get_key(opt_key, type); auto add = [&](const std::string &k) { if (in_filtered) - emplace(options, k, label, opt.mode); - emplace(options_all_modes, k, label, opt.mode); + emplace(options, k, label, opt.mode, into_u8(_(opt.tooltip))); + emplace(options_all_modes, k, label, opt.mode, into_u8(_(opt.tooltip))); }; if (cnt == 0) add(key); @@ -459,7 +459,19 @@ void OptionsSearcher::dlg_msw_rescale() void OptionsSearcher::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, const wxString &icon) { - groups_and_categories[get_key(opt_key, type)] = GroupAndCategory{group, category, icon}; + // Update fields in place so a page rebuild (get_option after set_path) doesn't drop the + // previously recorded wiki path. + GroupAndCategory &gc = groups_and_categories[get_key(opt_key, type)]; + gc.group = group; + gc.category = category; + gc.icon = icon; +} + +void OptionsSearcher::set_path(const std::string &opt_key, Preset::Type type, const std::string &path) +{ + if (path.empty()) + return; + groups_and_categories[get_key(opt_key, type)].path = path; } //------------------------------------------ // SearchItem diff --git a/src/slic3r/GUI/Search.hpp b/src/slic3r/GUI/Search.hpp index bf2441e15c..3cec0ba8c2 100644 --- a/src/slic3r/GUI/Search.hpp +++ b/src/slic3r/GUI/Search.hpp @@ -46,6 +46,7 @@ struct GroupAndCategory wxString group; wxString category; wxString icon; // icon of the group's own header, or empty + std::string path; // wiki path (Line::label_path) of the option's line, or empty }; struct Option @@ -66,6 +67,8 @@ struct Option std::wstring category_local; bool multi_category { false }; ConfigOptionMode mode{comSimple}; // option's visibility threshold; drives the Speed Dial's mode prompt + std::string tooltip; // localized ConfigOptionDef::tooltip, or empty + std::string wiki_path; // Line::label_path for the option's row, or empty std::string opt_key() const; }; @@ -133,6 +136,10 @@ public: void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, const wxString &icon = wxEmptyString); + // Records the wiki path of an option's row (Line::label_path) so the Speed Dial can offer a + // "open wiki" affordance. Empty paths are ignored. + void set_path(const std::string &opt_key, Preset::Type type, const std::string &path); + size_t size() const { return found_size(); } const FoundOption &operator[](const size_t pos) const noexcept { return found[pos]; } diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index af079d48e4..e8dc321555 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -14,6 +14,7 @@ #include <wx/display.h> #include <wx/sizer.h> #include <wx/stattext.h> +#include <wx/utils.h> #ifdef __linux__ #include <gtk/gtk.h> @@ -152,6 +153,8 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload) 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 == "open_wiki") + open_wiki(payload.value("id", "")); else if (command == "search_tabs") search_tabs(); else if (command == "resize") @@ -253,6 +256,15 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti }); } +void SpeedDialWebDialog::open_wiki(const std::string& id) +{ + const AppAction* a = wxGetApp().action_registry().by_id(id); + if (!a || a->help_url.empty()) + return; + Hide(); + wxLaunchDefaultBrowser(from_u8(a->help_url)); +} + void SpeedDialWebDialog::send_actions() { nlohmann::json snap = wxGetApp().action_registry().snapshot(); diff --git a/src/slic3r/GUI/SpeedDialDialog.hpp b/src/slic3r/GUI/SpeedDialDialog.hpp index 26734dd841..98a32674a5 100644 --- a/src/slic3r/GUI/SpeedDialDialog.hpp +++ b/src/slic3r/GUI/SpeedDialDialog.hpp @@ -21,6 +21,7 @@ private: void handle_web_command(const nlohmann::json& payload); void resize_to_content(int height); void run_action(const std::string& id, const std::string& title, const std::string& param = ""); + void open_wiki(const std::string& id); void send_actions(); void search_tabs(); diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index 02e5205fe6..1743f09ab3 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -105,6 +105,21 @@ TEST_CASE("Command action construction keys by catalog key", "[ActionSource][Spe CHECK(action->icon == c.icon); } +// The footer description/wiki link is settings-only: built-in commands leave both fields empty, so +// the palette's detail strip depends on list-level visibility for them. +TEST_CASE("Actions default to no description or wiki link", "[ActionSource][SpeedDial]") +{ + const TestAppAction action; + CHECK(action.tooltip.empty()); + CHECK(action.help_url.empty()); + + REQUIRE_FALSE(Slic3r::GUI::NativeCommands::catalog().empty()); + std::unique_ptr<AppAction> command = Slic3r::GUI::NativeCommands::make_action(Slic3r::GUI::NativeCommands::catalog().front()); + REQUIRE(command != nullptr); + CHECK(command->tooltip.empty()); + CHECK(command->help_url.empty()); +} + // Two-phase commands declare the input the palette must collect before they can run. TEST_CASE("Two-phase commands declare their input phase", "[ActionSource][SpeedDial]") { From 966e029b958deb61d07c16f3ff81f3156781248c Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Fri, 11 Sep 2026 17:15:57 +0800 Subject: [PATCH 20/29] Code refactoring for better maintainability --- localization/i18n/OrcaSlicer.pot | 24 +- localization/i18n/ca/OrcaSlicer_ca.po | 33 ++- localization/i18n/cs/OrcaSlicer_cs.po | 33 ++- localization/i18n/de/OrcaSlicer_de.po | 33 ++- localization/i18n/en/OrcaSlicer_en.po | 24 +- localization/i18n/es/OrcaSlicer_es.po | 29 ++- localization/i18n/eu/OrcaSlicer_eu.po | 29 ++- localization/i18n/fr/OrcaSlicer_fr.po | 33 ++- localization/i18n/hu/OrcaSlicer_hu.po | 33 ++- localization/i18n/it/OrcaSlicer_it.po | 33 ++- localization/i18n/ja/OrcaSlicer_ja.po | 33 ++- localization/i18n/ko/OrcaSlicer_ko.po | 33 ++- localization/i18n/list.txt | 1 + localization/i18n/lt/OrcaSlicer_lt.po | 33 ++- localization/i18n/nl/OrcaSlicer_nl.po | 33 ++- localization/i18n/pl/OrcaSlicer_pl.po | 33 ++- localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 29 ++- localization/i18n/ru/OrcaSlicer_ru.po | 29 ++- localization/i18n/sv/OrcaSlicer_sv.po | 33 ++- localization/i18n/th/OrcaSlicer_th.po | 33 ++- localization/i18n/tr/OrcaSlicer_tr.po | 33 ++- localization/i18n/uk/OrcaSlicer_uk.po | 33 ++- localization/i18n/vi/OrcaSlicer_vi.po | 33 ++- localization/i18n/zh_CN/OrcaSlicer_zh_CN.po | 33 ++- localization/i18n/zh_TW/OrcaSlicer_zh_TW.po | 33 ++- src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/ActionRegistry.cpp | 8 +- src/slic3r/GUI/OptionsGroup.cpp | 4 +- src/slic3r/GUI/Plater.cpp | 5 + src/slic3r/GUI/Plater.hpp | 1 + src/slic3r/GUI/Search.cpp | 219 +------------------ src/slic3r/GUI/Search.hpp | 82 +------ src/slic3r/GUI/SettingsIndex.cpp | 229 ++++++++++++++++++++ src/slic3r/GUI/SettingsIndex.hpp | 98 +++++++++ src/slic3r/GUI/Tab.cpp | 16 +- src/slic3r/GUI/UnsavedChangesDialog.cpp | 32 +-- 36 files changed, 938 insertions(+), 517 deletions(-) create mode 100644 src/slic3r/GUI/SettingsIndex.cpp create mode 100644 src/slic3r/GUI/SettingsIndex.hpp diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index 15768b24ea..0f51cef7e9 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -5035,16 +5035,13 @@ msgstr "" msgid "System agents" msgstr "" -msgid "No plugin selected" -msgstr "" - msgid "Add plugin" msgstr "" -msgid "Select plugin" +msgid "Remove plugin" msgstr "" -msgid "Remove plugin" +msgid "No plugin selected" msgstr "" msgid "Configure" @@ -7197,6 +7194,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + msgid "Plugin Selection" msgstr "" @@ -8591,6 +8594,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "" diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po index d121446844..9c0d0da8a9 100644 --- a/localization/i18n/ca/OrcaSlicer_ca.po +++ b/localization/i18n/ca/OrcaSlicer_ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: 2025-03-15 10:55+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -5473,22 +5473,18 @@ msgstr "Format no vàlid. Format vectorial esperat: \"%1%\"" msgid "System agents" msgstr "Agents del sistema" -# AI Translated -msgid "No plugin selected" -msgstr "Cap connector seleccionat" - # AI Translated msgid "Add plugin" msgstr "Afegir connector" -# AI Translated -msgid "Select plugin" -msgstr "Seleccionar connector" - # AI Translated msgid "Remove plugin" msgstr "Eliminar connector" +# AI Translated +msgid "No plugin selected" +msgstr "Cap connector seleccionat" + # AI Translated msgid "Configure" msgstr "Configurar" @@ -7723,6 +7719,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Inferior" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "Selecció de connectors" @@ -9259,6 +9261,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Finestra emergent per seleccionar el mode d'agrupació de filaments" @@ -22233,6 +22244,10 @@ msgstr "" "Evitar la deformació( warping )\n" "Sabíeu que quan imprimiu materials propensos a deformar-se, com ara l'ABS, augmentar adequadament la temperatura del llit pot reduir la probabilitat de deformació?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "Seleccionar connector" + #~ msgid "Select the language" #~ msgstr "Seleccioneu l'idioma" diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po index 0fee4eedd1..63b362970a 100644 --- a/localization/i18n/cs/OrcaSlicer_cs.po +++ b/localization/i18n/cs/OrcaSlicer_cs.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: Jakub Hencl\n" "Language-Team: \n" @@ -5430,22 +5430,18 @@ msgstr "Neplatný formát. Očekávaný vektorový formát: \"%1%\"" msgid "System agents" msgstr "Systémoví agenti" -# AI Translated -msgid "No plugin selected" -msgstr "Není vybrán žádný plugin" - # AI Translated msgid "Add plugin" msgstr "Přidat plugin" -# AI Translated -msgid "Select plugin" -msgstr "Vybrat plugin" - # AI Translated msgid "Remove plugin" msgstr "Odebrat plugin" +# AI Translated +msgid "No plugin selected" +msgstr "Není vybrán žádný plugin" + # AI Translated msgid "Configure" msgstr "Konfigurovat" @@ -7690,6 +7686,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Spodní" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "Výběr pluginů" @@ -9213,6 +9215,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Zobrazit dialog pro výběr režimu seskupení filamentů" @@ -22214,6 +22225,10 @@ msgstr "" "Zamezte kroucení\n" "Víte, že při tisku materiálů náchylných ke kroucení, jako je ABS, může vhodné zvýšení teploty vyhřívané desky snížit pravděpodobnost kroucení?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "Vybrat plugin" + #~ msgid "Select the language" #~ msgstr "Zvolte jazyk" diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index 8b61d9e08d..8d7798eb80 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: Heiko Liebscher <hliebschergmail.com>\n" "Language-Team: \n" @@ -5335,22 +5335,18 @@ msgstr "Ungültiges Format. Erwartetes Vektorformat: \"%1%\"" msgid "System agents" msgstr "Systemagenten" -# AI Translated -msgid "No plugin selected" -msgstr "Kein Plugin ausgewählt" - # AI Translated msgid "Add plugin" msgstr "Plugin hinzufügen" -# AI Translated -msgid "Select plugin" -msgstr "Plugin auswählen" - # AI Translated msgid "Remove plugin" msgstr "Plugin entfernen" +# AI Translated +msgid "No plugin selected" +msgstr "Kein Plugin ausgewählt" + # AI Translated msgid "Configure" msgstr "Konfigurieren" @@ -7560,6 +7556,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Untere" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "Plugin-Auswahl" @@ -9084,6 +9086,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Popup zum Auswählen des Filament-Gruppierungsmodus" @@ -21634,6 +21645,10 @@ msgstr "" "Verwerfungen vermeiden\n" "Wussten Sie, dass beim Drucken von Materialien, die zu Verwerfungen neigen, wie z.B. ABS, durch eine entsprechende Erhöhung der Heizbetttemperatur die Wahrscheinlichkeit von Verwerfungen verringert werden kann?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "Plugin auswählen" + #~ msgid "Select the language" #~ msgstr "Sprache wählen" diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po index 0cdadcb5d3..cc044e6286 100644 --- a/localization/i18n/en/OrcaSlicer_en.po +++ b/localization/i18n/en/OrcaSlicer_en.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: \n" @@ -5031,16 +5031,13 @@ msgstr "" msgid "System agents" msgstr "" -msgid "No plugin selected" -msgstr "" - msgid "Add plugin" msgstr "" -msgid "Select plugin" +msgid "Remove plugin" msgstr "" -msgid "Remove plugin" +msgid "No plugin selected" msgstr "" msgid "Configure" @@ -7193,6 +7190,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + msgid "Plugin Selection" msgstr "" @@ -8587,6 +8590,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "" diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po index 9ea245a1bd..ee2214ed85 100644 --- a/localization/i18n/es/OrcaSlicer_es.po +++ b/localization/i18n/es/OrcaSlicer_es.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: Ian A. Bassi <>\n" "Language-Team: \n" @@ -5198,18 +5198,15 @@ msgstr "Formato inválido. Formato de vector esperado: \"%1%\"" msgid "System agents" msgstr "Agentes del sistema" -msgid "No plugin selected" -msgstr "Ningún plugin seleccionado" - msgid "Add plugin" msgstr "Añadir plugin" -msgid "Select plugin" -msgstr "Seleccionar plugin" - msgid "Remove plugin" msgstr "Eliminar plugin" +msgid "No plugin selected" +msgstr "Ningún plugin seleccionado" + msgid "Configure" msgstr "Configurar" @@ -7405,6 +7402,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Inferior" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + msgid "Plugin Selection" msgstr "Selección de plugins" @@ -8851,6 +8854,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Ventana emergente para seleccionar el modo de agrupación de filamentos" @@ -21180,6 +21192,9 @@ msgstr "" "Evita la deformación\n" "¿Sabías que al imprimir materiales propensos a la deformación como el ABS, aumentar adecuadamente la temperatura de la cama térmica puede reducir la probabilidad de deformaciones?" +#~ msgid "Select plugin" +#~ msgstr "Seleccionar plugin" + #~ msgid "Select the language" #~ msgstr "Seleccionar el idioma" diff --git a/localization/i18n/eu/OrcaSlicer_eu.po b/localization/i18n/eu/OrcaSlicer_eu.po index 558c1a80dc..4488f05951 100644 --- a/localization/i18n/eu/OrcaSlicer_eu.po +++ b/localization/i18n/eu/OrcaSlicer_eu.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: 2026-07-20 13:33+0200\n" "Last-Translator: Manu Goiogana <mgoiogana@gmail.com>\n" "Language-Team: \n" @@ -5247,18 +5247,15 @@ msgstr "Formatuak ez du balio. Espero den formatu bektoriala: \"%1%\"" msgid "System agents" msgstr "Sistema-agenteak" -msgid "No plugin selected" -msgstr "Ez da pluginik hautatu" - msgid "Add plugin" msgstr "Gehitu plugina" -msgid "Select plugin" -msgstr "Hautatu plugina" - msgid "Remove plugin" msgstr "Kendu plugina" +msgid "No plugin selected" +msgstr "Ez da pluginik hautatu" + msgid "Configure" msgstr "Konfiguratu" @@ -7452,6 +7449,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Behea" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + msgid "Plugin Selection" msgstr "Plugin-hautaketa" @@ -8932,6 +8935,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Erakutsi filamentuak taldekatzeko modua hautatzeko leihoa" @@ -21367,6 +21379,9 @@ msgstr "" "Saihestu okertzea\n" "Ba al zenekien ABS bezalako okertzeko joera duten materialak inprimatzean ohe beroaren tenperatura egoki igotzeak okertzeko probabilitatea murriztu dezakeela?" +#~ msgid "Select plugin" +#~ msgstr "Hautatu plugina" + #~ msgid "Select the language" #~ msgstr "Hautatu hizkuntza" diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index aaac6d576a..156e639fde 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Guislain Cyril, Thomas Lété\n" @@ -5285,22 +5285,18 @@ msgstr "Format invalide. Format vectoriel attendu : \"%1%\"" msgid "System agents" msgstr "Agents système" -# AI Translated -msgid "No plugin selected" -msgstr "Aucun plugin sélectionné" - # AI Translated msgid "Add plugin" msgstr "Ajouter un plugin" -# AI Translated -msgid "Select plugin" -msgstr "Sélectionner un plugin" - # AI Translated msgid "Remove plugin" msgstr "Supprimer le plugin" +# AI Translated +msgid "No plugin selected" +msgstr "Aucun plugin sélectionné" + # AI Translated msgid "Configure" msgstr "Configurer" @@ -7503,6 +7499,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Inférieur" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "Sélection des plugins" @@ -9000,6 +9002,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Fenêtre contextuelle pour sélectionner le mode de regroupement des filaments" @@ -21524,6 +21535,10 @@ msgstr "" "Éviter la déformation\n" "Saviez-vous que lors de l’impression de matériaux susceptibles de se déformer, tels que l’ABS, une augmentation appropriée de la température du plateau chauffant peut réduire la probabilité de déformation?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "Sélectionner un plugin" + #~ msgid "Select the language" #~ msgstr "Sélectionner la langue" diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po index f92b87a246..2514284794 100644 --- a/localization/i18n/hu/OrcaSlicer_hu.po +++ b/localization/i18n/hu/OrcaSlicer_hu.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -5382,22 +5382,18 @@ msgstr "Érvénytelen formátum. Elvárt vektor formátum: \"%1%\"" msgid "System agents" msgstr "Rendszerügynökök" -# AI Translated -msgid "No plugin selected" -msgstr "Nincs kiválasztott bővítmény" - # AI Translated msgid "Add plugin" msgstr "Bővítmény hozzáadása" -# AI Translated -msgid "Select plugin" -msgstr "Bővítmény kiválasztása" - # AI Translated msgid "Remove plugin" msgstr "Bővítmény eltávolítása" +# AI Translated +msgid "No plugin selected" +msgstr "Nincs kiválasztott bővítmény" + # AI Translated msgid "Configure" msgstr "Konfigurálás" @@ -7617,6 +7613,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Alsó" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "Bővítmény kiválasztása" @@ -9136,6 +9138,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Felugró ablak a filamentcsoportosítási mód kiválasztásához" @@ -21955,6 +21966,10 @@ msgstr "" "Kunkorodás elkerülése\n" "Tudtad, hogy a kunkorodásra hajlamos anyagok (például ABS) nyomtatásakor az asztal hőmérsékletének növelése csökkentheti a kunkorodás valószínűségét?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "Bővítmény kiválasztása" + #~ msgid "Select the language" #~ msgstr "Válaszd ki a nyelvet" diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po index 814c2b627d..d4db3efbfd 100644 --- a/localization/i18n/it/OrcaSlicer_it.po +++ b/localization/i18n/it/OrcaSlicer_it.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -5383,22 +5383,18 @@ msgstr "Formato non valido. Formato vettoriale previsto: \"%1%\"" msgid "System agents" msgstr "Agenti di sistema" -# AI Translated -msgid "No plugin selected" -msgstr "Nessun plugin selezionato" - # AI Translated msgid "Add plugin" msgstr "Aggiungi plugin" -# AI Translated -msgid "Select plugin" -msgstr "Seleziona plugin" - # AI Translated msgid "Remove plugin" msgstr "Rimuovi plugin" +# AI Translated +msgid "No plugin selected" +msgstr "Nessun plugin selezionato" + # AI Translated msgid "Configure" msgstr "Configura" @@ -7621,6 +7617,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Inferiore" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "Selezione plugin" @@ -9138,6 +9140,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Popup per selezionare la modalità di raggruppamento filamenti" @@ -21979,6 +21990,10 @@ msgstr "" "Evita le deformazioni\n" "Sapevi che quando si stampano materiali soggetti a deformazioni come l'ABS, aumentare in modo appropriato la temperatura del piano riscaldato può ridurre la probabilità di deformazione?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "Seleziona plugin" + #~ msgid "Select the language" #~ msgstr "Seleziona la lingua" diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po index c170f2f6c1..d9cd604979 100644 --- a/localization/i18n/ja/OrcaSlicer_ja.po +++ b/localization/i18n/ja/OrcaSlicer_ja.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -5396,22 +5396,18 @@ msgstr "無効なフォーマット、%1%であるはずです。" msgid "System agents" msgstr "システムエージェント" -# AI Translated -msgid "No plugin selected" -msgstr "プラグインが選択されていません" - # AI Translated msgid "Add plugin" msgstr "プラグインを追加" -# AI Translated -msgid "Select plugin" -msgstr "プラグインを選択" - # AI Translated msgid "Remove plugin" msgstr "プラグインを削除" +# AI Translated +msgid "No plugin selected" +msgstr "プラグインが選択されていません" + # AI Translated msgid "Configure" msgstr "設定" @@ -7627,6 +7623,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "底面" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "プラグインの選択" @@ -9158,6 +9160,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "フィラメントグルーピングモード選択のポップアップ" @@ -22534,6 +22545,10 @@ msgstr "" "反りを避ける\n" "ABSのような反りやすい素材を印刷する場合、ヒートベッドの温度を適切に上げることで、反りが発生する確率を下げることができることをご存知ですか?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "プラグインを選択" + #~ msgid "Select the language" #~ msgstr "言語を選択" diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po index 2e883a3f99..3ada1afc50 100644 --- a/localization/i18n/ko/OrcaSlicer_ko.po +++ b/localization/i18n/ko/OrcaSlicer_ko.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: 2025-06-02 17:12+0900\n" "Last-Translator: crwusiz <crwusiz@gmail.com>\n" "Language-Team: \n" @@ -5408,22 +5408,18 @@ msgstr "잘못된 형식입니다. 필요한 벡터 형식: \"%1%\"" msgid "System agents" msgstr "시스템 에이전트" -# AI Translated -msgid "No plugin selected" -msgstr "선택된 플러그인 없음" - # AI Translated msgid "Add plugin" msgstr "플러그인 추가" -# AI Translated -msgid "Select plugin" -msgstr "플러그인 선택" - # AI Translated msgid "Remove plugin" msgstr "플러그인 제거" +# AI Translated +msgid "No plugin selected" +msgstr "선택된 플러그인 없음" + # AI Translated msgid "Configure" msgstr "구성" @@ -7640,6 +7636,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "하부" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "플러그인 선택" @@ -9220,6 +9222,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "필라멘트 그룹화 모드를 선택하기 위한 팝업" @@ -22401,6 +22412,10 @@ msgstr "" "뒤틀림 방지\n" "ABS와 같이 뒤틀림이 발생하기 쉬운 소재를 출력할 때, 히트베드 온도를 적절하게 높이면 뒤틀림 가능성을 줄일 수 있다는 사실을 알고 계셨나요?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "플러그인 선택" + #~ msgid "Select the language" #~ msgstr "언어 선택" diff --git a/localization/i18n/list.txt b/localization/i18n/list.txt index a36ecd5cab..13be57a17e 100644 --- a/localization/i18n/list.txt +++ b/localization/i18n/list.txt @@ -179,6 +179,7 @@ src/slic3r/GUI/PublishDialog.cpp src/slic3r/GUI/PublishSettingsDialog.cpp src/slic3r/GUI/SavePresetDialog.cpp src/slic3r/GUI/Search.cpp +src/slic3r/GUI/SettingsIndex.cpp src/slic3r/GUI/Selection.cpp src/slic3r/GUI/SelectMachine.cpp src/slic3r/GUI/PrePrintChecker.cpp diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po index 6a642229f8..4ccc073ffc 100644 --- a/localization/i18n/lt/OrcaSlicer_lt.po +++ b/localization/i18n/lt/OrcaSlicer_lt.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: 2026-07-02 14:13+0300\n" "Last-Translator: Gintaras Kučinskas <sharanchius@gmail.com>\n" "Language-Team: \n" @@ -5369,22 +5369,18 @@ msgstr "Netinkamas formatas. Tinkamas vektorinis formatas: \"%1%\"" msgid "System agents" msgstr "Sisteminiai agentai" -# AI Translated -msgid "No plugin selected" -msgstr "Nepasirinktas joks papildinys" - # AI Translated msgid "Add plugin" msgstr "Pridėti papildinį" -# AI Translated -msgid "Select plugin" -msgstr "Pasirinkti papildinį" - # AI Translated msgid "Remove plugin" msgstr "Pašalinti papildinį" +# AI Translated +msgid "No plugin selected" +msgstr "Nepasirinktas joks papildinys" + # AI Translated msgid "Configure" msgstr "Konfigūruoti" @@ -7604,6 +7600,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Apatinis" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "Papildinio pasirinkimas" @@ -9119,6 +9121,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Iššokantis langas gijų grupavimo režimui pasirinkti" @@ -21681,6 +21692,10 @@ msgstr "" "Venkite deformacijų (warping)\n" "Ar žinojote, kad spausdinant medžiagas, kurios yra linkusios trauktis ir riestis (pvz., ABS), tinkamas kaitinamojo pagrindo temperatūros padidinimas gali sumažinti deformacijų (warping) tikimybę?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "Pasirinkti papildinį" + #~ msgid "Select the language" #~ msgstr "Pasirinkite kalbą" diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po index bcf29ef0ea..f056bb73e2 100644 --- a/localization/i18n/nl/OrcaSlicer_nl.po +++ b/localization/i18n/nl/OrcaSlicer_nl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -5878,22 +5878,18 @@ msgstr "Onjuist formaat. Het Vector formaat wordt verwacht: \"%1%\"" msgid "System agents" msgstr "Systeemagenten" -# AI Translated -msgid "No plugin selected" -msgstr "Geen plug-in geselecteerd" - # AI Translated msgid "Add plugin" msgstr "Plug-in toevoegen" -# AI Translated -msgid "Select plugin" -msgstr "Plug-in selecteren" - # AI Translated msgid "Remove plugin" msgstr "Plug-in verwijderen" +# AI Translated +msgid "No plugin selected" +msgstr "Geen plug-in geselecteerd" + # AI Translated msgid "Configure" msgstr "Configureren" @@ -8292,6 +8288,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Onderste" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "Plug-inselectie" @@ -9969,6 +9971,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + # AI Translated msgid "Pop up to select filament grouping mode" msgstr "Pop-up om de filamentgroeperingsmodus te kiezen" @@ -24121,6 +24132,10 @@ msgstr "" "Kromtrekken voorkomen\n" "Wist je dat bij het printen van materialen die gevoelig zijn voor kromtrekken, zoals ABS, een juiste verhoging van de temperatuur van het warmtebed de kans op kromtrekken kan verkleinen?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "Plug-in selecteren" + #~ msgid "Select the language" #~ msgstr "Kies de taal" diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po index 880d786d5e..c3a0c934ee 100644 --- a/localization/i18n/pl/OrcaSlicer_pl.po +++ b/localization/i18n/pl/OrcaSlicer_pl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer 2.3.0-rc\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: Krzysztof Morga <<tlumaczeniebs@gmail.com>>\n" "Language-Team: \n" @@ -5498,22 +5498,18 @@ msgstr "Nieprawidłowy format. Oczekiwano formatu wektorowego: „%1%”" msgid "System agents" msgstr "Agenci systemowi" -# AI Translated -msgid "No plugin selected" -msgstr "Nie wybrano wtyczki" - # AI Translated msgid "Add plugin" msgstr "Dodaj wtyczkę" -# AI Translated -msgid "Select plugin" -msgstr "Wybierz wtyczkę" - # AI Translated msgid "Remove plugin" msgstr "Usuń wtyczkę" +# AI Translated +msgid "No plugin selected" +msgstr "Nie wybrano wtyczki" + # AI Translated msgid "Configure" msgstr "Konfiguruj" @@ -7798,6 +7794,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Dół" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "Wybór wtyczek" @@ -9375,6 +9377,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Okno dialogowe do wyboru trybu grupowania filamentów" @@ -22578,6 +22589,10 @@ msgstr "" "Unikaj odkształceń\n" "Czy wiesz, że podczas drukowania filamentami podatnymi na odkształcenia, takimi jak ABS, odpowiednie zwiększenie temperatury podgrzewanej płyty może zmniejszyć prawdopodobieństwo odkształceń?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "Wybierz wtyczkę" + #~ msgid "Select the language" #~ msgstr "Wybierz język" diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 183c8f7088..86c240703b 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: 2026-07-26 11:14-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: Portuguese, Brazilian\n" @@ -5212,18 +5212,15 @@ msgstr "Formato inválido. Formato de vetor esperado: \"%1%\"" msgid "System agents" msgstr "Agentes do sistema" -msgid "No plugin selected" -msgstr "Nenhum plugin selecionado" - msgid "Add plugin" msgstr "Adicionar plugin" -msgid "Select plugin" -msgstr "Selecionar plugin" - msgid "Remove plugin" msgstr "Remover plugin" +msgid "No plugin selected" +msgstr "Nenhum plugin selecionado" + msgid "Configure" msgstr "Configurar" @@ -7423,6 +7420,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Inferior" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + msgid "Plugin Selection" msgstr "Seleção de plugins" @@ -8902,6 +8905,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Abrir seleção do modo de agrupamento de filamento" @@ -21292,6 +21304,9 @@ msgstr "" "Evitar empenamento\n" "Você sabia que ao imprimir materiais propensos ao empenamento como ABS, aumentar adequadamente a temperatura da mesa aquecida pode reduzir a probabilidade de empenamento?" +#~ msgid "Select plugin" +#~ msgstr "Selecionar plugin" + #~ msgid "Select the language" #~ msgstr "Selecione o idioma" diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po index 8d1bf38c13..898a19609d 100644 --- a/localization/i18n/ru/OrcaSlicer_ru.po +++ b/localization/i18n/ru/OrcaSlicer_ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer V2.5.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: 2026-02-25 13:38+0300\n" "Last-Translator: Felix14_v2\n" "Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg <andylg@yandex.ru>\n" @@ -5378,18 +5378,15 @@ msgstr "Недопустимый формат. Ожидаемый векторн msgid "System agents" msgstr "Системные агенты" -msgid "No plugin selected" -msgstr "Плагины не выбраны" - msgid "Add plugin" msgstr "Добавить плагин" -msgid "Select plugin" -msgstr "Выбрать плагин" - msgid "Remove plugin" msgstr "Удалить плагин" +msgid "No plugin selected" +msgstr "Плагины не выбраны" + msgid "Configure" msgstr "Настроить" @@ -7681,6 +7678,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Снизу" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + msgid "Plugin Selection" msgstr "Выбор плагинов" @@ -9182,6 +9185,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + # Запрашивать выбор режима группировки? msgid "Pop up to select filament grouping mode" msgstr "Всплывающее окно для выбора режима группировки материалов" @@ -22331,6 +22343,9 @@ msgstr "" "Предотвращение коробления материала\n" "Знаете ли вы, что при печати материалами, склонными к короблению, таких как ABS, повышение температуры подогреваемого стола может снизить эту вероятность?" +#~ msgid "Select plugin" +#~ msgstr "Выбрать плагин" + #~ msgid "Select the language" #~ msgstr "Выбор языка" diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po index e71aaaa56a..0477d1e69e 100644 --- a/localization/i18n/sv/OrcaSlicer_sv.po +++ b/localization/i18n/sv/OrcaSlicer_sv.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -5954,22 +5954,18 @@ msgstr "Ogiltligt format. Förväntat vector format: \"%1%\"" msgid "System agents" msgstr "Systemagenter" -# AI Translated -msgid "No plugin selected" -msgstr "Ingen insticksmodul vald" - # AI Translated msgid "Add plugin" msgstr "Lägg till insticksmodul" -# AI Translated -msgid "Select plugin" -msgstr "Välj insticksmodul" - # AI Translated msgid "Remove plugin" msgstr "Ta bort insticksmodul" +# AI Translated +msgid "No plugin selected" +msgstr "Ingen insticksmodul vald" + # AI Translated msgid "Configure" msgstr "Konfigurera" @@ -8379,6 +8375,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Bottenlager" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "Val av insticksmodul" @@ -10075,6 +10077,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + # AI Translated msgid "Pop up to select filament grouping mode" msgstr "Visa dialogruta för val av filamentgrupperingsläge" @@ -24409,6 +24420,10 @@ msgstr "" "Undvik vridning\n" "Visste du att när du skriver ut material som är benägna att vrida, såsom ABS, kan en lämplig ökning av värmebäddens temperatur minska sannolikheten för vridning?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "Välj insticksmodul" + #~ msgid "Select the language" #~ msgstr "Välj språk" diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po index 9c0bab2947..50f3e6b9f7 100644 --- a/localization/i18n/th/OrcaSlicer_th.po +++ b/localization/i18n/th/OrcaSlicer_th.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: 2026-06-19 13:40+0700\n" "Last-Translator: Icezaza\n" "Language-Team: Thai\n" @@ -5363,22 +5363,18 @@ msgstr "รูปแบบไม่ถูกต้อง รูปแบบเ msgid "System agents" msgstr "เอเจนต์ระบบ" -# AI Translated -msgid "No plugin selected" -msgstr "ไม่ได้เลือกปลั๊กอิน" - # AI Translated msgid "Add plugin" msgstr "เพิ่มปลั๊กอิน" -# AI Translated -msgid "Select plugin" -msgstr "เลือกปลั๊กอิน" - # AI Translated msgid "Remove plugin" msgstr "ลบปลั๊กอิน" +# AI Translated +msgid "No plugin selected" +msgstr "ไม่ได้เลือกปลั๊กอิน" + # AI Translated msgid "Configure" msgstr "กำหนดค่า" @@ -7577,6 +7573,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "ล่าง" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "การเลือกปลั๊กอิน" @@ -9088,6 +9090,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "ปรากฏขึ้นเพื่อเลือกโหมดการจัดกลุ่มเส้นพลาสติก" @@ -21709,6 +21720,10 @@ msgstr "" "หลีกเลี่ยงการบิดเบี้ยว\n" "คุณรู้หรือไม่ว่าเมื่อพิมพ์วัสดุที่มีแนวโน้มที่จะเกิดการบิดเบี้ยว เช่น ABS การเพิ่มอุณหภูมิฐานพิมพ์อย่างเหมาะสมสามารถลดความน่าจะเป็นของการบิดเบี้ยวได้" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "เลือกปลั๊กอิน" + #~ msgid "Select the language" #~ msgstr "เลือกภาษา" diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index 864bd15610..56302c41ad 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: 2026-08-21 23:18+0300\n" "Last-Translator: GlauTech\n" "Language-Team: \n" @@ -5426,22 +5426,18 @@ msgstr "Geçersiz format. Beklenen vektör formatı: \"%1%\"" msgid "System agents" msgstr "Sistem aracıları" -# AI Translated -msgid "No plugin selected" -msgstr "Eklenti seçilmedi" - # AI Translated msgid "Add plugin" msgstr "Eklenti ekle" -# AI Translated -msgid "Select plugin" -msgstr "Eklenti seç" - # AI Translated msgid "Remove plugin" msgstr "Eklentiyi kaldır" +# AI Translated +msgid "No plugin selected" +msgstr "Eklenti seçilmedi" + # AI Translated msgid "Configure" msgstr "Yapılandır" @@ -7666,6 +7662,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Alt" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "Eklenti Seçimi" @@ -9193,6 +9195,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "Filament gruplama modunu seçmek için açılır pencere" @@ -22164,6 +22175,10 @@ msgstr "" "Eğilmeyi önleyin\n" "ABS gibi bükülmeye yatkın malzemelere baskı yaparken, ısıtma yatağı sıcaklığının uygun şekilde arttırılmasının bükülme olasılığını azaltabileceğini biliyor muydunuz?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "Eklenti seç" + #~ msgid "Select the language" #~ msgstr "Dili seçin" diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po index 5c352f52dd..fd6c32585a 100644 --- a/localization/i18n/uk/OrcaSlicer_uk.po +++ b/localization/i18n/uk/OrcaSlicer_uk.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: orcaslicerua\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: 2026-07-17 16:25+0300\n" "Last-Translator: Andrij Mizyk <andm1zyk@proton.me>\n" "Language-Team: Ukrainian\n" @@ -5375,22 +5375,18 @@ msgstr "Невірний формат. Очікуваний векторний msgid "System agents" msgstr "Системні агенти" -# AI Translated -msgid "No plugin selected" -msgstr "Плагін не вибрано" - # AI Translated msgid "Add plugin" msgstr "Додати плагін" -# AI Translated -msgid "Select plugin" -msgstr "Вибрати плагін" - # AI Translated msgid "Remove plugin" msgstr "Вилучити плагін" +# AI Translated +msgid "No plugin selected" +msgstr "Плагін не вибрано" + # AI Translated msgid "Configure" msgstr "Налаштувати" @@ -7650,6 +7646,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Низ" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "Вибір плагіна" @@ -9211,6 +9213,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + # AI Translated msgid "Pop up to select filament grouping mode" msgstr "Показувати вікно вибору режиму групування філаментів" @@ -22325,6 +22336,10 @@ msgstr "" "Уникнення деформації\n" "Чи знаєте ви, що при друку матеріалами, схильними до деформації, такими як ABS, відповідне підвищення температури столу може зменшити ймовірність деформації?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "Вибрати плагін" + #~ msgid "Select the language" #~ msgstr "Вибрати мову" diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po index dedb1d33cb..7db1c04e0c 100644 --- a/localization/i18n/vi/OrcaSlicer_vi.po +++ b/localization/i18n/vi/OrcaSlicer_vi.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: 2025-10-02 17:43+0700\n" "Last-Translator: \n" "Language-Team: hainguyen.ts13@gmail.com\n" @@ -5679,22 +5679,18 @@ msgstr "Định dạng không hợp lệ. Mong đợi định dạng vector: \"% msgid "System agents" msgstr "Tác nhân hệ thống" -# AI Translated -msgid "No plugin selected" -msgstr "Chưa chọn plugin" - # AI Translated msgid "Add plugin" msgstr "Thêm plugin" -# AI Translated -msgid "Select plugin" -msgstr "Chọn plugin" - # AI Translated msgid "Remove plugin" msgstr "Xóa plugin" +# AI Translated +msgid "No plugin selected" +msgstr "Chưa chọn plugin" + # AI Translated msgid "Configure" msgstr "Cấu hình" @@ -8035,6 +8031,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "Dưới" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "Chọn plugin" @@ -9674,6 +9676,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + # AI Translated msgid "Pop up to select filament grouping mode" msgstr "Hiện cửa sổ để chọn chế độ nhóm filament" @@ -23048,6 +23059,10 @@ msgstr "" "Tránh cong vênh\n" "Bạn có biết rằng khi in vật liệu dễ cong vênh như ABS, tăng nhiệt độ bàn nóng một cách thích hợp có thể giảm xác suất cong vênh không?" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "Chọn plugin" + #~ msgid "Select the language" #~ msgstr "Chọn ngôn ngữ" diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po index cfacff0458..4338213c14 100644 --- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po +++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Slic3rPE\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: 2026-06-11 12:37-0300\n" "Last-Translator: Handle <mail@bysb.net>\n" "Language-Team: \n" @@ -5219,22 +5219,18 @@ msgstr "无效格式,应该是\"%1%\"这种数组格式" msgid "System agents" msgstr "系统代理" -# AI Translated -msgid "No plugin selected" -msgstr "未选择插件" - # AI Translated msgid "Add plugin" msgstr "添加插件" -# AI Translated -msgid "Select plugin" -msgstr "选择插件" - # AI Translated msgid "Remove plugin" msgstr "移除插件" +# AI Translated +msgid "No plugin selected" +msgstr "未选择插件" + # AI Translated msgid "Configure" msgstr "配置" @@ -7430,6 +7426,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "底部" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + msgid "Plugin Selection" msgstr "插件选择" @@ -8910,6 +8912,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "弹出选择耗材丝分组模式" @@ -21457,6 +21468,10 @@ msgstr "" "避免翘曲\n" "您知道吗?打印ABS这类易翘曲材料时,适当提高热床温度可以降低翘曲的概率。" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "选择插件" + #~ msgid "Select the language" #~ msgstr "选择语言" diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po index fc24dcfaa2..2f55ba6d8f 100644 --- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po +++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 12:33+0800\n" +"POT-Creation-Date: 2026-09-11 17:15+0800\n" "PO-Revision-Date: 2025-11-28 13:48-0600\n" "Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n" "Language-Team: \n" @@ -5348,22 +5348,18 @@ msgstr "無效格式,應該是「%1%」這種格式" msgid "System agents" msgstr "系統代理程式" -# AI Translated -msgid "No plugin selected" -msgstr "未選擇外掛" - # AI Translated msgid "Add plugin" msgstr "新增外掛" -# AI Translated -msgid "Select plugin" -msgstr "選擇外掛" - # AI Translated msgid "Remove plugin" msgstr "移除外掛" +# AI Translated +msgid "No plugin selected" +msgstr "未選擇外掛" + # AI Translated msgid "Configure" msgstr "設定" @@ -7569,6 +7565,12 @@ msgctxt "Layers" msgid "Bottom" msgstr "底部" +msgid "This setting does not specify a plugin capability type." +msgstr "" + +msgid "This setting specifies an unrecognized plugin capability type: " +msgstr "" + # AI Translated msgid "Plugin Selection" msgstr "外掛選擇" @@ -9083,6 +9085,15 @@ msgstr "" msgid "When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page." msgstr "" +msgid "Recent actions" +msgstr "" + +msgid "actions" +msgstr "" + +msgid "How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions." +msgstr "" + msgid "Pop up to select filament grouping mode" msgstr "彈出視窗選擇線材分組模式" @@ -21671,6 +21682,10 @@ msgstr "" "避免翹曲\n" "您知道嗎?當列印容易翹曲的材料(如 ABS)時,適當提高熱床溫度可以降低翹曲的機率。" +# AI Translated +#~ msgid "Select plugin" +#~ msgstr "選擇外掛" + #~ msgid "Select the language" #~ msgstr "選擇語言" diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 1647a26844..2b28622db4 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -478,6 +478,8 @@ set(SLIC3R_GUI_SOURCES GUI/SkipPartCanvas.hpp GUI/Search.cpp GUI/Search.hpp + GUI/SettingsIndex.cpp + GUI/SettingsIndex.hpp GUI/Selection.cpp GUI/Selection.hpp GUI/SelectMachine.cpp diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index 3770d6fb68..3f751e6913 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -8,7 +8,7 @@ #include "Notebook.hpp" #include "OptionsGroup.hpp" #include "Plater.hpp" -#include "Search.hpp" +#include "SettingsIndex.hpp" #include "Tab.hpp" #include "slic3r/plugin/PluginManager.hpp" @@ -526,11 +526,11 @@ void ActionRegistry::materialize_setting_actions() { assert(wxThread::IsMain()); - // Reuse the Sidebar's live searcher: it's the only OptionsSearcher whose groups_and_categories - // map is populated (Tab::add_key feeds it at build time), and it already mirrors the current + // Reuse the Sidebar's live settings index: it's the only catalog whose group/category map is + // populated (Tab::add_key feeds it at build time), and it already mirrors the current // configs/printer-technology. Use the all-modes view so the Speed Dial lists every setting, // including those above the user's current mode, and can prompt to switch before jumping. - const std::vector<Search::Option>& options = wxGetApp().sidebar().get_searcher().all_modes_options(); + const std::vector<Search::Option>& options = wxGetApp().sidebar().settings_index().all_options(); // Load the persisted per-action state ONCE (not per-option) so a re-materialised setting keeps // its recency/favourite; mirroring seed_state but amortised over the whole option set. diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 2fc41ba90b..6c950ca501 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -248,7 +248,7 @@ void OptionsGroup::append_line(const Line& line) // affordance for it. Settings tabs only; the searcher already exists by the time tabs are built. if (m_use_custom_ctrl && !line.label_path.empty()) for (const auto& opt : line.get_options()) - wxGetApp().sidebar().get_searcher().set_path(opt.opt_id, static_cast<Preset::Type>(config_type()), line.label_path); + wxGetApp().sidebar().settings_index().set_path(opt.opt_id, static_cast<Preset::Type>(config_type()), line.label_path); if (line.full_width && (line.widget != nullptr || !line.get_extra_widgets().empty())) return; @@ -656,7 +656,7 @@ Option ConfigOptionsGroup::get_option(const std::string& opt_key, int opt_index m_opt_map.emplace(opt_id, pair); if (m_use_custom_ctrl) // fill group and category values just for options from Settings Tab - wxGetApp().sidebar().get_searcher().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category(), this->icon); + wxGetApp().sidebar().settings_index().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category(), this->icon); return Option(*m_config->def()->get(opt_key), opt_id); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 555cd7a9ad..a28ebb545b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6419,6 +6419,11 @@ Search::OptionsSearcher& Sidebar::get_searcher() return p->searcher; } +Search::SettingsIndex& Sidebar::settings_index() +{ + return p->searcher.index(); +} + std::string& Sidebar::get_search_line() { return p->searcher.search_string(); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 5536efb05b..3342eb3311 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -282,6 +282,7 @@ public: std::vector<std::string>& types, std::vector<size_t>* config_indices = nullptr); Search::OptionsSearcher& get_searcher(); + Search::SettingsIndex& settings_index(); std::string& get_search_line(); void update_printer_thumbnail(); diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index 916d93a40c..427271ad2f 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -62,107 +62,12 @@ static char marker_by_type(Preset::Type type, PrinterTechnology pt) } } -std::string Option::opt_key() const { return into_u8(key).substr(2); } - void FoundOption::get_marked_label_and_tooltip(const char **label_, const char **tooltip_) const { *label_ = marked_label.c_str(); *tooltip_ = tooltip.c_str(); } -template<class T> -// void change_opt_key(std::string& opt_key, DynamicPrintConfig* config) -void change_opt_key(std::string &opt_key, DynamicPrintConfig *config, int &cnt) -{ - T *opt_cur = static_cast<T *>(config->option(opt_key)); - cnt = opt_cur->values.size(); - return; - - if (opt_cur->values.size() > 0) opt_key += "#" + std::to_string(0); -} - -static std::string get_key(const std::string &opt_key, Preset::Type type) { return std::to_string(int(type)) + ";" + opt_key; } - -void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode) -{ - auto emplace = [this, type](std::vector<Option> &dst, const std::string &key, const wxString &label, ConfigOptionMode opt_mode, std::string tooltip) { - const GroupAndCategory &gc = groups_and_categories[key]; - if (gc.group.IsEmpty() || gc.category.IsEmpty()) return; - - wxString suffix; - wxString suffix_local; - if (gc.category == "Machine limits") { - //suffix = key.back() == '1' ? L("Stealth") : L("Normal"); - suffix = key.back() == '1' ? wxEmptyString : wxEmptyString; - suffix_local = " " + _(suffix); - suffix = " " + suffix; - } - - if (!label.IsEmpty()) - dst.emplace_back(Option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(), gc.group.ToStdWstring(), - _(gc.group).ToStdWstring(), into_u8(gc.icon), gc.category.ToStdWstring(), GUI::Tab::translate_category(gc.category, type).ToStdWstring(), - false, opt_mode, std::move(tooltip), gc.path}); - }; - - for (std::string opt_key : config->keys()) { - const ConfigOptionDef &opt = config->def()->options.at(opt_key); - const bool in_filtered = opt.mode <= mode; - - int cnt = 0; - - if ((type == Preset::TYPE_SLA_MATERIAL || type == Preset::TYPE_PRINTER || type == Preset::TYPE_PRINT) && opt_key != "printable_area") - switch (config->option(opt_key)->type()) { - case coInts: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break; - case coBools: change_opt_key<ConfigOptionBools>(opt_key, config, cnt); break; - case coFloats: change_opt_key<ConfigOptionFloats>(opt_key, config, cnt); break; - case coStrings: change_opt_key<ConfigOptionStrings>(opt_key, config, cnt); break; - case coPercents: change_opt_key<ConfigOptionPercents>(opt_key, config, cnt); break; - case coPoints: change_opt_key<ConfigOptionPoints>(opt_key, config, cnt); break; - // BBS - case coEnums: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break; - default: break; - } - - if (type == Preset::TYPE_FILAMENT && filament_options_with_variant.find(opt_key) != filament_options_with_variant.end()) - opt_key += "#0"; - - wxString label = opt.full_label.empty() ? opt.label : opt.full_label; - - std::string key = get_key(opt_key, type); - auto add = [&](const std::string &k) { - if (in_filtered) - emplace(options, k, label, opt.mode, into_u8(_(opt.tooltip))); - emplace(options_all_modes, k, label, opt.mode, into_u8(_(opt.tooltip))); - }; - if (cnt == 0) - add(key); - else - for (int i = 0; i < cnt; ++i) - // ! It's very important to use "#". opt_key#n is a real option key used in GroupAndCategory - add(key + "#" + std::to_string(i)); - } -} - -void OptionsSearcher::sort_options() -{ - // Both views are label-sorted and multi_category-marked. They are separate consumers (the sidebar - // search and the Speed Dial); keeping them in sync here prevents the all-modes view from silently - // diverging in order or flags. - auto sort_and_mark = [](std::vector<Option> &v) { - std::sort(v.begin(), v.end(), [](const Option &o1, const Option &o2) { return o1.label < o2.label; }); - Option *last = nullptr; - for (auto &opt : v) { - if (last && last->label == opt.label && last->group == opt.group && last->type == opt.type && last->category != opt.category) { - last->multi_category = true; - opt.multi_category = true; - } - last = &opt; - } - }; - sort_and_mark(options); - sort_and_mark(options_all_modes); -} - // Mark a string using ColorMarkerStart and ColorMarkerEnd symbols static std::wstring mark_string(const std::wstring &str, const std::vector<uint16_t> &matches, Preset::Type type, PrinterTechnology pt) { @@ -247,7 +152,8 @@ bool OptionsSearcher::search(const std::string &search, bool force /* = false*/, return wxString(marker_by_type(opt.type, printer_technology)) + opt.category_local + sep + opt.group_local + sep + opt.label_local; }; - std::vector<uint16_t> matches, matches2; + std::vector<uint16_t> matches, matches2; + const std::vector<Option> &options = m_index.options(); for (size_t i = 0; i < options.size(); i++) { const Option &opt = options[i]; if (full_list) { @@ -319,117 +225,21 @@ OptionsSearcher::~OptionsSearcher() {} void OptionsSearcher::init(std::vector<InputInfo> input_values) { - options.clear(); - options_all_modes.clear(); - for (auto i : input_values) append_options(i.config, i.type, i.mode); - sort_options(); + m_index.init(std::move(input_values)); search(search_line, true, search_type); } void OptionsSearcher::apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode) { - // options_all_modes is a separate consumer (the Speed Dial), so "nothing initialised yet" means - // both views are empty - the mode-filtered options can be empty while the all-modes view is not. - if (options.empty() && options_all_modes.empty()) return; - - options.erase(std::remove_if(options.begin(), options.end(), [type](Option opt) { return opt.type == type; }), options.end()); - options_all_modes.erase(std::remove_if(options_all_modes.begin(), options_all_modes.end(), [type](Option opt) { return opt.type == type; }), - options_all_modes.end()); - - append_options(config, type, mode); - - sort_options(); - - search(search_line, true, search_type); + if (m_index.apply(config, type, mode)) + search(search_line, true, search_type); } const Option &OptionsSearcher::get_option(size_t pos_in_filter) const { assert(pos_in_filter != size_t(-1) && found[pos_in_filter].option_idx != size_t(-1)); - return options[found[pos_in_filter].option_idx]; -} - -const Option &OptionsSearcher::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const -{ - std::string opt_key2 = opt_key; - if (auto n = opt_key.find('#'); n != std::string::npos) { - variant_index = std::atoi(opt_key.c_str() + n + 1); - opt_key2 = opt_key.substr(0, n); - } - auto it = std::lower_bound(options.begin(), options.end(), Option({boost::nowide::widen(get_key(opt_key2, type))})); - // BBS: return the 0th option when not found in searcher caused by mode difference - // assert(it != options.end()); - if (it == options.end()) { variant_index = -2 ; return options[0]; } - if (it->opt_key() == opt_key2) { - variant_index = -1; - } else { - const std::string opt_key3 = opt_key2 + "#"; - it = std::lower_bound(it, options.end(), Option({boost::nowide::widen(get_key(opt_key3, type))})); - if (it == options.end() || it->opt_key().compare(0, opt_key3.length(), opt_key3) != 0) { - variant_index = -2; // Not found - return options[0]; - } - auto it2 = it; - ++it2; - if (it2 != options.end() && it2->opt_key().compare(0, opt_key3.length(), opt_key3) == 0 - && printer_options_with_variant_1.find(opt_key2) == printer_options_with_variant_1.end()) - variant_index = -2; - } - - return options[it - options.begin()]; -} - -static Option create_option(const std::string &opt_key, const wxString &label, Preset::Type type, const GroupAndCategory &gc) -{ - wxString suffix; - wxString suffix_local; - if (gc.category == "Machine limits") { - //suffix = opt_key.back() == '1' ? L("Stealth") : L("Normal"); - suffix = opt_key.back() == '1' ? wxEmptyString : wxEmptyString; - suffix_local = " " + _(suffix); - suffix = " " + suffix; - } - - wxString category = gc.category; - if (type == Preset::TYPE_PRINTER && category.Contains("Extruder ")) { - std::string opt_idx = opt_key.substr(opt_key.find("#") + 1); - category = wxString::Format("%s %d", "Extruder", atoi(opt_idx.c_str()) + 1); - } - - return Option{boost::nowide::widen(get_key(opt_key, type)), - type, - (label + suffix).ToStdWstring(), - (_(label) + suffix_local).ToStdWstring(), - gc.group.ToStdWstring(), - _(gc.group).ToStdWstring(), - into_u8(gc.icon), - gc.category.ToStdWstring(), - GUI::Tab::translate_category(category, type).ToStdWstring()}; -} - -Option OptionsSearcher::get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const -{ - std::string key = get_key(opt_key, type); - auto it = std::lower_bound(options.begin(), options.end(), Option({boost::nowide::widen(key)})); - // BBS: return the 0th option when not found in searcher caused by mode difference - if (it == options.end()) return options[0]; - if (it->key == boost::nowide::widen(key)) return options[it - options.begin()]; - if (groups_and_categories.find(key) == groups_and_categories.end()) { - size_t pos = key.find('#'); - if (pos == std::string::npos) return options[it - options.begin()]; - - std::string zero_opt_key = key.substr(0, pos + 1) + "0"; - - if (groups_and_categories.find(zero_opt_key) == groups_and_categories.end()) return options[it - options.begin()]; - - return create_option(opt_key, label, type, groups_and_categories.at(zero_opt_key)); - } - - const GroupAndCategory &gc = groups_and_categories.at(key); - if (gc.group.IsEmpty() || gc.category.IsEmpty()) return options[it - options.begin()]; - - return create_option(opt_key, label, type, gc); + return m_index.option_at(found[pos_in_filter].option_idx); } void OptionsSearcher::show_dialog(Preset::Type type, wxWindow *parent, TextInput *input, wxWindow* ssearch_btn) @@ -456,23 +266,6 @@ void OptionsSearcher::dlg_msw_rescale() { if (search_dialog) search_dialog->msw_rescale(); } - -void OptionsSearcher::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, const wxString &icon) -{ - // Update fields in place so a page rebuild (get_option after set_path) doesn't drop the - // previously recorded wiki path. - GroupAndCategory &gc = groups_and_categories[get_key(opt_key, type)]; - gc.group = group; - gc.category = category; - gc.icon = icon; -} - -void OptionsSearcher::set_path(const std::string &opt_key, Preset::Type type, const std::string &path) -{ - if (path.empty()) - return; - groups_and_categories[get_key(opt_key, type)].path = path; -} //------------------------------------------ // SearchItem //------------------------------------------ diff --git a/src/slic3r/GUI/Search.hpp b/src/slic3r/GUI/Search.hpp index 3cec0ba8c2..92ccd4d28c 100644 --- a/src/slic3r/GUI/Search.hpp +++ b/src/slic3r/GUI/Search.hpp @@ -19,6 +19,7 @@ #include "wxExtensions.hpp" #include "GUI_Utils.hpp" #include "libslic3r/Preset.hpp" +#include "SettingsIndex.hpp" #include "Widgets/ScrolledWindow.hpp" #include "Widgets/TextInput.hpp" #include "Widgets/PopupWindow.hpp" @@ -34,45 +35,6 @@ namespace Search { class SearchDialog; -struct InputInfo -{ - DynamicPrintConfig *config{nullptr}; - Preset::Type type{Preset::TYPE_INVALID}; - ConfigOptionMode mode{comSimple}; -}; - -struct GroupAndCategory -{ - wxString group; - wxString category; - wxString icon; // icon of the group's own header, or empty - std::string path; // wiki path (Line::label_path) of the option's line, or empty -}; - -struct Option -{ - // bool operator<(const Option& other) const { return other.label > this->label; } - bool operator<(const Option &other) const { return other.key > this->key; } - - // Fuzzy matching works at a character level. Thus matching with wide characters is a safer bet than with short characters, - // though for some languages (Chinese?) it may not work correctly. - std::wstring key; - Preset::Type type{Preset::TYPE_INVALID}; - std::wstring label; - std::wstring label_local; - std::wstring group; - std::wstring group_local; - std::string group_icon; // SVG base name of the group's own header icon, or empty - std::wstring category; - std::wstring category_local; - bool multi_category { false }; - ConfigOptionMode mode{comSimple}; // option's visibility threshold; drives the Speed Dial's mode prompt - std::string tooltip; // localized ConfigOptionDef::tooltip, or empty - std::string wiki_path; // Line::label_path for the option's row, or empty - - std::string opt_key() const; -}; - struct FoundOption { // UTF8 encoding, to be consumed by ImGUI by reference. @@ -96,28 +58,19 @@ struct OptionViewParameters class OptionsSearcher { - std::string search_line; - Preset::Type search_type = Preset::TYPE_INVALID; + SettingsIndex m_index; - std::map<std::string, GroupAndCategory> groups_and_categories; - PrinterTechnology printer_technology; - - std::vector<Option> options{}; - // Every option regardless of the current UI mode (Simple/Advanced/Expert/Developer), for the - // Speed Dial. The sidebar search keeps using the mode-filtered `options`. - std::vector<Option> options_all_modes{}; + std::string search_line; + Preset::Type search_type = Preset::TYPE_INVALID; + PrinterTechnology printer_technology; std::vector<FoundOption> found{}; - void append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode); - - void sort_options(); void sort_found() { std::sort(found.begin(), found.end(), [](const FoundOption &f1, const FoundOption &f2) { return f1.outScore > f2.outScore || (f1.outScore == f2.outScore && f1.label < f2.label); }); }; - size_t options_size() const { return options.size(); } size_t found_size() const { return found.size(); } public: @@ -128,44 +81,29 @@ public: OptionsSearcher(); ~OptionsSearcher(); + SettingsIndex & index() { return m_index; } + const SettingsIndex &index() const { return m_index; } + + // Rebuild the catalog and re-run the current query so the cached results track it. void init(std::vector<InputInfo> input_values); void apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode); + bool search(); bool search(const std::string &search, bool force = false, Preset::Type type = Preset::TYPE_INVALID); - void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, - const wxString &icon = wxEmptyString); - - // Records the wiki path of an option's row (Line::label_path) so the Speed Dial can offer a - // "open wiki" affordance. Empty paths are ignored. - void set_path(const std::string &opt_key, Preset::Type type, const std::string &path); - size_t size() const { return found_size(); } const FoundOption &operator[](const size_t pos) const noexcept { return found[pos]; } const Option & get_option(size_t pos_in_filter) const; - const Option & get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const; - Option get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const; const std::vector<FoundOption> &found_options() { return found; } - const GroupAndCategory & get_group_and_category(const std::string &opt_key) { return groups_and_categories[opt_key]; } std::string & search_string() { return search_line; } void set_printer_technology(PrinterTechnology pt) { printer_technology = pt; } - void sort_options_by_key() - { - std::sort(options.begin(), options.end(), [](const Option &o1, const Option &o2) { return o1.key < o2.key; }); - } - void sort_options_by_label() { sort_options(); } - void show_dialog(Preset::Type type, wxWindow *parent, TextInput *input, wxWindow *ssearch_btn); void dlg_sys_color_changed(); void dlg_msw_rescale(); - - // Every option across all UI modes (Developer included), regardless of the current mode. - // Used by the Speed Dial so it can list settings the user would have to switch mode to edit. - const std::vector<Option>& all_modes_options() const { return options_all_modes; } }; //------------------------------------------ diff --git a/src/slic3r/GUI/SettingsIndex.cpp b/src/slic3r/GUI/SettingsIndex.cpp new file mode 100644 index 0000000000..6b0a01893d --- /dev/null +++ b/src/slic3r/GUI/SettingsIndex.cpp @@ -0,0 +1,229 @@ +#include "SettingsIndex.hpp" + +#include <algorithm> +#include <cstddef> +#include <cstdlib> +#include <string> +#include <vector> + +#include <boost/nowide/convert.hpp> + +#include "GUI.hpp" +#include "I18N.hpp" +#include "Tab.hpp" + +#include "libslic3r/PrintConfig.hpp" + +namespace Slic3r { + +using GUI::into_u8; + +namespace Search { + +static std::string get_key(const std::string &opt_key, Preset::Type type) { return std::to_string(int(type)) + ";" + opt_key; } + +std::string Option::opt_key() const { return into_u8(key).substr(2); } + +template<class T> +// void change_opt_key(std::string& opt_key, DynamicPrintConfig* config) +void change_opt_key(std::string &opt_key, DynamicPrintConfig *config, int &cnt) +{ + T *opt_cur = static_cast<T *>(config->option(opt_key)); + cnt = opt_cur->values.size(); + return; + + if (opt_cur->values.size() > 0) opt_key += "#" + std::to_string(0); +} + +// Single assembler for an indexed Option, shared by append_options() and create_option(), so a new +// Option field is only wired up in one place. +static Option make_option(const std::string &key, Preset::Type type, const wxString &label, const GroupAndCategory &gc, + ConfigOptionMode mode, const std::string &tooltip, bool rewrite_extruder_category) +{ + wxString suffix; + wxString suffix_local; + if (gc.category == "Machine limits") { + //suffix = key.back() == '1' ? L("Stealth") : L("Normal"); + suffix = key.back() == '1' ? wxEmptyString : wxEmptyString; + suffix_local = " " + _(suffix); + suffix = " " + suffix; + } + + wxString category = gc.category; + if (rewrite_extruder_category && type == Preset::TYPE_PRINTER && category.Contains("Extruder ")) { + std::string opt_idx = key.substr(key.find("#") + 1); + category = wxString::Format("%s %d", "Extruder", atoi(opt_idx.c_str()) + 1); + } + + return Option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(), + gc.group.ToStdWstring(), _(gc.group).ToStdWstring(), into_u8(gc.icon), gc.category.ToStdWstring(), + GUI::Tab::translate_category(category, type).ToStdWstring(), false, mode, tooltip, gc.path}; +} + +void SettingsIndex::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode) +{ + for (std::string opt_key : config->keys()) { + const ConfigOptionDef &opt = config->def()->options.at(opt_key); + const bool in_filtered = opt.mode <= mode; + + int cnt = 0; + + if ((type == Preset::TYPE_SLA_MATERIAL || type == Preset::TYPE_PRINTER || type == Preset::TYPE_PRINT) && opt_key != "printable_area") + switch (config->option(opt_key)->type()) { + case coInts: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break; + case coBools: change_opt_key<ConfigOptionBools>(opt_key, config, cnt); break; + case coFloats: change_opt_key<ConfigOptionFloats>(opt_key, config, cnt); break; + case coStrings: change_opt_key<ConfigOptionStrings>(opt_key, config, cnt); break; + case coPercents: change_opt_key<ConfigOptionPercents>(opt_key, config, cnt); break; + case coPoints: change_opt_key<ConfigOptionPoints>(opt_key, config, cnt); break; + // BBS + case coEnums: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break; + default: break; + } + + if (type == Preset::TYPE_FILAMENT && filament_options_with_variant.find(opt_key) != filament_options_with_variant.end()) + opt_key += "#0"; + + wxString label = opt.full_label.empty() ? opt.label : opt.full_label; + + std::string key = get_key(opt_key, type); + auto add = [&](const std::string &k) { + const GroupAndCategory &gc = m_groups_and_categories[k]; + if (gc.group.IsEmpty() || gc.category.IsEmpty() || label.IsEmpty()) return; + + const std::string tooltip = into_u8(_(opt.tooltip)); + if (in_filtered) + m_options.emplace_back(make_option(k, type, label, gc, opt.mode, tooltip, false)); + m_all_modes.emplace_back(make_option(k, type, label, gc, opt.mode, tooltip, false)); + }; + if (cnt == 0) + add(key); + else + for (int i = 0; i < cnt; ++i) + // ! It's very important to use "#". opt_key#n is a real option key used in GroupAndCategory + add(key + "#" + std::to_string(i)); + } +} + +void SettingsIndex::sort_options() +{ + // Both views are label-sorted and multi_category-marked. They are separate consumers (the sidebar + // search and the Speed Dial); keeping them in sync here prevents the all-modes view from silently + // diverging in order or flags. + auto sort_and_mark = [](std::vector<Option> &v) { + std::sort(v.begin(), v.end(), [](const Option &o1, const Option &o2) { return o1.label < o2.label; }); + Option *last = nullptr; + for (auto &opt : v) { + if (last && last->label == opt.label && last->group == opt.group && last->type == opt.type && last->category != opt.category) { + last->multi_category = true; + opt.multi_category = true; + } + last = &opt; + } + }; + sort_and_mark(m_options); + sort_and_mark(m_all_modes); +} + +void SettingsIndex::init(std::vector<InputInfo> input_values) +{ + m_options.clear(); + m_all_modes.clear(); + for (auto i : input_values) append_options(i.config, i.type, i.mode); + sort_options(); +} + +bool SettingsIndex::apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode) +{ + // m_all_modes is a separate consumer (the Speed Dial), so "nothing initialised yet" means both + // views are empty - the mode-filtered m_options can be empty while m_all_modes is not. + if (m_options.empty() && m_all_modes.empty()) return false; + + m_options.erase(std::remove_if(m_options.begin(), m_options.end(), [type](Option opt) { return opt.type == type; }), m_options.end()); + m_all_modes.erase(std::remove_if(m_all_modes.begin(), m_all_modes.end(), [type](Option opt) { return opt.type == type; }), m_all_modes.end()); + + append_options(config, type, mode); + + sort_options(); + + return true; +} + +const Option &SettingsIndex::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const +{ + std::string opt_key2 = opt_key; + if (auto n = opt_key.find('#'); n != std::string::npos) { + variant_index = std::atoi(opt_key.c_str() + n + 1); + opt_key2 = opt_key.substr(0, n); + } + auto it = std::lower_bound(m_options.begin(), m_options.end(), Option({boost::nowide::widen(get_key(opt_key2, type))})); + // BBS: return the 0th option when not found in searcher caused by mode difference + // assert(it != options.end()); + if (it == m_options.end()) { variant_index = -2 ; return m_options[0]; } + if (it->opt_key() == opt_key2) { + variant_index = -1; + } else { + const std::string opt_key3 = opt_key2 + "#"; + it = std::lower_bound(it, m_options.end(), Option({boost::nowide::widen(get_key(opt_key3, type))})); + if (it == m_options.end() || it->opt_key().compare(0, opt_key3.length(), opt_key3) != 0) { + variant_index = -2; // Not found + return m_options[0]; + } + auto it2 = it; + ++it2; + if (it2 != m_options.end() && it2->opt_key().compare(0, opt_key3.length(), opt_key3) == 0 + && printer_options_with_variant_1.find(opt_key2) == printer_options_with_variant_1.end()) + variant_index = -2; + } + + return m_options[it - m_options.begin()]; +} + +static Option create_option(const std::string &opt_key, const wxString &label, Preset::Type type, const GroupAndCategory &gc) +{ + return make_option(get_key(opt_key, type), type, label, gc, comSimple, std::string(), true); +} + +Option SettingsIndex::get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const +{ + std::string key = get_key(opt_key, type); + auto it = std::lower_bound(m_options.begin(), m_options.end(), Option({boost::nowide::widen(key)})); + // BBS: return the 0th option when not found in searcher caused by mode difference + if (it == m_options.end()) return m_options[0]; + if (it->key == boost::nowide::widen(key)) return m_options[it - m_options.begin()]; + if (m_groups_and_categories.find(key) == m_groups_and_categories.end()) { + size_t pos = key.find('#'); + if (pos == std::string::npos) return m_options[it - m_options.begin()]; + + std::string zero_opt_key = key.substr(0, pos + 1) + "0"; + + if (m_groups_and_categories.find(zero_opt_key) == m_groups_and_categories.end()) return m_options[it - m_options.begin()]; + + return create_option(opt_key, label, type, m_groups_and_categories.at(zero_opt_key)); + } + + const GroupAndCategory &gc = m_groups_and_categories.at(key); + if (gc.group.IsEmpty() || gc.category.IsEmpty()) return m_options[it - m_options.begin()]; + + return create_option(opt_key, label, type, gc); +} + +void SettingsIndex::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, const wxString &icon) +{ + // Update fields in place so a page rebuild (get_option after set_path) doesn't drop the + // previously recorded wiki path. + GroupAndCategory &gc = m_groups_and_categories[get_key(opt_key, type)]; + gc.group = group; + gc.category = category; + gc.icon = icon; +} + +void SettingsIndex::set_path(const std::string &opt_key, Preset::Type type, const std::string &path) +{ + if (path.empty()) + return; + m_groups_and_categories[get_key(opt_key, type)].path = path; +} + +} // namespace Search +} // namespace Slic3r diff --git a/src/slic3r/GUI/SettingsIndex.hpp b/src/slic3r/GUI/SettingsIndex.hpp new file mode 100644 index 0000000000..5a2f91fa51 --- /dev/null +++ b/src/slic3r/GUI/SettingsIndex.hpp @@ -0,0 +1,98 @@ +#ifndef slic3r_SettingsIndex_hpp_ +#define slic3r_SettingsIndex_hpp_ + +#include <algorithm> +#include <cstddef> +#include <map> +#include <string> +#include <vector> + +#include <wx/string.h> + +#include <libslic3r/Config.hpp> +#include <libslic3r/Preset.hpp> + +namespace Slic3r { +namespace Search { + +struct InputInfo +{ + DynamicPrintConfig *config{nullptr}; + Preset::Type type{Preset::TYPE_INVALID}; + ConfigOptionMode mode{comSimple}; +}; + +struct GroupAndCategory +{ + wxString group; + wxString category; + wxString icon; // icon of the group's own header, or empty + std::string path; // wiki path (Line::label_path) of the option's line, or empty +}; + +struct Option +{ + // bool operator<(const Option& other) const { return other.label > this->label; } + bool operator<(const Option &other) const { return other.key > this->key; } + + // Fuzzy matching works at a character level. Thus matching with wide characters is a safer bet than with short characters, + // though for some languages (Chinese?) it may not work correctly. + std::wstring key; + Preset::Type type{Preset::TYPE_INVALID}; + std::wstring label; + std::wstring label_local; + std::wstring group; + std::wstring group_local; + std::string group_icon; // SVG base name of the group's own header icon, or empty + std::wstring category; + std::wstring category_local; + bool multi_category { false }; + ConfigOptionMode mode{comSimple}; // option's visibility threshold; drives the Speed Dial's mode prompt + std::string tooltip; // localized ConfigOptionDef::tooltip, or empty + std::string wiki_path; // Line::label_path for the option's row, or empty + + std::string opt_key() const; +}; + +// Catalog of settings and their metadata. Owns the group/category registry populated by the +// settings pages, plus two views of the options: the mode-filtered view the sidebar search +// queries, and every option regardless of mode for the Speed Dial. +class SettingsIndex +{ + std::map<std::string, GroupAndCategory> m_groups_and_categories; + + std::vector<Option> m_options; // mode-filtered view used by the sidebar search + std::vector<Option> m_all_modes; // every option regardless of mode, for the Speed Dial + + void append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode); + void sort_options(); + +public: + void init(std::vector<InputInfo> input_values); + // Rebuild the given type's options; returns false when the index was never initialised. + bool apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode); + + void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, + const wxString &icon = wxEmptyString); + void set_path(const std::string &opt_key, Preset::Type type, const std::string &path); + + const std::vector<Option> &options() const { return m_options; } + const std::vector<Option> &all_options() const { return m_all_modes; } + const Option & option_at(size_t pos) const { return m_options[pos]; } + + const Option &get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const; + Option get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const; + + const GroupAndCategory &get_group_and_category(const std::string &opt_key) { return m_groups_and_categories[opt_key]; } + + void sort_options_by_key() + { + std::sort(m_options.begin(), m_options.end(), [](const Option &o1, const Option &o2) { return o1.key < o2.key; }); + } + void sort_options_by_label() { sort_options(); } +}; + +} // namespace Search +} // namespace Slic3r + +#endif // slic3r_SettingsIndex_hpp_ diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index d404283c70..c51ef26a0e 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5049,8 +5049,8 @@ void TabPrinter::build_fff() } // Register by hand so the UnsavedChanges dialog can render a row for it. - wxGetApp().sidebar().get_searcher().add_key("printer_agent", m_type, optgroup->title, - optgroup->config_category(), optgroup->icon); + wxGetApp().sidebar().settings_index().add_key("printer_agent", m_type, optgroup->title, + optgroup->config_category(), optgroup->icon); } } @@ -5761,11 +5761,11 @@ if (is_marlin_flavor) } else if (m_extruders_count_old == 1) { first_extruder_title = wxString::Format("Extruder %d", 1); } - auto & searcher = wxGetApp().sidebar().get_searcher(); + auto & index = wxGetApp().sidebar().settings_index(); for (auto &group : m_pages[n_before_extruders]->m_optgroups) { group->set_config_category_and_type(first_extruder_title, m_type); for (auto &opt : group->opt_map()) - searcher.add_key(opt.first + "#0", m_type, group->title, first_extruder_title, group->icon); + index.add_key(opt.first + "#0", m_type, group->title, first_extruder_title, group->icon); } Thaw(); @@ -7867,10 +7867,10 @@ wxSizer* TabPrinter::create_bed_shape_widget(wxWindow* parent) })); { - Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher(); - const Search::GroupAndCategory& gc = searcher.get_group_and_category("printable_area"); - searcher.add_key("bed_custom_texture", m_type, gc.group, gc.category, gc.icon); - searcher.add_key("bed_custom_model", m_type, gc.group, gc.category, gc.icon); + Search::SettingsIndex& index = wxGetApp().sidebar().settings_index(); + const Search::GroupAndCategory& gc = index.get_group_and_category("printable_area"); + index.add_key("bed_custom_texture", m_type, gc.group, gc.category, gc.icon); + index.add_key("bed_custom_model", m_type, gc.group, gc.category, gc.icon); } return sizer; diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp index 65b678953d..5a182cd959 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.cpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -1485,12 +1485,12 @@ std::string UnsavedChangesDialog::subreplace(std::string resource_str, std::stri void UnsavedChangesDialog::update_tree(Preset::Type type, DynamicConfig * config, int from, int to) { - Search::OptionsSearcher &searcher = wxGetApp().sidebar().get_searcher(); - searcher.sort_options_by_key(); + Search::SettingsIndex &index = wxGetApp().sidebar().settings_index(); + index.sort_options_by_key(); for (const std::string &opt_key : config->keys()) { int variant_index = -2; - const Search::Option &option = searcher.get_option(opt_key, type, variant_index); + const Search::Option &option = index.get_option(opt_key, type, variant_index); auto category = option.category_local; auto opt = dynamic_cast<ConfigOptionVectorBase*>(config->option(opt_key)); std::string value_from = opt->vserialize()[from]; @@ -1502,8 +1502,8 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, DynamicConfig * config void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* presets_) { - Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher(); - searcher.sort_options_by_key(); + Search::SettingsIndex& index = wxGetApp().sidebar().settings_index(); + index.sort_options_by_key(); // list of the presets with unsaved changes std::vector<PresetCollection*> presets_list; @@ -1558,11 +1558,11 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres for (const std::string& opt_key : dirty_options) { int variant_index = -2; - const Search::Option &option = searcher.get_option(opt_key, type, variant_index); + const Search::Option &option = index.get_option(opt_key, type, variant_index); if (option.opt_key() != opt_key && variant_index < -1) { // When founded option isn't the correct one. // It can be for dirty_options: "default_print_profile", "printer_model", "printer_settings_id", - // because of they don't exist in searcher + // because of they don't exist in the index continue; } auto category = option.category_local; @@ -1590,8 +1590,8 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres } } - // Revert sort of searcher back - searcher.sort_options_by_label(); + // Revert sort of index back + index.sort_options_by_label(); } void UnsavedChangesDialog::on_dpi_changed(const wxRect& suggested_rect) @@ -2043,8 +2043,8 @@ void DiffPresetDialog::update_bottom_info(wxString bottom_info) void DiffPresetDialog::update_tree() { - Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher(); - searcher.sort_options_by_key(); + Search::SettingsIndex& index = wxGetApp().sidebar().settings_index(); + index.sort_options_by_key(); m_tree->Clear(); wxString bottom_info = ""; @@ -2124,14 +2124,14 @@ void DiffPresetDialog::update_tree() wxString right_val = get_string_value(opt_key, right_congig); const std::string lookup_key = get_pure_opt_key(opt_key); - Search::Option option = searcher.get_option(lookup_key, get_full_label(lookup_key, left_config), type); + Search::Option option = index.get_option(lookup_key, get_full_label(lookup_key, left_config), type); if (get_pure_opt_key(option.opt_key()) != lookup_key) - option = searcher.get_option(opt_key, get_full_label(opt_key, left_config), type); + option = index.get_option(opt_key, get_full_label(opt_key, left_config), type); if (get_pure_opt_key(option.opt_key()) != lookup_key) { // When the found option is not the requested one. // This can happen for dirty_options such as: // "default_print_profile", "printer_model", "printer_settings_id", - // because they do not exist in the searcher. + // because they do not exist in the index. continue; } m_tree->Append(opt_key, type, option.category_local, option.group_local, option.label_local, @@ -2155,8 +2155,8 @@ void DiffPresetDialog::update_tree() Refresh(); } - // Revert sort of searcher back - searcher.sort_options_by_label(); + // Revert sort of index back + index.sort_options_by_label(); } void DiffPresetDialog::on_dpi_changed(const wxRect&) From 87e278c4b3bbb8cf54f36709d4dfdb39cb92ec0b Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Mon, 14 Sep 2026 12:02:06 +0800 Subject: [PATCH 21/29] Remove unused capture --- src/slic3r/GUI/MainFrame.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 828c698ce1..ee37f3602e 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -739,7 +739,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ } // Defer out of the native key-event stack: open_speed_dial() may create a WebView and // run script, the same window work the codebase avoids doing on native callbacks. - this->CallAfter([this] { wxGetApp().open_speed_dial(); }); + this->CallAfter([] { 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; } From 0131533ae055de1d8d65c28320e220c565fe0588 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Mon, 14 Sep 2026 14:59:06 +0800 Subject: [PATCH 22/29] Get speed dial to work with translations --- localization/i18n/OrcaSlicer.pot | 401 +++++++++++++++++-- localization/i18n/ca/OrcaSlicer_ca.po | 410 +++++++++++++++++-- localization/i18n/cs/OrcaSlicer_cs.po | 410 +++++++++++++++++-- localization/i18n/de/OrcaSlicer_de.po | 408 +++++++++++++++++-- localization/i18n/en/OrcaSlicer_en.po | 401 +++++++++++++++++-- localization/i18n/es/OrcaSlicer_es.po | 407 +++++++++++++++++-- localization/i18n/eu/OrcaSlicer_eu.po | 404 +++++++++++++++++-- localization/i18n/fr/OrcaSlicer_fr.po | 404 +++++++++++++++++-- localization/i18n/hu/OrcaSlicer_hu.po | 408 +++++++++++++++++-- localization/i18n/it/OrcaSlicer_it.po | 408 +++++++++++++++++-- localization/i18n/ja/OrcaSlicer_ja.po | 412 +++++++++++++++++-- localization/i18n/ko/OrcaSlicer_ko.po | 412 +++++++++++++++++-- localization/i18n/list.txt | 3 + localization/i18n/lt/OrcaSlicer_lt.po | 408 +++++++++++++++++-- localization/i18n/nl/OrcaSlicer_nl.po | 414 ++++++++++++++++++-- localization/i18n/pl/OrcaSlicer_pl.po | 412 +++++++++++++++++-- localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 408 +++++++++++++++++-- localization/i18n/ru/OrcaSlicer_ru.po | 404 +++++++++++++++++-- localization/i18n/sv/OrcaSlicer_sv.po | 412 +++++++++++++++++-- localization/i18n/th/OrcaSlicer_th.po | 408 +++++++++++++++++-- localization/i18n/tr/OrcaSlicer_tr.po | 410 +++++++++++++++++-- localization/i18n/uk/OrcaSlicer_uk.po | 406 +++++++++++++++++-- localization/i18n/vi/OrcaSlicer_vi.po | 412 +++++++++++++++++-- localization/i18n/zh_CN/OrcaSlicer_zh_CN.po | 404 +++++++++++++++++-- localization/i18n/zh_TW/OrcaSlicer_zh_TW.po | 408 +++++++++++++++++-- resources/web/data/text.js | 34 -- resources/web/dialog/SpeedDial/index.html | 1 - resources/web/dialog/SpeedDial/speeddial.js | 42 +- src/slic3r/GUI/ActionRegistry.cpp | 21 + src/slic3r/GUI/ActionRegistry.hpp | 6 + src/slic3r/GUI/GUI_App.cpp | 9 + src/slic3r/GUI/NativeCommands.cpp | 33 +- src/slic3r/GUI/NativeCommands.hpp | 6 +- src/slic3r/GUI/SpeedDialDialog.cpp | 59 +++ src/slic3r/GUI/SpeedDialDialog.hpp | 1 + 35 files changed, 9054 insertions(+), 952 deletions(-) diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index 3702bbfcca..7564105304 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -3915,6 +3915,21 @@ msgstr "" msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "" +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "" @@ -5006,8 +5021,10 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "" #, possible-c-format, possible-boost-format -msgid "" -"Is it %s%% or %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "" + +msgid "%" msgstr "" #, possible-boost-format @@ -5362,9 +5379,6 @@ msgctxt "Noun" msgid "Print" msgstr "" -msgid "Printer" -msgstr "" - msgid "Time Estimation" msgstr "" @@ -7037,6 +7051,231 @@ msgstr "" msgid "Please refer to Wiki before use->" msgstr "" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "" + +msgid "Export 3MF" +msgstr "" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +msgid "Delete All Objects" +msgstr "" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +msgid "Split to Objects" +msgstr "" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "" @@ -7826,18 +8065,12 @@ msgstr "" msgid "Delete Object" msgstr "" -msgid "Delete All Objects" -msgstr "" - msgid "Reset Project" msgstr "" msgid "The selected object couldn't be split." msgstr "" -msgid "Split to Objects" -msgstr "" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "" @@ -8808,9 +9041,6 @@ msgstr "" msgid "Dimmed layer brightness" msgstr "" -msgid "%" -msgstr "" - msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" "99% is barely darkened, 0% renders them black. Capped at 99% because 100% would be the same as disabling the option." @@ -9324,6 +9554,126 @@ msgstr "" msgid "Simply switch to \"%1%\"" msgstr "" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "%s actions" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "%s tabs" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "%s matches" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "" @@ -10268,9 +10618,6 @@ msgstr "" msgid "Printable space" msgstr "" -msgid "Printer Agent" -msgstr "" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "" @@ -11951,6 +12298,9 @@ msgstr "" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "" +msgid "Printer Agent" +msgstr "" + msgid "Select the network agent implementation for printer communication." msgstr "" @@ -15597,9 +15947,6 @@ msgstr "" msgid "Slicing Mode" msgstr "" -msgid "Other" -msgstr "" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "" @@ -16430,9 +16777,6 @@ msgstr "" msgid " not in range " msgstr "" -msgid "Export 3MF" -msgstr "" - msgid "This exports the project as a 3MF file." msgstr "" @@ -16448,9 +16792,6 @@ msgstr "" msgid "Load cached slicing data from directory." msgstr "" -msgid "Export STL" -msgstr "" - msgid "Export the objects as single STL." msgstr "" @@ -17100,9 +17441,6 @@ msgstr "" msgid "This OBJ file couldn't be read because it's empty." msgstr "" -msgid "Max Volumetric Speed Calibration" -msgstr "" - msgid "Manage Result" msgstr "" @@ -17849,9 +18187,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "" -msgid "Flow Ratio Calibration" -msgstr "" - msgid "Calibration Test Type" msgstr "" diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po index bb1b5380b8..f91b6ad66e 100644 --- a/localization/i18n/ca/OrcaSlicer_ca.po +++ b/localization/i18n/ca/OrcaSlicer_ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: 2025-03-15 10:55+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -4253,6 +4253,21 @@ msgstr "OrcaSlicer va néixer amb aquest mateix esperit, inspirant-se en PrusaSl msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Avui, OrcaSlicer és el laminador de codi obert més utilitzat i amb un desenvolupament més actiu de la comunitat d'impressió 3D. Moltes de les seves innovacions han estat adoptades per altres laminadors, cosa que el converteix en una força motriu per a tot el sector." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Impressora" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "Configuració de materials AMS" @@ -5440,10 +5455,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "El valor %s està fora de rang. El rang vàlid és de %d a %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"És %s%% or %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "És %s%% or %s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5808,9 +5824,6 @@ msgctxt "Noun" msgid "Print" msgstr "Imprimir" -msgid "Printer" -msgstr "Impressora" - msgid "Time Estimation" msgstr "Estimació temporal" @@ -7551,6 +7564,234 @@ msgstr "No tornis a mostrar aquest diàleg" msgid "Please refer to Wiki before use->" msgstr "Consulteu la Wiki abans d'usar ->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "Exportar STL" + +msgid "Export 3MF" +msgstr "Exportar 3MF" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Calibratge de Velocitat Volumètrica Màxima" + +msgid "Pressure Advance Calibration" +msgstr "" + +# AI Translated +msgid "Flow Ratio Calibration" +msgstr "Calibratge de la relació de flux" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "Suprimir tots els objectes" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "Separar en objectes" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "S'ha desconnectat el ratolí 3D." @@ -8418,10 +8659,6 @@ msgstr "" msgid "Delete Object" msgstr "Suprimir l'objecte" -# AI Translated -msgid "Delete All Objects" -msgstr "Suprimir tots els objectes" - # AI Translated msgid "Reset Project" msgstr "Restablir el projecte" @@ -8429,10 +8666,6 @@ msgstr "Restablir el projecte" msgid "The selected object couldn't be split." msgstr "L'objecte seleccionat no s'ha pogut partir." -# AI Translated -msgid "Split to Objects" -msgstr "Separar en objectes" - # AI Translated msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Voleu desactivar la caiguda automàtica per conservar el posicionament en Z?\n" @@ -9523,9 +9756,6 @@ msgstr "En desplaçar el control lliscant de capes a la previsualització lamina msgid "Dimmed layer brightness" msgstr "Brillantor de les capes enfosquides" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -10081,6 +10311,126 @@ msgstr "Per a \"%1%\", afegir \"%2%\" com a perfil nou" msgid "Simply switch to \"%1%\"" msgstr "Simplement canviar a \"%1%\"" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Altre" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Tasca cancel·lada" @@ -11109,9 +11459,6 @@ msgstr "Perfils de processos compatibles" msgid "Printable space" msgstr "Espai imprimible" -msgid "Printer Agent" -msgstr "Agent de la impressora" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Seleccioneu la implementació de l'agent de xarxa per a la comunicació amb la impressora. Els agents disponibles es registren a l'inici." @@ -12956,6 +13303,9 @@ msgstr "Utilitzar 3MF en lloc de G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Activeu-ho si la impressora accepta un fitxer 3MF com a treball d'impressió. Quan està activat, Orca Slicer envia el fitxer laminat com a .gcode.3mf, en lloc d'un fitxer .gcode simple." +msgid "Printer Agent" +msgstr "Agent de la impressora" + msgid "Select the network agent implementation for printer communication." msgstr "Seleccioneu la implementació de l'agent de xarxa per a la comunicació amb la impressora." @@ -17209,9 +17559,6 @@ msgstr "Les esquerdes de menys de dues vegades el radi de tancament de buits s'o msgid "Slicing Mode" msgstr "Mode de laminat" -msgid "Other" -msgstr "Altre" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "Utilitzeu \"Parell-imparell\" per als models d'avió 3DLabPrint. Utilitzeu \"Tancar forats\" per tancar tots els forats del model." @@ -18157,9 +18504,6 @@ msgstr "amplada de línia massa gran " msgid " not in range " msgstr " fora de rang " -msgid "Export 3MF" -msgstr "Exportar 3MF" - msgid "This exports the project as a 3MF file." msgstr "Exportar projecte a 3MF." @@ -18175,9 +18519,6 @@ msgstr "Carregar dades de laminació" msgid "Load cached slicing data from directory." msgstr "Carregar les dades de laminació a la memòria cau des del directori" -msgid "Export STL" -msgstr "Exportar STL" - msgid "Export the objects as single STL." msgstr "Exportar tots els objectes com a un STL." @@ -18836,9 +19177,6 @@ msgstr "El fitxer conté índex de vèrtex no vàlid." msgid "This OBJ file couldn't be read because it's empty." msgstr "Aquest fitxer OBJ no s'ha pogut llegir perquè està buit." -msgid "Max Volumetric Speed Calibration" -msgstr "Calibratge de Velocitat Volumètrica Màxima" - msgid "Manage Result" msgstr "Gestionar els resultats" @@ -19687,10 +20025,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "NOTA: valors alts poden causar desplaçament de capes (>%s)" -# AI Translated -msgid "Flow Ratio Calibration" -msgstr "Calibratge de la relació de flux" - # AI Translated msgid "Calibration Test Type" msgstr "Tipus de prova de calibratge" diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po index b0f514d13f..fb82edd388 100644 --- a/localization/i18n/cs/OrcaSlicer_cs.po +++ b/localization/i18n/cs/OrcaSlicer_cs.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: \n" "Last-Translator: Jakub Hencl\n" "Language-Team: \n" @@ -4208,6 +4208,21 @@ msgstr "OrcaSlicer vznikl ve stejném duchu a vycházel z projektů PrusaSlicer, msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Dnes je OrcaSlicer nejpoužívanějším a nejaktivněji vyvíjeným open-source slicerem v komunitě 3D tisku. Mnoho jeho inovací převzaly i další slicery, díky čemuž se stal hnací silou celého odvětví." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Tiskárna" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "Nastavení materiálů AMS" @@ -5397,10 +5412,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "Hodnota %s je mimo rozsah. Platný rozsah je od %d do %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"Je to %s%% nebo %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "Je to %s%% nebo %s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5765,9 +5781,6 @@ msgctxt "Noun" msgid "Print" msgstr "Tisk" -msgid "Printer" -msgstr "Tiskárna" - msgid "Time Estimation" msgstr "Odhad času" @@ -7518,6 +7531,233 @@ msgstr "Tento dialog již nezobrazovat" msgid "Please refer to Wiki before use->" msgstr "Před použitím si přečtěte Wiki ->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "Exportovat STL" + +msgid "Export 3MF" +msgstr "Exportovat 3MF" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Kalibrace maximální objemové rychlosti" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "Kalibrace průtokového poměru" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "Odstranit všechny objekty" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "Rozdělit na objekty" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "3D myš odpojena." @@ -8379,10 +8619,6 @@ msgstr "" msgid "Delete Object" msgstr "Odstranit objekt" -# AI Translated -msgid "Delete All Objects" -msgstr "Odstranit všechny objekty" - # AI Translated msgid "Reset Project" msgstr "Resetovat projekt" @@ -8390,10 +8626,6 @@ msgstr "Resetovat projekt" msgid "The selected object couldn't be split." msgstr "Zvolený objekt nelze rozdělit." -# AI Translated -msgid "Split to Objects" -msgstr "Rozdělit na objekty" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Zakázat automatické položení pro zachování pozice v ose Z?\n" @@ -9478,9 +9710,6 @@ msgstr "Při posouvání posuvníku vrstev v náhledu po slicování vykresluje msgid "Dimmed layer brightness" msgstr "Jas ztmavených vrstev" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -10030,6 +10259,126 @@ msgstr "Pro \"%1%\" přidat \"%2%\" jako novou předvolbu" msgid "Simply switch to \"%1%\"" msgstr "Jednoduše přepněte na \"%1%\"" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Ostatní" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Úloha zrušena" @@ -11058,10 +11407,6 @@ msgstr "Kompatibilní procesní profily" msgid "Printable space" msgstr "Tisknutelný prostor" -# AI Translated -msgid "Printer Agent" -msgstr "Agent tiskárny" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Vyberte implementaci síťového agenta pro komunikaci s tiskárnou. Dostupní agenti jsou registrováni při spuštění." @@ -12934,6 +13279,10 @@ msgstr "Použít 3MF místo G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Zapněte, pokud tiskárna přijímá jako tiskovou úlohu soubor 3MF. Je-li zapnuto, odešle Orca Slicer slicovaný soubor jako .gcode.3mf místo prostého souboru .gcode." +# AI Translated +msgid "Printer Agent" +msgstr "Agent tiskárny" + # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Vyberte implementaci síťového agenta pro komunikaci s tiskárnou." @@ -17149,9 +17498,6 @@ msgstr "Trhliny menší než 2x poloměr uzavření mezery jsou při řezání t msgid "Slicing Mode" msgstr "Režim slicingu" -msgid "Other" -msgstr "Ostatní" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "Použijte \"Sudý-lichý\" pro modely letadel 3DLabPrint. Použijte \"Zavřít díry\" pro uzavření všech otvorů v modelu." @@ -18077,9 +18423,6 @@ msgstr "příliš velká šířka čáry " msgid " not in range " msgstr " není v rozsahu " -msgid "Export 3MF" -msgstr "Exportovat 3MF" - msgid "This exports the project as a 3MF file." msgstr "Exportovat projekt jako 3MF." @@ -18095,9 +18438,6 @@ msgstr "Načíst data řezu" msgid "Load cached slicing data from directory." msgstr "Načíst uložená data řezu z adresáře." -msgid "Export STL" -msgstr "Exportovat STL" - msgid "Export the objects as single STL." msgstr "Exportovat objekty jako jeden STL." @@ -18758,9 +19098,6 @@ msgstr "Soubor obsahuje neplatný index vrcholu." msgid "This OBJ file couldn't be read because it's empty." msgstr "Tento soubor OBJ nelze načíst, protože je prázdný." -msgid "Max Volumetric Speed Calibration" -msgstr "Kalibrace maximální objemové rychlosti" - msgid "Manage Result" msgstr "Správa výsledku" @@ -19607,9 +19944,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "POZNÁMKA: Vysoké hodnoty mohou způsobit posun vrstev (>%s)" -msgid "Flow Ratio Calibration" -msgstr "Kalibrace průtokového poměru" - msgid "Calibration Test Type" msgstr "Typ kalibračního testu" diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index 93d122d413..ce1a7e37d4 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: \n" "Last-Translator: Heiko Liebscher <hliebschergmail.com>\n" "Language-Team: \n" @@ -4121,6 +4121,21 @@ msgstr "OrcaSlicer begann in diesem gleichen Geist, indem es von PrusaSlicer, Ba msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Heute ist OrcaSlicer der am weitesten verbreitete und aktiv entwickelte Open-Source-Slicer in der 3D-Druck-Community. Viele seiner Innovationen wurden von anderen Slicern übernommen und treiben die gesamte Industrie voran." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Drucker" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "AMS Material Einstellung" @@ -5302,10 +5317,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "Wert %s ist außerhalb des Bereichs. Der gültige Bereich liegt zwischen %d und %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"Heißt es %s%% oder %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "Heißt es %s%% oder %s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5665,9 +5681,6 @@ msgctxt "Noun" msgid "Print" msgstr "aktuelle Platte drucken" -msgid "Printer" -msgstr "Drucker" - msgid "Time Estimation" msgstr "Geschätzte Zeit" @@ -7395,6 +7408,233 @@ msgstr "Diesen Dialog nicht erneut anzeigen" msgid "Please refer to Wiki before use->" msgstr "Bitte lesen Sie vor der Verwendung das Wiki->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "Exportiere STL" + +msgid "Export 3MF" +msgstr "3mf exportieren" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Maximale volumetrische Geschwindigkeitskalibrierung" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "Flussratenkalibrierung" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "Alle Objekte löschen" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "In Objekte aufteilen" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "3D-Maus nicht angeschlossen." @@ -8249,10 +8489,6 @@ msgstr "" msgid "Delete Object" msgstr "Objekt löschen" -# AI Translated -msgid "Delete All Objects" -msgstr "Alle Objekte löschen" - # AI Translated msgid "Reset Project" msgstr "Projekt zurücksetzen" @@ -8260,10 +8496,6 @@ msgstr "Projekt zurücksetzen" msgid "The selected object couldn't be split." msgstr "Das ausgewählte Objekt konnte nicht geteilt werden." -# AI Translated -msgid "Split to Objects" -msgstr "In Objekte aufteilen" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Auto-Drop deaktivieren, um die Z-Positionierung beizubehalten?\n" @@ -9325,9 +9557,6 @@ msgstr "Beim Bewegen des Schichtreglers in der geslicten Vorschau werden die Sch msgid "Dimmed layer brightness" msgstr "Helligkeit abgedunkelter Schichten" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -9863,6 +10092,126 @@ msgstr "Für \"%1%\", fügen Sie \"%2%\" als neues Profil hinzu" msgid "Simply switch to \"%1%\"" msgstr "Wechseln Sie einfach zu \"%1%\"" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Sonstiges" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Auftrag abgebrochen" @@ -10876,9 +11225,6 @@ msgstr "Kompatible Prozessprofile" msgid "Printable space" msgstr "Druckbarer Raum" -msgid "Printer Agent" -msgstr "Drucker-Agent" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Wählen Sie die Implementierung des Netzwerkagenten für die Druckerkommunikation. Verfügbare Agenten werden beim Start registriert." @@ -12662,6 +13008,9 @@ msgstr "Benutze 3MF statt G-Code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Aktivieren Sie diese Option, wenn der Drucker eine 3MF-Datei als Druckauftrag akzeptiert. Wenn aktiviert, sendet Orca Slicer die geslicete Datei als .gcode.3mf, anstatt als einfache .gcode-Datei." +msgid "Printer Agent" +msgstr "Drucker-Agent" + msgid "Select the network agent implementation for printer communication." msgstr "Wählen Sie die Netzwerk-Agent-Implementierung für die Druckerkommunikation aus." @@ -16807,9 +17156,6 @@ msgstr "Risse, die kleiner als das 2-fache des Lückenschlussradius sind, werden msgid "Slicing Mode" msgstr "Slicing-Modus" -msgid "Other" -msgstr "Sonstiges" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "Verwenden Sie \"Gerade-ungerade\" für 3DLabPrint-Flugzeugmodelle. Verwenden Sie \"Löcher schließen\", um alle Löcher im Modell zu schließen." @@ -17728,9 +18074,6 @@ msgstr "Zu große Linienbreite" msgid " not in range " msgstr "nicht im Bereich" -msgid "Export 3MF" -msgstr "3mf exportieren" - msgid "This exports the project as a 3MF file." msgstr "Projekt als 3MF exportieren." @@ -17746,9 +18089,6 @@ msgstr "Slicing-Daten laden" msgid "Load cached slicing data from directory." msgstr "Zwischengespeicherte Slicing-Daten aus dem Verzeichnis laden" -msgid "Export STL" -msgstr "Exportiere STL" - msgid "Export the objects as single STL." msgstr "Exportieren Sie die Objekte als einzelne STL." @@ -18405,9 +18745,6 @@ msgstr "Die Datei enthält einen ungültigen Scheitelpunktindex." msgid "This OBJ file couldn't be read because it's empty." msgstr "Diese OBJ-Datei konnte nicht gelesen werden, da sie leer ist." -msgid "Max Volumetric Speed Calibration" -msgstr "Maximale volumetrische Geschwindigkeitskalibrierung" - msgid "Manage Result" msgstr "Ergebnis verwalten" @@ -19249,9 +19586,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "Hinweis: Hohe Werte können zu Schichverschiebungen führen (>%s)" -msgid "Flow Ratio Calibration" -msgstr "Flussratenkalibrierung" - msgid "Calibration Test Type" msgstr "Kalibrierungstesttyp" diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po index 9537fbafe9..932fbbe8a1 100644 --- a/localization/i18n/en/OrcaSlicer_en.po +++ b/localization/i18n/en/OrcaSlicer_en.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: \n" @@ -3911,6 +3911,21 @@ msgstr "" msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "" +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "" @@ -5002,8 +5017,10 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "" #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "" + +msgid "%" msgstr "" #, boost-format @@ -5358,9 +5375,6 @@ msgctxt "Noun" msgid "Print" msgstr "" -msgid "Printer" -msgstr "" - msgid "Time Estimation" msgstr "" @@ -7033,6 +7047,231 @@ msgstr "" msgid "Please refer to Wiki before use->" msgstr "" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "" + +msgid "Export 3MF" +msgstr "" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +msgid "Delete All Objects" +msgstr "" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +msgid "Split to Objects" +msgstr "" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "" @@ -7822,18 +8061,12 @@ msgstr "" msgid "Delete Object" msgstr "" -msgid "Delete All Objects" -msgstr "" - msgid "Reset Project" msgstr "" msgid "The selected object couldn't be split." msgstr "" -msgid "Split to Objects" -msgstr "" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "" @@ -8804,9 +9037,6 @@ msgstr "" msgid "Dimmed layer brightness" msgstr "" -msgid "%" -msgstr "" - msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" "99% is barely darkened, 0% renders them black. Capped at 99% because 100% would be the same as disabling the option." @@ -9320,6 +9550,126 @@ msgstr "" msgid "Simply switch to \"%1%\"" msgstr "" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "" @@ -10264,9 +10614,6 @@ msgstr "" msgid "Printable space" msgstr "" -msgid "Printer Agent" -msgstr "" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "" @@ -11947,6 +12294,9 @@ msgstr "" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "" +msgid "Printer Agent" +msgstr "" + msgid "Select the network agent implementation for printer communication." msgstr "" @@ -15593,9 +15943,6 @@ msgstr "" msgid "Slicing Mode" msgstr "" -msgid "Other" -msgstr "" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "" @@ -16426,9 +16773,6 @@ msgstr "" msgid " not in range " msgstr "" -msgid "Export 3MF" -msgstr "" - msgid "This exports the project as a 3MF file." msgstr "" @@ -16444,9 +16788,6 @@ msgstr "" msgid "Load cached slicing data from directory." msgstr "" -msgid "Export STL" -msgstr "" - msgid "Export the objects as single STL." msgstr "" @@ -17096,9 +17437,6 @@ msgstr "" msgid "This OBJ file couldn't be read because it's empty." msgstr "" -msgid "Max Volumetric Speed Calibration" -msgstr "" - msgid "Manage Result" msgstr "" @@ -17845,9 +18183,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "" -msgid "Flow Ratio Calibration" -msgstr "" - msgid "Calibration Test Type" msgstr "" diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po index 83784427a3..888e3065a3 100644 --- a/localization/i18n/es/OrcaSlicer_es.po +++ b/localization/i18n/es/OrcaSlicer_es.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: \n" "Last-Translator: Ian A. Bassi <>\n" "Language-Team: \n" @@ -4000,6 +4000,21 @@ msgstr "OrcaSlicer nació con ese mismo espíritu, inspirándose en PrusaSlicer, msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Hoy en día, OrcaSlicer es el programa de corte de código abierto más utilizado y con mayor desarrollo activo en la comunidad de la impresión 3D. Muchas de sus innovaciones han sido adoptadas por otros programas de corte, lo que lo convierte en un motor impulsor de todo el sector." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Impresora" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "Ajustes de materiales AMS" @@ -5166,10 +5181,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "El valor %s está fuera de rango. El rango válido es de %d a %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"¿Es %s%% o %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "¿Es %s%% o %s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5523,9 +5539,6 @@ msgctxt "Noun" msgid "Print" msgstr "Impresión" -msgid "Printer" -msgstr "Impresora" - msgid "Time Estimation" msgstr "Tiempo Estimado" @@ -7241,6 +7254,231 @@ msgstr "No mostrar este mensaje de nuevo" msgid "Please refer to Wiki before use->" msgstr "Consulte la Wiki antes de usar->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "Exportar STL" + +msgid "Export 3MF" +msgstr "Exportar 3MF" + +msgid "Export Sliced File" +msgstr "Exportar Archivo laminado" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Calibración de Velocidad Volumétrica Máxima" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "Calibración de factor de flujo" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +msgid "Delete All Objects" +msgstr "Borrar todos los objetos" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +msgid "Split to Objects" +msgstr "Separar en objetos" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "Ratón 3D desconectado." @@ -8048,18 +8286,12 @@ msgstr "" msgid "Delete Object" msgstr "Borrar objeto" -msgid "Delete All Objects" -msgstr "Borrar todos los objetos" - msgid "Reset Project" msgstr "Reiniciar proyecto" msgid "The selected object couldn't be split." msgstr "El objeto seleccionado no ha podido ser dividido." -msgid "Split to Objects" -msgstr "Separar en objetos" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "¿Desactivar la caída automática para mantener la posición en el eje Z?\n" @@ -9088,9 +9320,6 @@ msgstr "Al desplazar el control deslizante de capas en la vista previa laminada, msgid "Dimmed layer brightness" msgstr "Brillo de las capas atenuadas" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -9621,6 +9850,126 @@ msgstr "Para \"%1%\", añada \"%2%\" como un nuevo perfil" msgid "Simply switch to \"%1%\"" msgstr "Simplemente cambia a \"%1%\"" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Otro" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Tarea cancelada" @@ -10589,9 +10938,6 @@ msgstr "Perfiles de proceso compatibles" msgid "Printable space" msgstr "Espacio imprimible" -msgid "Printer Agent" -msgstr "Agente de impresora" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Seleccione la implementación del agente de red para la comunicación con la impresora. Los agentes disponibles se registran al iniciar el sistema." @@ -12342,6 +12688,9 @@ msgstr "Utiliza 3MF en lugar de G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Activa esta opción si la impresora admite un archivo 3MF como trabajo de impresión. Cuando está activada, Orca Slicer envía el archivo cortado como un archivo .gcode.3mf, en lugar de como un archivo .gcode convencional." +msgid "Printer Agent" +msgstr "Agente de impresora" + msgid "Select the network agent implementation for printer communication." msgstr "Seleccione la implementación del agente de red para la comunicación con la impresora." @@ -16426,9 +16775,6 @@ msgstr "Las grietas más pequeñas que 2x el radio de cierre se rellenan durante msgid "Slicing Mode" msgstr "Modo de laminado" -msgid "Other" -msgstr "Otro" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "Utilice \"Par-impar\" para los modelos de avión de 3DLabPrint. Utilice \"Cerrar orificios\" para cerrar todos los orificios del modelo." @@ -17330,9 +17676,6 @@ msgstr "ancho de línea excesivo " msgid " not in range " msgstr " fuera de rango " -msgid "Export 3MF" -msgstr "Exportar 3MF" - msgid "This exports the project as a 3MF file." msgstr "Exportar el proyecto como 3MF." @@ -17348,9 +17691,6 @@ msgstr "Cargar datos de laminado" msgid "Load cached slicing data from directory." msgstr "Cargar datos de laminado en caché desde el directorio." -msgid "Export STL" -msgstr "Exportar STL" - msgid "Export the objects as single STL." msgstr "Exportar los objetos como un único STL." @@ -18007,9 +18347,6 @@ msgstr "El archivo contiene un índice de vértices no válido." msgid "This OBJ file couldn't be read because it's empty." msgstr "Este archivo OBJ no se ha podido leer porque está vacío." -msgid "Max Volumetric Speed Calibration" -msgstr "Calibración de Velocidad Volumétrica Máxima" - msgid "Manage Result" msgstr "Administrar Resultados" @@ -18855,9 +19192,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "NOTA: Valores altos pueden causar desplazamiento de capa (>%s)" -msgid "Flow Ratio Calibration" -msgstr "Calibración de factor de flujo" - msgid "Calibration Test Type" msgstr "Tipo de calibración" @@ -24246,9 +24580,6 @@ msgstr "" #~ msgid "Error at line %1%:\n" #~ msgstr "Error en la línea %1%:\n" -#~ msgid "Export Sliced File" -#~ msgstr "Exportar Archivo laminado" - #~ msgid "Export current Sliced file" #~ msgstr "Exportar el archivo laminado actual" diff --git a/localization/i18n/eu/OrcaSlicer_eu.po b/localization/i18n/eu/OrcaSlicer_eu.po index 122a7a90a5..e9aeb9ad71 100644 --- a/localization/i18n/eu/OrcaSlicer_eu.po +++ b/localization/i18n/eu/OrcaSlicer_eu.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: 2026-07-20 13:33+0200\n" "Last-Translator: Manu Goiogana <mgoiogana@gmail.com>\n" "Language-Team: \n" @@ -4039,6 +4039,21 @@ msgstr "OrcaSlicer espiritu berean sortu zen, PrusaSlicer, BambuStudio, SuperSli msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Gaur egun, OrcaSlicer 3D inprimaketaren komunitatean gehien erabiltzen eta aktiboen garatzen den kode irekiko xerratzailea da. Haren berrikuntza asko beste xerratzaile batzuek bereganatu dituzte, eta industria osoaren eragile nagusietako bat bihurtu da." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Inprimagailua" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "AMS Materialen Ezarpena" @@ -5214,10 +5229,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "%s balioa tartetik kanpo dago. Baliozko tartea %d eta %d artekoa da." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"%s%% edo %s %s da?" +msgid "Is it %s%% or %s %s?" +msgstr "%s%% edo %s %s da?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5572,9 +5588,6 @@ msgctxt "Noun" msgid "Print" msgstr "Inprimaketa" -msgid "Printer" -msgstr "Inprimagailua" - msgid "Time Estimation" msgstr "Denbora zenbatespena" @@ -7288,6 +7301,231 @@ msgstr "Ez erakutsi elkarrizketa-koadro hau berriro" msgid "Please refer to Wiki before use->" msgstr "Kontsultatu Wikia erabili aurretik ->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "Esportatu STL" + +msgid "Export 3MF" +msgstr "Esportatu 3MF" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Gehieneko abiadura bolumetrikoaren kalibrazioa" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "Fluxu-erlazioaren kalibrazioa" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +msgid "Delete All Objects" +msgstr "Ezabatu objektu guztiak" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +msgid "Split to Objects" +msgstr "Banatu objektutan" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "3D Sagua deskonektatuta." @@ -8127,18 +8365,12 @@ msgstr "" msgid "Delete Object" msgstr "Ezabatu objektua" -msgid "Delete All Objects" -msgstr "Ezabatu objektu guztiak" - msgid "Reset Project" msgstr "Berrezarri proiektua" msgid "The selected object couldn't be split." msgstr "Hautatutako objektua ezin izan da zatitu." -msgid "Split to Objects" -msgstr "Banatu objektutan" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Auto-Drop desgaitu Z posizioa mantentzeko?\n" @@ -9172,9 +9404,6 @@ msgstr "Xerratutako aurrebistan geruza-graduatzailea mugitzean, unekoaren azpiko msgid "Dimmed layer brightness" msgstr "Ilundutako geruzen distira" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -9707,6 +9936,126 @@ msgstr "\"%1%\"(e)n, gehitu \"%2%\" aurrezarpen berri gisa" msgid "Simply switch to \"%1%\"" msgstr "Aldatu \"%1%\"(e)ra soilik" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Bestelakoak" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Zeregina bertan behera utzi da" @@ -10701,9 +11050,6 @@ msgstr "Prozesu-profil bateragarriak" msgid "Printable space" msgstr "Inprimatzeko espazioa" -msgid "Printer Agent" -msgstr "Inprimagailu-agentea" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Hautatu sare-agentearen inplementazioa inprimagailuarekin komunikatzeko. Erabilgarri dauden agenteak abioan erregistratzen dira." @@ -12473,6 +12819,9 @@ msgstr "Erabili 3MF G-codearen ordez" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Gaitu aukera hau inprimagailuak 3MF fitxategi bat inprimatze-lan gisa onartzen badu. Gaituta dagoenean, OrcaSlicerrek xerratutako fitxategia .gcode.3mf gisa bidaltzen du, .gcode fitxategi arrunt baten ordez." +msgid "Printer Agent" +msgstr "Inprimagailu-agentea" + msgid "Select the network agent implementation for printer communication." msgstr "Hautatu inprimagailuarekin komunikatzeko sare-agentearen inplementazioa." @@ -16596,9 +16945,6 @@ msgstr "Tartea ixteko erradioaren bikoitza baino txikiagoak diren pitzadurak bet msgid "Slicing Mode" msgstr "Xerratze-modua" -msgid "Other" -msgstr "Bestelakoak" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "Erabili \"Bikoiti-bakoiti\" 3DLabPrint hegazkin-modeloetarako. Erabili \"Itxi zuloak\" modeloko zulo guztiak ixteko." @@ -17509,9 +17855,6 @@ msgstr "lerro-zabalera handiegia " msgid " not in range " msgstr " ez dago tartean " -msgid "Export 3MF" -msgstr "Esportatu 3MF" - msgid "This exports the project as a 3MF file." msgstr "Honek proiektua 3MF gisa esportatzen du." @@ -17527,9 +17870,6 @@ msgstr "Kargatu xerratze-datuak" msgid "Load cached slicing data from directory." msgstr "Kargatu cachean gordetako xerratze-datuak direktoriotik." -msgid "Export STL" -msgstr "Esportatu STL" - msgid "Export the objects as single STL." msgstr "Esportatu objektuak STL bakar gisa." @@ -18186,9 +18526,6 @@ msgstr "Fitxategiak erpin-indize baliogabea du." msgid "This OBJ file couldn't be read because it's empty." msgstr "OBJ fitxategi hau ezin izan da irakurri, hutsik dagoelako." -msgid "Max Volumetric Speed Calibration" -msgstr "Gehieneko abiadura bolumetrikoaren kalibrazioa" - msgid "Manage Result" msgstr "Kudeatu emaitza" @@ -19035,9 +19372,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "OHARRA: balio handiek geruza-desplazamendua eragin dezakete (>%s)" -msgid "Flow Ratio Calibration" -msgstr "Fluxu-erlazioaren kalibrazioa" - msgid "Calibration Test Type" msgstr "Kalibrazio-probaren mota" diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index 26b3d76150..0982d62593 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Guislain Cyril, Thomas Lété\n" @@ -4076,6 +4076,21 @@ msgstr "OrcaSlicer est né dans ce même esprit, en s’inspirant de PrusaSlicer msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Aujourd’hui, OrcaSlicer est le slicer open-source le plus utilisé et le plus activement développé de la communauté de l’impression 3D. Beaucoup de ses innovations ont été adoptées par d’autres slicers, ce qui en fait une force motrice pour toute l’industrie." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Imprimante" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "Réglage des matériaux AMS" @@ -5252,10 +5267,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "La valeur %s est hors plage. La plage valide est comprise entre %d et %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"Est-ce %s%% ou %s %s ?" +msgid "Is it %s%% or %s %s?" +msgstr "Est-ce %s%% ou %s %s ?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5615,9 +5631,6 @@ msgctxt "Noun" msgid "Print" msgstr "Imprimer" -msgid "Printer" -msgstr "Imprimante" - msgid "Time Estimation" msgstr "Estimation de temps" @@ -7338,6 +7351,231 @@ msgstr "Ne plus afficher cette boîte de dialogue" msgid "Please refer to Wiki before use->" msgstr "Veuillez consulter le Wiki avant utilisation ->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "Exporter STL" + +msgid "Export 3MF" +msgstr "Exporter 3MF" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Calibration de la vitesse volumétrique maximale" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "Calibration du rapport de débit" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +msgid "Delete All Objects" +msgstr "Supprimer tous les objets" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +msgid "Split to Objects" +msgstr "Scinder en objets" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "Souris 3D déconnectée." @@ -8183,18 +8421,12 @@ msgstr "" msgid "Delete Object" msgstr "Supprimer l’objet" -msgid "Delete All Objects" -msgstr "Supprimer tous les objets" - msgid "Reset Project" msgstr "Réinitialiser le projet" msgid "The selected object couldn't be split." msgstr "L'objet sélectionné n'a pas pu être divisé." -msgid "Split to Objects" -msgstr "Scinder en objets" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Désactiver le dépôt automatique pour préserver le positionnement en Z ?\n" @@ -9241,9 +9473,6 @@ msgstr "Lors du défilement du curseur de couche dans l'aperçu découpé, affic msgid "Dimmed layer brightness" msgstr "Luminosité des couches assombries" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -9777,6 +10006,126 @@ msgstr "Pour \"%1%\", ajoutez \"%2%\" comme nouveau préréglage" msgid "Simply switch to \"%1%\"" msgstr "Passez simplement à \"%1%\"" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Autre" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Tâche annulée" @@ -10790,9 +11139,6 @@ msgstr "Profils de traitement compatibles" msgid "Printable space" msgstr "Espace imprimable" -msgid "Printer Agent" -msgstr "Agent d'imprimante" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Sélectionner l'implémentation de l'agent réseau pour la communication avec l'imprimante. Les agents disponibles sont enregistrés au démarrage." @@ -12567,6 +12913,9 @@ msgstr "Utiliser le 3MF au lieu du G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Activez ceci si l’imprimante accepte un fichier 3MF comme tâche d’impression. Lorsque cette option est activée, Orca Slicer envoie le fichier découpé au format .gcode.3mf au lieu d’un simple fichier .gcode." +msgid "Printer Agent" +msgstr "Agent d'imprimante" + msgid "Select the network agent implementation for printer communication." msgstr "Sélectionner l'implémentation de l'agent réseau pour la communication avec l'imprimante." @@ -16702,9 +17051,6 @@ msgstr "Les fissures plus petites que 2x le rayon de fermeture de l’espace son msgid "Slicing Mode" msgstr "Mode de découpe" -msgid "Other" -msgstr "Autre" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "Utilisez « Pair-impair » pour les modèles d'avion 3DLabPrint. Utilisez « Fermer les trous » pour fermer tous les trous du modèle." @@ -17619,9 +17965,6 @@ msgstr "largeur de ligne trop grande " msgid " not in range " msgstr " hors plage " -msgid "Export 3MF" -msgstr "Exporter 3MF" - msgid "This exports the project as a 3MF file." msgstr "Exporter le projet au format 3MF." @@ -17637,9 +17980,6 @@ msgstr "Charger les données de tranchage" msgid "Load cached slicing data from directory." msgstr "Charger les données de tranchage mises en cache à partir du répertoire" -msgid "Export STL" -msgstr "Exporter STL" - msgid "Export the objects as single STL." msgstr "Exporter les objets en tant que STL unique." @@ -18296,9 +18636,6 @@ msgstr "Le fichier contient un index de sommets non valide." msgid "This OBJ file couldn't be read because it's empty." msgstr "Ce fichier OBJ n'a pas pu être lu car il est vide." -msgid "Max Volumetric Speed Calibration" -msgstr "Calibration de la vitesse volumétrique maximale" - msgid "Manage Result" msgstr "Gérer le résultat" @@ -19143,9 +19480,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "NOTE : Des valeurs élevées peuvent causer un décalage de couche (>%s)" -msgid "Flow Ratio Calibration" -msgstr "Calibration du rapport de débit" - msgid "Calibration Test Type" msgstr "Type de test de calibration" diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po index 49e639927e..dda48aee71 100644 --- a/localization/i18n/hu/OrcaSlicer_hu.po +++ b/localization/i18n/hu/OrcaSlicer_hu.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -4165,6 +4165,21 @@ msgstr "Az OrcaSlicer ugyanebben a szellemben indult, a PrusaSlicer, a BambuStud msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Ma az OrcaSlicer a legszélesebb körben használt és legaktívabban fejlesztett nyílt forráskódú szeletelő a 3D nyomtatási közösségben. Sok újítását más szeletelők is átvették, így az egész iparág hajtóereje." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Nyomtató" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "AMS anyagok beállítása" @@ -5349,10 +5364,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "%s érték tartományon kívül van. Az érvényes tartomány: %d - %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"%s%% vagy %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "%s%% vagy %s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5712,9 +5728,6 @@ msgctxt "Noun" msgid "Print" msgstr "Nyomtatás" -msgid "Printer" -msgstr "Nyomtató" - msgid "Time Estimation" msgstr "Időbecslés" @@ -7449,6 +7462,233 @@ msgstr "Ne jelenjen meg többé ez a párbeszédablak" msgid "Please refer to Wiki before use->" msgstr "Használat előtt nézd meg a Wikit ->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "STL exportálása" + +msgid "Export 3MF" +msgstr "3MF exportálása" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Max. volumetrikus sebesség kalibrálása" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "Anyagáramlás kalibrálás" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "Összes objektum törlése" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "Felosztás objektumokra" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "3D Mouse csatlakoztatva." @@ -8303,10 +8543,6 @@ msgstr "" msgid "Delete Object" msgstr "Objektum törlése" -# AI Translated -msgid "Delete All Objects" -msgstr "Összes objektum törlése" - # AI Translated msgid "Reset Project" msgstr "Projekt visszaállítása" @@ -8314,10 +8550,6 @@ msgstr "Projekt visszaállítása" msgid "The selected object couldn't be split." msgstr "A kijelölt objektumot nem lehet feldarabolni." -# AI Translated -msgid "Split to Objects" -msgstr "Felosztás objektumokra" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Letiltja az Auto-Drop funkciót a Z pozicionálás megőrzéséhez?\n" @@ -9383,9 +9615,6 @@ msgstr "A rétegcsúszka mozgatásakor a szeletelt előnézetben az aktuális r msgid "Dimmed layer brightness" msgstr "Elhalványított rétegek fényereje" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -9937,6 +10166,126 @@ msgstr "Ehhez: \"%1%\", add hozzá \"%2%\"-t új beállításként" msgid "Simply switch to \"%1%\"" msgstr "Csak válts a(z) \"%1%\"-ra" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Egyéb" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Feladat megszakítva" @@ -10962,9 +11311,6 @@ msgstr "Kompatibilis folyamatprofilok" msgid "Printable space" msgstr "Nyomtatási terület" -msgid "Printer Agent" -msgstr "Nyomtatóügynök" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Válaszd ki a nyomtatóval való kommunikációhoz használt hálózati ügynököt. Az elérhető ügynököket indításkor regisztrálja a rendszer." @@ -12774,6 +13120,9 @@ msgstr "3MF használata G-kód helyett" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Kapcsold be, ha a nyomtató 3MF fájlt fogad el nyomtatási feladatként. Bekapcsolva az Orca Slicer a szeletelt fájlt .gcode.3mf formátumban küldi el egyszerű .gcode fájl helyett." +msgid "Printer Agent" +msgstr "Nyomtatóügynök" + msgid "Select the network agent implementation for printer communication." msgstr "Válaszd ki a nyomtató kommunikációjához használt hálózati ügynök implementációját." @@ -16964,9 +17313,6 @@ msgstr "A háromszögháló szeletelésekor kitölti a hézagzárási sugár ké msgid "Slicing Mode" msgstr "Szeletelési mód" -msgid "Other" -msgstr "Egyéb" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "Használd a \"Páros-páratlan\" opciót a 3DLabPrint repülőgépmodellekhez. Használd a \"Hézagok lezárása\" lehetőséget a modell összes házagának lezárásához." @@ -17892,9 +18238,6 @@ msgstr "túl nagy vonalszélesség " msgid " not in range " msgstr " nincs a tartományban " -msgid "Export 3MF" -msgstr "3MF exportálása" - msgid "This exports the project as a 3MF file." msgstr "Projekt exportálása 3MF formátumban." @@ -17910,9 +18253,6 @@ msgstr "Szeletelési adatok betöltése" msgid "Load cached slicing data from directory." msgstr "Gyorsítótárazott szeletelési adatok betöltése mappából" -msgid "Export STL" -msgstr "STL exportálása" - msgid "Export the objects as single STL." msgstr "Az objektumok exportálása egyetlen STL fájlként." @@ -18571,9 +18911,6 @@ msgstr "A fájl érvénytelen csúcsindexet tartalmaz." msgid "This OBJ file couldn't be read because it's empty." msgstr "Ez az OBJ fájl nem olvasható be, mert üres." -msgid "Max Volumetric Speed Calibration" -msgstr "Max. volumetrikus sebesség kalibrálása" - msgid "Manage Result" msgstr "Eredmények kezelése" @@ -19419,9 +19756,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "MEGJEGYZÉS: A magas értékek rétegeltolódást okozhatnak (>%s)" -msgid "Flow Ratio Calibration" -msgstr "Anyagáramlás kalibrálás" - msgid "Calibration Test Type" msgstr "Kalibrációs teszt típusa" diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po index cd3d58fe61..d00e28edc9 100644 --- a/localization/i18n/it/OrcaSlicer_it.po +++ b/localization/i18n/it/OrcaSlicer_it.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -4168,6 +4168,21 @@ msgstr "OrcaSlicer è nato con lo stesso spirito, traendo ispirazione da PrusaSl msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Oggi, OrcaSlicer è il programma di sezionamento a sorgente aperta più utilizzato e attivamente sviluppato nella comunità della stampa 3D. Molte delle sue innovazioni sono state adottate da altri programmi di stampa, rendendolo un punto di riferimento per l'intero settore." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Stampante" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "Impostazione materiali AMS" @@ -5350,10 +5365,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "Il valore %s è fuori intervallo. L'intervallo valido è da %d a %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"È %s%% o %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "È %s%% o %s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5713,9 +5729,6 @@ msgctxt "Noun" msgid "Print" msgstr "Stampa" -msgid "Printer" -msgstr "Stampante" - msgid "Time Estimation" msgstr "Tempo stimato" @@ -7452,6 +7465,233 @@ msgstr "Non mostrare più questa finestra di dialogo" msgid "Please refer to Wiki before use->" msgstr "Fare riferimento alla Wiki prima dell'uso ->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "Esporta STL" + +msgid "Export 3MF" +msgstr "Esporta 3MF" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Calibrazione della velocità volumetrica massima" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "Calibrazione flusso di stampa" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "Elimina tutti gli oggetti" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "Dividi in oggetti" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "Mouse 3D disconnesso." @@ -8303,10 +8543,6 @@ msgstr "" msgid "Delete Object" msgstr "Elimina oggetto" -# AI Translated -msgid "Delete All Objects" -msgstr "Elimina tutti gli oggetti" - # AI Translated msgid "Reset Project" msgstr "Reimposta progetto" @@ -8314,10 +8550,6 @@ msgstr "Reimposta progetto" msgid "The selected object couldn't be split." msgstr "L'oggetto selezionato non può essere diviso." -# AI Translated -msgid "Split to Objects" -msgstr "Dividi in oggetti" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Disabilitare la funzione di Rilascio automatico per preservare il posizionamento sull'asse Z?\n" @@ -9402,9 +9634,6 @@ msgstr "Quando si scorre il cursore degli strati nell'anteprima elaborata, gli s msgid "Dimmed layer brightness" msgstr "Luminosità degli strati attenuati" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -9955,6 +10184,126 @@ msgstr "Per \"%1%\", aggiungere \"%2%\" come nuovo profilo" msgid "Simply switch to \"%1%\"" msgstr "Passa semplicemente a \"%1%\"" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Altro" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Attività annullata" @@ -10976,9 +11325,6 @@ msgstr "Profili di processo compatibili" msgid "Printable space" msgstr "Spazio di stampa" -msgid "Printer Agent" -msgstr "Agente stampante" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Selezionare l'implementazione dell'agente di rete per la comunicazione con la stampante. Gli agenti disponibili vengono registrati all'avvio." @@ -12794,6 +13140,9 @@ msgstr "Usa 3MF invece di G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Abilita questa opzione se la stampante accetta un file 3MF come processo di stampa. Quando è abilitata, Orca Slicer invia il file elaborato come .gcode.3mf, invece di un semplice file .gcode." +msgid "Printer Agent" +msgstr "Agente stampante" + msgid "Select the network agent implementation for printer communication." msgstr "Selezionare l'implementazione dell'agente di rete per la comunicazione con la stampante." @@ -16988,9 +17337,6 @@ msgstr "Le fessure più piccole di 2 volte il raggio di chiusura degli spazi vuo msgid "Slicing Mode" msgstr "Modalità elaborazione" -msgid "Other" -msgstr "Altro" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "Usa \"Pari-dispari\" per modelli di aeroplano 3DLabPrint. Utilizza \"Chiudi fori\" per chiudere tutti i fori del modello." @@ -17915,9 +18261,6 @@ msgstr "larghezza della linea troppo grande " msgid " not in range " msgstr " fuori portata " -msgid "Export 3MF" -msgstr "Esporta 3MF" - msgid "This exports the project as a 3MF file." msgstr "Esporta il progetto come file 3MF." @@ -17933,9 +18276,6 @@ msgstr "Carica dati elaborati" msgid "Load cached slicing data from directory." msgstr "Carica i dati di elaborazione memorizzati nella cache dalla directory." -msgid "Export STL" -msgstr "Esporta STL" - msgid "Export the objects as single STL." msgstr "Esporta gli oggetti in un singolo STL." @@ -18594,9 +18934,6 @@ msgstr "Il file contiene un indice dei vertici non valido." msgid "This OBJ file couldn't be read because it's empty." msgstr "Impossibile leggere il file OBJ perché è vuoto." -msgid "Max Volumetric Speed Calibration" -msgstr "Calibrazione della velocità volumetrica massima" - msgid "Manage Result" msgstr "Gestisci risultato" @@ -19441,9 +19778,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "NOTA: valori elevati possono causare spostamento degli strati (>%s)" -msgid "Flow Ratio Calibration" -msgstr "Calibrazione flusso di stampa" - msgid "Calibration Test Type" msgstr "Tipo test calibrazione" diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po index 11c12281a0..e0a168ddd4 100644 --- a/localization/i18n/ja/OrcaSlicer_ja.po +++ b/localization/i18n/ja/OrcaSlicer_ja.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -4178,6 +4178,21 @@ msgstr "OrcaSlicerも同じ精神のもと、PrusaSlicer、BambuStudio、SuperSl msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "現在、OrcaSlicerは3Dプリントコミュニティで最も広く使われ、最も活発に開発されているオープンソースのスライサーです。その革新の多くは他のスライサーにも採用され、業界全体を牽引する存在となっています。" +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "プリンター" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "AMS素材設定" @@ -5364,10 +5379,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "値%sは範囲外です。有効な範囲は%dから%dです。" #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"%s%% か、それとも %s %sですか?" +msgid "Is it %s%% or %s %s?" +msgstr "%s%% か、それとも %s %sですか?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5729,9 +5745,6 @@ msgctxt "Noun" msgid "Print" msgstr "造形する" -msgid "Printer" -msgstr "プリンター" - msgid "Time Estimation" msgstr "予測時間" @@ -7461,6 +7474,235 @@ msgstr "このダイアログを再度表示しない" msgid "Please refer to Wiki before use->" msgstr "使用前にWikiを参照してください->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +# AI Translated +msgid "Export STL" +msgstr "STLをエクスポート" + +msgid "Export 3MF" +msgstr "3mf をエクスポート" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "最大体積速度キャリブレーション" + +msgid "Pressure Advance Calibration" +msgstr "" + +# AI Translated +msgid "Flow Ratio Calibration" +msgstr "流量比キャリブレーション" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "すべてのオブジェクトを削除" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "オブジェクトに分割" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "3D Mouseが切断されました。" @@ -8320,10 +8562,6 @@ msgstr "" msgid "Delete Object" msgstr "オブジェクトを削除" -# AI Translated -msgid "Delete All Objects" -msgstr "すべてのオブジェクトを削除" - # AI Translated msgid "Reset Project" msgstr "プロジェクトをリセット" @@ -8331,10 +8569,6 @@ msgstr "プロジェクトをリセット" msgid "The selected object couldn't be split." msgstr "選択したオブジェクトを分割できませんでした。" -# AI Translated -msgid "Split to Objects" -msgstr "オブジェクトに分割" - # AI Translated msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Z位置を維持するために自動ドロップを無効にしますか?\n" @@ -9423,9 +9657,6 @@ msgstr "スライスプレビューで積層スライダーを操作する際、 msgid "Dimmed layer brightness" msgstr "暗くした積層の明るさ" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -9978,6 +10209,126 @@ msgstr "\"%1%\"に対して、\"%2%\"を新しいプリセットとして追加 msgid "Simply switch to \"%1%\"" msgstr "\"%1%\"に切り替え" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "その他" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "タスクを取消しました" @@ -10994,9 +11345,6 @@ msgstr "互換性のあるプロセスプロファイル" msgid "Printable space" msgstr "造形可能領域" -msgid "Printer Agent" -msgstr "プリンターエージェント" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "プリンター通信用のネットワークエージェント実装を選択します。使用可能なエージェントは起動時に登録されます。" @@ -12838,6 +13186,9 @@ msgstr "G-codeの代わりに3MFを使用" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "プリンターが印刷ジョブとして3MFファイルを受け付ける場合に有効にします。有効にすると、Orca Slicerはスライス済みファイルを通常の.gcodeファイルではなく.gcode.3mfとして送信します。" +msgid "Printer Agent" +msgstr "プリンターエージェント" + msgid "Select the network agent implementation for printer communication." msgstr "プリンター通信用のネットワークエージェント実装を選択します。" @@ -17334,9 +17685,6 @@ msgstr "三角メッシュのスライス時に、隙間閉じ半径の2倍よ msgid "Slicing Mode" msgstr "スライシングモード" -msgid "Other" -msgstr "その他" - # AI Translated msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "3DLabPrintの飛行機モデルには「偶奇」を使用してください。モデル内のすべての穴を閉じるには「穴を閉じる」を使用してください。" @@ -18314,9 +18662,6 @@ msgstr "線幅が大きすぎます" msgid " not in range " msgstr "範囲外 " -msgid "Export 3MF" -msgstr "3mf をエクスポート" - msgid "This exports the project as a 3MF file." msgstr "プロジェクトを3MF式で出力" @@ -18332,10 +18677,6 @@ msgstr "スライスデータを読込み" msgid "Load cached slicing data from directory." msgstr "スライスデータを読込み" -# AI Translated -msgid "Export STL" -msgstr "STLをエクスポート" - # AI Translated msgid "Export the objects as single STL." msgstr "オブジェクトを単一のSTLとしてエクスポートします。" @@ -19062,9 +19403,6 @@ msgstr "ファイルに無効な頂点インデックスが含まれています msgid "This OBJ file couldn't be read because it's empty." msgstr "このOBJファイルは空なので読み込めませんでした。" -msgid "Max Volumetric Speed Calibration" -msgstr "最大体積速度キャリブレーション" - msgid "Manage Result" msgstr "結果を管理" @@ -19945,10 +20283,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "注意: 値が大きいとレイヤーずれが発生する場合があります (>%s)" -# AI Translated -msgid "Flow Ratio Calibration" -msgstr "流量比キャリブレーション" - # AI Translated msgid "Calibration Test Type" msgstr "キャリブレーションテストのタイプ" diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po index 8864285965..c29a0723f7 100644 --- a/localization/i18n/ko/OrcaSlicer_ko.po +++ b/localization/i18n/ko/OrcaSlicer_ko.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: 2025-06-02 17:12+0900\n" "Last-Translator: crwusiz <crwusiz@gmail.com>\n" "Language-Team: \n" @@ -4185,6 +4185,21 @@ msgstr "OrcaSlicer도 같은 정신으로 시작하여 PrusaSlicer, BambuStudio, msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "오늘날 OrcaSlicer는 3D 프린팅 커뮤니티에서 가장 널리 사용되고 가장 활발하게 개발되는 오픈 소스 슬라이서입니다. 그 혁신 가운데 다수가 다른 슬라이서에도 채택되어 업계 전체를 이끄는 원동력이 되고 있습니다." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "프린터" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "AMS 재료 설정" @@ -5375,10 +5390,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "값 %s이 범위를 벗어났습니다. 유효한 범위는 %d에서 %d까지입니다." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"%s%% 또는 %s %s입니까?" +msgid "Is it %s%% or %s %s?" +msgstr "%s%% 또는 %s %s입니까?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5741,9 +5757,6 @@ msgctxt "Noun" msgid "Print" msgstr "출력" -msgid "Printer" -msgstr "프린터" - msgid "Time Estimation" msgstr "추정 시간" @@ -7473,6 +7486,234 @@ msgstr "이 대화 상자를 다시 표시하지 마세요." msgid "Please refer to Wiki before use->" msgstr "사용하기 전에 Wiki를 참조하십시오->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "STL 내보내기" + +msgid "Export 3MF" +msgstr "3MF 내보내기" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "최대 압출 속도 교정" + +msgid "Pressure Advance Calibration" +msgstr "" + +# AI Translated +msgid "Flow Ratio Calibration" +msgstr "압출량 비율 교정" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "모든 객체 삭제" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "객체로 분할" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "3D 마우스가 분리됨." @@ -8341,10 +8582,6 @@ msgstr "" msgid "Delete Object" msgstr "객체 삭제" -# AI Translated -msgid "Delete All Objects" -msgstr "모든 객체 삭제" - # AI Translated msgid "Reset Project" msgstr "프로젝트 초기화" @@ -8352,10 +8589,6 @@ msgstr "프로젝트 초기화" msgid "The selected object couldn't be split." msgstr "선택한 객체를 분할할 수 없습니다." -# AI Translated -msgid "Split to Objects" -msgstr "객체로 분할" - # AI Translated msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Z 위치를 유지하기 위해 자동 내려놓기를 비활성화하시겠습니까?\n" @@ -9497,9 +9730,6 @@ msgstr "슬라이스된 미리보기에서 레이어 슬라이더를 움직일 msgid "Dimmed layer brightness" msgstr "어둡게 표시된 레이어의 밝기" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -10068,6 +10298,126 @@ msgstr "%1%에 대해, %2%를 새 사전 설정으로 추가합니다" msgid "Simply switch to \"%1%\"" msgstr "\"%1%\"로 단순 전환" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "기타" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "작업이 취소됨" @@ -11103,10 +11453,6 @@ msgstr "호환 프로세스 사전설정" msgid "Printable space" msgstr "출력 가능 공간" -# AI Translated -msgid "Printer Agent" -msgstr "프린터 에이전트" - # AI Translated msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "프린터 통신에 사용할 네트워크 에이전트 구현을 선택합니다. 사용 가능한 에이전트는 시작 시 등록됩니다." @@ -12975,6 +13321,10 @@ msgstr "G-code 대신 3MF 사용" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "프린터가 출력 작업으로 3MF 파일을 허용하는 경우 이 옵션을 활성화하십시오. 활성화하면 Orca Slicer가 슬라이스된 파일을 일반 .gcode 파일 대신 .gcode.3mf로 전송합니다." +# AI Translated +msgid "Printer Agent" +msgstr "프린터 에이전트" + # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "프린터 통신에 사용할 네트워크 에이전트 구현을 선택합니다." @@ -17295,9 +17645,6 @@ msgstr "간격 폐쇄 반경의 2배보다 작은 균열은 삼각형 메시 슬 msgid "Slicing Mode" msgstr "슬라이싱 모드" -msgid "Other" -msgstr "기타" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "3DLabPrint 비행기 모델에는 \"짝수-홀수\"를 사용하세요. \"구멍 닫기\"를 사용하여 모델의 모든 구멍을 닫습니다." @@ -18247,9 +18594,6 @@ msgstr "너무 넓은 선 너비 " msgid " not in range " msgstr " 범위를 벗어남 " -msgid "Export 3MF" -msgstr "3MF 내보내기" - msgid "This exports the project as a 3MF file." msgstr "프로젝트를 3MF로 내보내기." @@ -18265,9 +18609,6 @@ msgstr "슬라이싱 데이터 로드" msgid "Load cached slicing data from directory." msgstr "디렉토리에 캐시된 슬라이싱 데이터 로드" -msgid "Export STL" -msgstr "STL 내보내기" - msgid "Export the objects as single STL." msgstr "객체를 단일 STL로 내보냅니다." @@ -18943,9 +19284,6 @@ msgstr "파일에 잘못된 꼭지점 인덱스가 포함되어 있습니다." msgid "This OBJ file couldn't be read because it's empty." msgstr "이 OBJ 파일은 비어 있어서 읽을 수 없습니다." -msgid "Max Volumetric Speed Calibration" -msgstr "최대 압출 속도 교정" - msgid "Manage Result" msgstr "결과 관리" @@ -19828,10 +20166,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "참고: 값이 크면 레이어 밀림이 발생할 수 있습니다 (>%s)" -# AI Translated -msgid "Flow Ratio Calibration" -msgstr "압출량 비율 교정" - # AI Translated msgid "Calibration Test Type" msgstr "교정 테스트 유형" diff --git a/localization/i18n/list.txt b/localization/i18n/list.txt index 13be57a17e..c91efdbe58 100644 --- a/localization/i18n/list.txt +++ b/localization/i18n/list.txt @@ -125,6 +125,7 @@ src/slic3r/GUI/ThermalPreconditioningDialog.hpp src/slic3r/GUI/Jobs/SLAImportJob.cpp src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp src/slic3r/GUI/AboutDialog.cpp +src/slic3r/GUI/ActionRegistry.cpp src/slic3r/GUI/AMSMaterialsSetting.cpp src/slic3r/GUI/ExtrusionCalibration.cpp src/slic3r/GUI/AmsMappingPopup.cpp @@ -159,6 +160,7 @@ src/slic3r/GUI/SelectMachinePop.cpp src/slic3r/GUI/StatusPanel.cpp src/slic3r/GUI/Monitor.cpp src/slic3r/GUI/MsgDialog.cpp +src/slic3r/GUI/NativeCommands.cpp src/slic3r/GUI/NotificationManager.hpp src/slic3r/GUI/NotificationManager.cpp src/slic3r/GUI/ObjectDataViewModel.cpp @@ -180,6 +182,7 @@ src/slic3r/GUI/PublishSettingsDialog.cpp src/slic3r/GUI/SavePresetDialog.cpp src/slic3r/GUI/Search.cpp src/slic3r/GUI/SettingsIndex.cpp +src/slic3r/GUI/SpeedDialDialog.cpp src/slic3r/GUI/Selection.cpp src/slic3r/GUI/SelectMachine.cpp src/slic3r/GUI/PrePrintChecker.cpp diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po index d052b87a93..0384fde3aa 100644 --- a/localization/i18n/lt/OrcaSlicer_lt.po +++ b/localization/i18n/lt/OrcaSlicer_lt.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: 2026-07-02 14:13+0300\n" "Last-Translator: Gintaras Kučinskas <sharanchius@gmail.com>\n" "Language-Team: \n" @@ -4155,6 +4155,21 @@ msgstr "„OrcaSlicer“ buvo sukurtas vadovaujantis ta pačia dvasia, remiantis msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Šiandien „OrcaSlicer“ yra plačiausiai naudojama ir aktyviausiai tobulinama atvirojo kodo paruošimo spausdinimui (slicer) programa 3D spausdinimo bendruomenėje.. Daugelis jos naujovių buvo perimtos kitų pjaustymo programų, todėl ji tapo visos pramonės varomąja jėga." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Spausdintuvas" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "AMS medžiagų nuostatos" @@ -5336,10 +5351,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "Reikšmė %s yra už ribų. Galimas diapazonas yra nuo %d iki %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"Ar tai %s%% ar %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "Ar tai %s%% ar %s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5699,9 +5715,6 @@ msgctxt "Noun" msgid "Print" msgstr "Spausdinti" -msgid "Printer" -msgstr "Spausdintuvas" - msgid "Time Estimation" msgstr "Laiko įvertis (skaičiavimas)" @@ -7433,6 +7446,233 @@ msgstr "Daugiau nerodyti šio dialogo lango" msgid "Please refer to Wiki before use->" msgstr "Prieš naudodami peržiūrėkite „Wiki“ ->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "Eksportuoti STL" + +msgid "Export 3MF" +msgstr "Eksportuoti 3MF" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Didžiausio tūrinio greičio kalibravimas" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "Srauto santykio kalibravimas" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "Ištrinti visus objektus" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "Skaidyti į objektus" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "3D pelė atjungta." @@ -8295,10 +8535,6 @@ msgstr "" msgid "Delete Object" msgstr "Ištrinti objektą" -# AI Translated -msgid "Delete All Objects" -msgstr "Ištrinti visus objektus" - # AI Translated msgid "Reset Project" msgstr "Atkurti projektą" @@ -8306,10 +8542,6 @@ msgstr "Atkurti projektą" msgid "The selected object couldn't be split." msgstr "Pasirinkto objekto negalima suskaidyti." -# AI Translated -msgid "Split to Objects" -msgstr "Skaidyti į objektus" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Išjungti automatinį nuleidimą, kad būtų išsaugota Z pozicija?\n" @@ -9358,9 +9590,6 @@ msgstr "Slenkant sluoksnių slankiklį pjaustytoje peržiūroje, atvaizduoti že msgid "Dimmed layer brightness" msgstr "Pritemdytų sluoksnių ryškumas" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -9894,6 +10123,126 @@ msgstr "Profilio „%1%“ atveju, pridėti „%2%“ kaip naują profilį" msgid "Simply switch to \"%1%\"" msgstr "Paprasčiausiai persijunkite į \"%1%\"" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Kita" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Užduotis atšaukta" @@ -10912,9 +11261,6 @@ msgstr "Suderinami apdorojimo profiliai" msgid "Printable space" msgstr "Erdvė spausdinimui" -msgid "Printer Agent" -msgstr "Spausdintuvo agentas" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Pasirinkite tinklo agento modulį ryšiui su spausdintuvu palaikyti. Prieinami agentai užregistruojami paleidimo metu." @@ -12700,6 +13046,9 @@ msgstr "Vietoj G-kodo naudoti 3MF" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Įjunkite, jei spausdintuvas spausdinimo užduotims priima 3MF failus. Kai įjungta, „Orca Slicer“ sugeneruotą failą siunčia kaip „.gcode.3mf“, o ne kaip paprastą „.gcode“ failą." +msgid "Printer Agent" +msgstr "Spausdintuvo agentas" + msgid "Select the network agent implementation for printer communication." msgstr "Pasirinkite tinklo agento modulį ryšiui su spausdintuvu palaikyti." @@ -16834,9 +17183,6 @@ msgstr "Plyšiai, mažesni nei 2x tarpo uždarymo spindulys, užpildomi trikampi msgid "Slicing Mode" msgstr "Sluoksniavimo režimas" -msgid "Other" -msgstr "Kita" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "„3DLabPrint“ lėktuvų modeliams naudokite „Lyginis-nelyginis“. Naudokite „Uždaryti kiaurymes“, kad uždarytumėte visas modelio kiaurymes." @@ -17767,9 +18113,6 @@ msgstr "per didelis linijos plotis " msgid " not in range " msgstr " nepatenka į intervalą " -msgid "Export 3MF" -msgstr "Eksportuoti 3MF" - msgid "This exports the project as a 3MF file." msgstr "Tai eksportuoja projektą kaip 3MF failą." @@ -17785,9 +18128,6 @@ msgstr "Įkelti sluoksniavimo duomenis" msgid "Load cached slicing data from directory." msgstr "Įkelti į talpyklą įrašytus sluoksniavimo duomenis iš katalogo." -msgid "Export STL" -msgstr "Eksportuoti STL" - msgid "Export the objects as single STL." msgstr "Eksportuoti visus objektus kaip vieną STL." @@ -18446,9 +18786,6 @@ msgstr "Faile yra neteisingas viršūnių indeksas." msgid "This OBJ file couldn't be read because it's empty." msgstr "Šio OBJ failo nepavyko perskaityti, nes jis tuščias." -msgid "Max Volumetric Speed Calibration" -msgstr "Didžiausio tūrinio greičio kalibravimas" - msgid "Manage Result" msgstr "Tvarkyti rezultatus" @@ -19296,9 +19633,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "PASTABA: didelės reikšmės gali lemti sluoksnių poslinkį (Layer shift) (>%s)" -msgid "Flow Ratio Calibration" -msgstr "Srauto santykio kalibravimas" - msgid "Calibration Test Type" msgstr "Kalibravimo testo tipas" diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po index e6f6388186..d283a3c231 100644 --- a/localization/i18n/nl/OrcaSlicer_nl.po +++ b/localization/i18n/nl/OrcaSlicer_nl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -4536,6 +4536,22 @@ msgstr "OrcaSlicer begon in diezelfde geest en putte uit PrusaSlicer, BambuStudi msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Vandaag de dag is OrcaSlicer de meest gebruikte en actiefst ontwikkelde open-source slicer in de 3D-printgemeenschap. Veel van zijn innovaties zijn overgenomen door andere slicers, waardoor het een drijvende kracht is voor de hele branche." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +# AI Translated +msgid "Printer" +msgstr "Printer" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "AMS Materiaal instellingen" @@ -5842,10 +5858,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "Waarde %s valt buiten het bereik. Het geldige bereik loopt van %d tot %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"Is het %s%% or %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "Is het %s%% or %s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -6232,10 +6249,6 @@ msgctxt "Noun" msgid "Print" msgstr "Print" -# AI Translated -msgid "Printer" -msgstr "Printer" - msgid "Time Estimation" msgstr "Geschatte duur" @@ -8120,6 +8133,234 @@ msgstr "Dit dialoogvenster niet meer tonen" msgid "Please refer to Wiki before use->" msgstr "Raadpleeg de wiki vóór gebruik->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "STL exporteren" + +msgid "Export 3MF" +msgstr "Exporteer 3mf" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Kalibratie van maximale volumetrische snelheid" + +msgid "Pressure Advance Calibration" +msgstr "" + +# AI Translated +msgid "Flow Ratio Calibration" +msgstr "Kalibratie van de flow verhouding" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "Alle objecten verwijderen" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "Splitsen naar objecten" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "3D-muis losgekoppeld." @@ -9052,10 +9293,6 @@ msgstr "" msgid "Delete Object" msgstr "Object verwijderen" -# AI Translated -msgid "Delete All Objects" -msgstr "Alle objecten verwijderen" - # AI Translated msgid "Reset Project" msgstr "Project terugzetten" @@ -9063,10 +9300,6 @@ msgstr "Project terugzetten" msgid "The selected object couldn't be split." msgstr "Het geselecteerde object kan niet opgesplitst worden." -# AI Translated -msgid "Split to Objects" -msgstr "Splitsen naar objecten" - # AI Translated msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Automatisch laten vallen uitschakelen om de Z-positie te behouden?\n" @@ -10249,9 +10482,6 @@ msgstr "Bij het verschuiven van de laagschuifregelaar in de slicevoorvertoning w msgid "Dimmed layer brightness" msgstr "Helderheid van gedimde lagen" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -10829,6 +11059,126 @@ msgstr "Voor \"%1%\", dient \"%2%\" toegevoegd te worden als nieuwe voorinstelli msgid "Simply switch to \"%1%\"" msgstr "Schakel eenvoudig over naar \"%1%\"" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Anders" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Taak geannuleerd" @@ -11931,10 +12281,6 @@ msgstr "Geschikte proces profielen" msgid "Printable space" msgstr "Ruimte waarbinnen geprint kan worden" -# AI Translated -msgid "Printer Agent" -msgstr "Printeragent" - # AI Translated msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Selecteer de implementatie van de netwerkagent voor de communicatie met de printer. Beschikbare agenten worden bij het opstarten geregistreerd." @@ -13926,6 +14272,10 @@ msgstr "3MF gebruiken in plaats van G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Schakel dit in als de printer een 3MF-bestand als printopdracht accepteert. Indien ingeschakeld verzendt Orca Slicer het geslicede bestand als een .gcode.3mf in plaats van als een gewoon .gcode-bestand." +# AI Translated +msgid "Printer Agent" +msgstr "Printeragent" + # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Selecteer de implementatie van de netwerkagent voor de communicatie met de printer." @@ -18625,9 +18975,6 @@ msgstr "Scheuren kleiner dan 2x de sluitradius van de spleet worden opgevuld tij msgid "Slicing Mode" msgstr "Slicing-modus" -msgid "Other" -msgstr "Anders" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "Gebruik „Even-Oneven” voor 3DLabPrint-vliegtuigmodellen. Gebruik „Gaten sluiten” om alle gaten in het model te sluiten." @@ -19643,9 +19990,6 @@ msgstr "te grote lijnbreedte " msgid " not in range " msgstr " niet in bereik " -msgid "Export 3MF" -msgstr "Exporteer 3mf" - msgid "This exports the project as a 3MF file." msgstr "Dit exporteert het project als 3MF." @@ -19661,9 +20005,6 @@ msgstr "Laad slicinggegevens" msgid "Load cached slicing data from directory." msgstr "Laad slicinggegevens in de cache uit de directory" -msgid "Export STL" -msgstr "STL exporteren" - # AI Translated msgid "Export the objects as single STL." msgstr "Exporteer de objecten als één STL." @@ -20478,9 +20819,6 @@ msgstr "Het bestand bevat een ongeldige hoekpuntindex." msgid "This OBJ file couldn't be read because it's empty." msgstr "Dit OBJ-bestand kon niet worden gelezen omdat het leeg is." -msgid "Max Volumetric Speed Calibration" -msgstr "Kalibratie van maximale volumetrische snelheid" - msgid "Manage Result" msgstr "Resultaat beheren" @@ -21389,10 +21727,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "LET OP: hoge waarden kunnen laagverschuiving veroorzaken (>%s)" -# AI Translated -msgid "Flow Ratio Calibration" -msgstr "Kalibratie van de flow verhouding" - # AI Translated msgid "Calibration Test Type" msgstr "Type kalibratietest" diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po index 3d7e64384f..d6728e9c40 100644 --- a/localization/i18n/pl/OrcaSlicer_pl.po +++ b/localization/i18n/pl/OrcaSlicer_pl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer 2.3.0-rc\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: \n" "Last-Translator: Krzysztof Morga <<tlumaczeniebs@gmail.com>>\n" "Language-Team: \n" @@ -4260,6 +4260,21 @@ msgstr "OrcaSlicer powstał w tym samym duchu, czerpiąc z PrusaSlicer, BambuStu msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Dziś OrcaSlicer jest najczęściej używanym i najaktywniej rozwijanym otwartoźródłowym slicerem w społeczności druku 3D. Wiele jego innowacji zostało przejętych przez inne slicery, co czyni go siłą napędową całej branży." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Drukarka" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "Ustawienia filamentów AMS" @@ -5463,10 +5478,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "Wartość %s jest spoza zakresu. Poprawny zakres wynosi od %d do %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"Czy to %s%% czy %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "Czy to %s%% czy %s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5838,9 +5854,6 @@ msgctxt "Noun" msgid "Print" msgstr "Drukuj" -msgid "Printer" -msgstr "Drukarka" - msgid "Time Estimation" msgstr "Szacowany czas" @@ -7621,6 +7634,234 @@ msgstr "Nie pokazuj tego okna dialogowego ponownie" msgid "Please refer to Wiki before use->" msgstr "Przed użyciem zapoznaj się z Wiki->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "Eksportuj STL" + +msgid "Export 3MF" +msgstr "Eksportuj 3MF" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Kalibracja Maks. Prędkości Przepływu" + +msgid "Pressure Advance Calibration" +msgstr "" + +# AI Translated +msgid "Flow Ratio Calibration" +msgstr "Kalibracja współczynnika przepływu" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "Usuń wszystkie obiekty" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "Podziel na obiekty" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "Mysz 3D niepodłączona." @@ -8497,10 +8738,6 @@ msgstr "" msgid "Delete Object" msgstr "Usuń obiekt" -# AI Translated -msgid "Delete All Objects" -msgstr "Usuń wszystkie obiekty" - # AI Translated msgid "Reset Project" msgstr "Zresetuj projekt" @@ -8508,10 +8745,6 @@ msgstr "Zresetuj projekt" msgid "The selected object couldn't be split." msgstr "Nie można podzielić wybranego obiektu." -# AI Translated -msgid "Split to Objects" -msgstr "Podziel na obiekty" - # AI Translated msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Wyłączyć automatyczne opuszczanie, aby zachować pozycję Z?\n" @@ -9652,9 +9885,6 @@ msgstr "Podczas przewijania suwaka warstw w podglądzie po cięciu renderuj wars msgid "Dimmed layer brightness" msgstr "Jasność przyciemnionych warstw" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -10224,6 +10454,126 @@ msgstr "Dla „%1%” dodaj „%2%” jako nowy szablon" msgid "Simply switch to \"%1%\"" msgstr "Po prostu przełącz na „%1%”" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Inne" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Zadanie anulowane" @@ -11275,10 +11625,6 @@ msgstr "Kompatybilne profile procesów" msgid "Printable space" msgstr "Przestrzeń do druku" -# AI Translated -msgid "Printer Agent" -msgstr "Agent drukarki" - # AI Translated msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Wybierz implementację agenta sieciowego do komunikacji z drukarką. Dostępni agenci są rejestrowani przy uruchamianiu." @@ -13144,6 +13490,10 @@ msgstr "Użyj 3MF zamiast G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Włącz tę opcję, jeśli drukarka przyjmuje plik 3MF jako zadanie druku. Po włączeniu Orca Slicer wysyła plik po cięciu jako .gcode.3mf zamiast zwykłego pliku .gcode." +# AI Translated +msgid "Printer Agent" +msgstr "Agent drukarki" + # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Wybierz implementację agenta sieciowego do komunikacji z drukarką." @@ -17472,9 +17822,6 @@ msgstr "Szpary mniejsze niż dwukrotność wartości parametru „promień zamyk msgid "Slicing Mode" msgstr "Tryb cięcia" -msgid "Other" -msgstr "Inne" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "Użyj „Parzysto-nieparzysty” dla modeli samolotów 3DLabPrint. Użyj „Zamknij otwory” do zamknięcia wszystkich otworów w modelu." @@ -18426,9 +18773,6 @@ msgstr "zbyt duża szerokość linii " msgid " not in range " msgstr " nie w zakresie " -msgid "Export 3MF" -msgstr "Eksportuj 3MF" - msgid "This exports the project as a 3MF file." msgstr "Eksportuj projekt jako 3MF." @@ -18444,9 +18788,6 @@ msgstr "Wczytaj dane cięcia" msgid "Load cached slicing data from directory." msgstr "Załaduj buforowane dane slicowania z katalogu" -msgid "Export STL" -msgstr "Eksportuj STL" - msgid "Export the objects as single STL." msgstr "Eksportuj obiekty jako jeden plik STL." @@ -19119,9 +19460,6 @@ msgstr "Plik zawiera nieprawidłowy indeks wierzchołka." msgid "This OBJ file couldn't be read because it's empty." msgstr "Ten plik OBJ nie mógł zostać odczytany, ponieważ jest pusty." -msgid "Max Volumetric Speed Calibration" -msgstr "Kalibracja Maks. Prędkości Przepływu" - msgid "Manage Result" msgstr "Zarządzanie Wynikiem" @@ -20006,10 +20344,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "UWAGA: wysokie wartości mogą powodować przesunięcie warstw (>%s)" -# AI Translated -msgid "Flow Ratio Calibration" -msgstr "Kalibracja współczynnika przepływu" - # AI Translated msgid "Calibration Test Type" msgstr "Typ testu kalibracyjnego" diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index ac4425c723..5285bf2d6e 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: 2026-07-26 11:14-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: Portuguese, Brazilian\n" @@ -4011,6 +4011,21 @@ msgstr "O OrcaSlicer começou com esse mesmo espírito, inspirando-se no PrusaSl msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Atualmente, o OrcaSlicer é o fatiador de código aberto mais utilizado e ativamente desenvolvido na comunidade de impressão 3D. Muitas de suas inovações foram adotadas por outros fatiadores, tornando-o uma força motriz para toda a indústria." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Impressora" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "Configuração de Materiais AMS" @@ -5180,10 +5195,12 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "Valor %s está fora do intervalo. O intervalo válido é de %d para %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"É %s%% ou %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "É %s%% ou %s %s?" + +# AI Translated +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5538,9 +5555,6 @@ msgctxt "Noun" msgid "Print" msgstr "Impressão" -msgid "Printer" -msgstr "Impressora" - msgid "Time Estimation" msgstr "Estimativa de Tempo" @@ -7259,6 +7273,232 @@ msgstr "Não mostrar esse diálogo novamente" msgid "Please refer to Wiki before use->" msgstr "Consulte o Wiki antes de usar->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "Exportar STL" + +msgid "Export 3MF" +msgstr "Exportar 3MF" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Calibração de Velocidade Volumétrica Máxima" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "Calibração de Taxa de Fluxo" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +msgid "Delete All Objects" +msgstr "Excluir Todos os Objetos" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "Dividir em objetos" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "Mouse 3D desconectado." @@ -8082,19 +8322,12 @@ msgstr "" msgid "Delete Object" msgstr "Excluir Objeto" -msgid "Delete All Objects" -msgstr "Excluir Todos os Objetos" - msgid "Reset Project" msgstr "Redefinir Projeto" msgid "The selected object couldn't be split." msgstr "O objeto selecionado não pôde ser dividido." -# AI Translated -msgid "Split to Objects" -msgstr "Dividir em objetos" - # AI Translated msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Desativar a Queda automática para preservar o posicionamento Z?\n" @@ -9143,10 +9376,6 @@ msgstr "Ao mover o controle deslizante de camadas na pré-visualização fatiada msgid "Dimmed layer brightness" msgstr "Brilho das camadas escurecidas" -# AI Translated -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -9680,6 +9909,126 @@ msgstr "Para \"%1%\", adicione \"%2%\" como uma nova predefinição" msgid "Simply switch to \"%1%\"" msgstr "Simplesmente mude para \"%1%\"" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Outro" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Tarefa cancelada" @@ -10656,9 +11005,6 @@ msgstr "Perfis de processo compatíveis" msgid "Printable space" msgstr "Espaço de impressão" -msgid "Printer Agent" -msgstr "Agente de Impressora" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Selecione a implementação do agente de rede para comunicação com a impressora. Os agentes disponíveis são registrados na inicialização." @@ -12418,6 +12764,9 @@ msgstr "Usar 3MF em vez de G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Ative esta opção se a impressora aceitar um arquivo 3MF como trabalho de impressão. Quando ativada, o OrcaSlicer envia o arquivo fatiado como .gcode.3mf, em vez de um arquivo .gcode comum." +msgid "Printer Agent" +msgstr "Agente de Impressora" + msgid "Select the network agent implementation for printer communication." msgstr "Selecione a implementação do agente de rede para comunicação com a impressora." @@ -16495,9 +16844,6 @@ msgstr "Frestas menores que 2x o vão de fatiamento serão preenchidas durante o msgid "Slicing Mode" msgstr "Modo de Fatiamento" -msgid "Other" -msgstr "Outro" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "Usar \"Par-impar\" para modelos de avião 3DLabPrint. Use \"Fechar buracos\" para fechar todos os buracos no modelo." @@ -17393,9 +17739,6 @@ msgstr "largura de linha muito grande " msgid " not in range " msgstr " fora do intervalo " -msgid "Export 3MF" -msgstr "Exportar 3MF" - msgid "This exports the project as a 3MF file." msgstr "Isso exporta o projeto como um arquivo 3MF." @@ -17411,9 +17754,6 @@ msgstr "Carregar dados de fatiamento" msgid "Load cached slicing data from directory." msgstr "Carregar dados de fatiamento em cache do diretório." -msgid "Export STL" -msgstr "Exportar STL" - msgid "Export the objects as single STL." msgstr "Exporte os objetos como STL único." @@ -18069,9 +18409,6 @@ msgstr "O arquivo contém um índice de vértice inválido." msgid "This OBJ file couldn't be read because it's empty." msgstr "Este arquivo OBJ não pôde ser lido porque está vazio." -msgid "Max Volumetric Speed Calibration" -msgstr "Calibração de Velocidade Volumétrica Máxima" - msgid "Manage Result" msgstr "Gerenciar Resultado" @@ -18906,9 +19243,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "NOTA: Valores altos podem causar deslocamento de camada (>%s)" -msgid "Flow Ratio Calibration" -msgstr "Calibração de Taxa de Fluxo" - msgid "Calibration Test Type" msgstr "Tipo de Teste de Calibração" diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po index 2ac4e13e95..e3274d939c 100644 --- a/localization/i18n/ru/OrcaSlicer_ru.po +++ b/localization/i18n/ru/OrcaSlicer_ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer V2.5.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: 2026-02-25 13:38+0300\n" "Last-Translator: Felix14_v2\n" "Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg <andylg@yandex.ru>\n" @@ -4140,6 +4140,21 @@ msgstr "OrcaSlicer начинал свой путь с тем же замысл msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "В наши дни OrcaSlicer – самый прогрессивный и распространённый в сообществе слайсер с открытым исходным кодом. Благодаря множеству инноваций он становится основой других слайсеров и движущей силой всей индустрии 3D-печати." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Принтер" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "Настройка материалов AMS" @@ -5346,10 +5361,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "Значение %s выходит за пределы допустимого диапазона. Допустимый диапазон - от %d до %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"Имелось ввиду %s%% или %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "Имелось ввиду %s%% или %s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5732,9 +5748,6 @@ msgctxt "Noun" msgid "Print" msgstr "Печать" -msgid "Printer" -msgstr "Принтер" - msgid "Time Estimation" msgstr "Оценка времени" @@ -7512,6 +7525,231 @@ msgstr "Не показывать снова" msgid "Please refer to Wiki before use->" msgstr "Перед использованием обратитесь к руководству →" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "Экспорт в STL" + +msgid "Export 3MF" +msgstr "Экспорт в 3MF" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Калибровка макс. объёмного расхода" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "Калибровка коэффициента потока" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +msgid "Delete All Objects" +msgstr "Удалить все модели" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +msgid "Split to Objects" +msgstr "Разделить на модели" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "3D-мышь отключена." @@ -8361,18 +8599,12 @@ msgstr "" msgid "Delete Object" msgstr "Удалить модель" -msgid "Delete All Objects" -msgstr "Удалить все модели" - msgid "Reset Project" msgstr "Сбросить проект" msgid "The selected object couldn't be split." msgstr "Невозможно разделить выбранную модель." -msgid "Split to Objects" -msgstr "Разделить на модели" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Отключить притягивание компонентов к столу для сохранения высоты?\n" @@ -9427,9 +9659,6 @@ msgstr "Затемнять слои, находящиеся ниже текущ msgid "Dimmed layer brightness" msgstr "Яркость затемнённых слоёв" -msgid "%" -msgstr "%" - msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" "99% is barely darkened, 0% renders them black. Capped at 99% because 100% would be the same as disabling the option." @@ -9976,6 +10205,126 @@ msgstr "Для «%1%» добавить «%2%» как новый профиль msgid "Simply switch to \"%1%\"" msgstr "Просто переключиться на «%1%»" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Прочее" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Задание отменено" @@ -10996,9 +11345,6 @@ msgstr "Совместимые настройки" msgid "Printable space" msgstr "Область печати" -msgid "Printer Agent" -msgstr "Сетевой агент" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Реализация сетевого агента для обмена информацией с принтером. Доступные реализации определяются при запуске." @@ -12787,6 +13133,9 @@ msgstr "Сжатие G-кода перед отправкой" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Рекомендуется для принтеров, поддерживающих печать из архивов 3MF. Файлы печати будут отправляться с расширением \".gcode.3mf\"." +msgid "Printer Agent" +msgstr "Сетевой агент" + msgid "Select the network agent implementation for printer communication." msgstr "Реализация сетевого агента для обмена информацией с принтером." @@ -17334,9 +17683,6 @@ msgstr "Часто в импортируемых в программу моде msgid "Slicing Mode" msgstr "Режим нарезки" -msgid "Other" -msgstr "Прочее" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "" "Режим нарезки «Чётный-нечётный» применяется для моделей с намеренно нарушенной целостностью. Например, для моделей самолётов с ресурса 3DLabPrint.\n" @@ -18363,9 +18709,6 @@ msgstr "слишком большая ширина линии " msgid " not in range " msgstr " вне диапазона " -msgid "Export 3MF" -msgstr "Экспорт в 3MF" - msgid "This exports the project as a 3MF file." msgstr "Экспорт проекта в 3MF." @@ -18382,9 +18725,6 @@ msgstr "Загрузка данных о нарезке" msgid "Load cached slicing data from directory." msgstr "Загрузка кэша данных нарезки из папки." -msgid "Export STL" -msgstr "Экспорт в STL" - msgid "Export the objects as single STL." msgstr "Экспорт моделей в единый STL-файл." @@ -19087,9 +19427,6 @@ msgstr "Файл содержит неверное количество верш msgid "This OBJ file couldn't be read because it's empty." msgstr "Этот OBJ файл не может быть прочитан, так как он пуст." -msgid "Max Volumetric Speed Calibration" -msgstr "Калибровка макс. объёмного расхода" - msgid "Manage Result" msgstr "Управление результатами" @@ -19944,9 +20281,6 @@ msgstr "Скорость должна быть в диапазоне от 0 до msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "Примечание: высокие значения могут привести к смещению слоёв (>%s)" -msgid "Flow Ratio Calibration" -msgstr "Калибровка коэффициента потока" - msgid "Calibration Test Type" msgstr "Вариант калибровки" diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po index 53abe120c3..4fee82d858 100644 --- a/localization/i18n/sv/OrcaSlicer_sv.po +++ b/localization/i18n/sv/OrcaSlicer_sv.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -4595,6 +4595,21 @@ msgstr "OrcaSlicer började i samma anda och hämtade från PrusaSlicer, BambuSt msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "I dag är OrcaSlicer det mest använda och mest aktivt utvecklade beredningsprogrammet med öppen källkod i 3D-utskriftscommunityn. Många av dess nyheter har tagits upp av andra program, vilket gör det till en drivkraft för hela branschen." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Skrivare" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "AMS Material Inställning" @@ -5916,10 +5931,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "Värdet %s ligger utanför intervallet. Giltigt intervall är från %d till %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"Det är %s%% eller %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "Det är %s%% eller %s %s?" + +msgid "%" +msgstr "%" # AI Translated #, boost-format @@ -6312,9 +6328,6 @@ msgctxt "Noun" msgid "Print" msgstr "Skriv ut" -msgid "Printer" -msgstr "Skrivare" - msgid "Time Estimation" msgstr "Beräknad tid" @@ -8209,6 +8222,234 @@ msgstr "Visa inte den här dialogrutan igen" msgid "Please refer to Wiki before use->" msgstr "Läs wikin före användning->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "Exportera STL" + +msgid "Export 3MF" +msgstr "Exportera 3mf" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Max volymetrisk hastighets kalibrering" + +msgid "Pressure Advance Calibration" +msgstr "" + +# AI Translated +msgid "Flow Ratio Calibration" +msgstr "Kalibrering av flödesförhållande" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "Radera alla objekt" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "Dela upp i objekt" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "3D mus bortkopplad." @@ -9141,10 +9382,6 @@ msgstr "" msgid "Delete Object" msgstr "Radera objekt" -# AI Translated -msgid "Delete All Objects" -msgstr "Radera alla objekt" - # AI Translated msgid "Reset Project" msgstr "Återställ projekt" @@ -9152,10 +9389,6 @@ msgstr "Återställ projekt" msgid "The selected object couldn't be split." msgstr "Det valda objektet kan inte delas." -# AI Translated -msgid "Split to Objects" -msgstr "Dela upp i objekt" - # AI Translated msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Vill du avaktivera automatiskt nedsläpp för att behålla Z-positionen?\n" @@ -10360,9 +10593,6 @@ msgstr "När du drar i lagerreglaget i den beredda förhandsgranskningen rendera msgid "Dimmed layer brightness" msgstr "Ljusstyrka för dämpade lager" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -10943,6 +11173,126 @@ msgstr "För \"%1%\", lägg till \"%2%\" som ny förinställning" msgid "Simply switch to \"%1%\"" msgstr "Byta till \"%1%\"" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Andra" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Uppgift avbruten" @@ -12089,10 +12439,6 @@ msgstr "Kompatibla process profiler" msgid "Printable space" msgstr "Utskriftsbar yta" -# AI Translated -msgid "Printer Agent" -msgstr "Skrivaragent" - # AI Translated msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Välj vilken nätverksagentimplementation som ska användas för kommunikation med skrivaren. Tillgängliga agenter registreras vid start." @@ -14095,6 +14441,10 @@ msgstr "Använd 3MF i stället för G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Aktivera detta om skrivaren tar emot en 3MF-fil som utskriftsjobb. När det är aktiverat skickar Orca Slicer den beredda filen som en .gcode.3mf i stället för en vanlig .gcode-fil." +# AI Translated +msgid "Printer Agent" +msgstr "Skrivaragent" + # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Välj vilken nätverksagentimplementation som ska användas för kommunikation med skrivaren." @@ -18849,9 +19199,6 @@ msgstr "Sprickor mindre än 2 x gap stängningsradie fylls under triangeln mesh msgid "Slicing Mode" msgstr "Berednings läge" -msgid "Other" -msgstr "Andra" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "Använd ”Jämn-Udda” för 3DLabPrint flygplans modeller. Använd ”Stäng hål” för att stänga alla hål i modellen." @@ -19874,9 +20221,6 @@ msgstr "för stor linjebredd " msgid " not in range " msgstr " inte inom intervallet " -msgid "Export 3MF" -msgstr "Exportera 3mf" - msgid "This exports the project as a 3MF file." msgstr "Exportera projekt som 3MF." @@ -19892,9 +20236,6 @@ msgstr "Ladda berednings data" msgid "Load cached slicing data from directory." msgstr "Ladda cachad berednings data från katalogen" -msgid "Export STL" -msgstr "Exportera STL" - # AI Translated msgid "Export the objects as single STL." msgstr "Exportera objekten som en enda STL." @@ -20699,9 +21040,6 @@ msgstr "Filen innehåller ett ogiltigt vertex index." msgid "This OBJ file couldn't be read because it's empty." msgstr "Denna OBJ fil kunde inte läsas eftersom den är tom." -msgid "Max Volumetric Speed Calibration" -msgstr "Max volymetrisk hastighets kalibrering" - msgid "Manage Result" msgstr "Hantera resultat" @@ -21621,10 +21959,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "OBS! Höga värden kan orsaka lagerförskjutning (>%s)" -# AI Translated -msgid "Flow Ratio Calibration" -msgstr "Kalibrering av flödesförhållande" - # AI Translated msgid "Calibration Test Type" msgstr "Typ av kalibreringstest" diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po index 888b22781a..3657d1ab80 100644 --- a/localization/i18n/th/OrcaSlicer_th.po +++ b/localization/i18n/th/OrcaSlicer_th.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: 2026-06-19 13:40+0700\n" "Last-Translator: Icezaza\n" "Language-Team: Thai\n" @@ -4150,6 +4150,21 @@ msgstr "OrcaSlicer เริ่มต้นด้วยจิตวิญญา msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "ปัจจุบัน OrcaSlicer เป็นตัวแบ่งส่วนข้อมูลแบบโอเพ่นซอร์สที่ใช้กันอย่างแพร่หลายและได้รับการพัฒนาอย่างแข็งขันที่สุดในชุมชนการพิมพ์ 3 มิติ นวัตกรรมหลายอย่างของบริษัทได้ถูกนำไปใช้โดยตัวแบ่งส่วนข้อมูลอื่นๆ ทำให้สิ่งนี้เป็นแรงผลักดันสำหรับอุตสาหกรรมทั้งหมด" +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "เครื่องพิมพ์" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "การตั้งค่าวัสดุ AMS" @@ -5330,10 +5345,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "ค่า %s อยู่นอกช่วง ช่วงที่ถูกต้องคือตั้งแต่ %d ถึง %d" #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"มันคือ %s%% หรือ %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "มันคือ %s%% หรือ %s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5693,9 +5709,6 @@ msgctxt "Noun" msgid "Print" msgstr "พิมพ์" -msgid "Printer" -msgstr "เครื่องพิมพ์" - msgid "Time Estimation" msgstr "การประมาณเวลา" @@ -7416,6 +7429,233 @@ msgstr "อย่าแสดงกล่องโต้ตอบนี้อี msgid "Please refer to Wiki before use->" msgstr "โปรดดู Wiki ก่อนใช้งาน ->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "ส่งออก STL" + +msgid "Export 3MF" +msgstr "ส่งออก 3MF" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "ปรับเทียบความเร็วปริมาตรสูงสุด" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "การปรับเทียบอัตราส่วนการไหล" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "ลบวัตถุทั้งหมด" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "แยกเป็นวัตถุ" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "เมาส์ 3D ถูกตัดการเชื่อมต่อ" @@ -8258,10 +8498,6 @@ msgstr "" msgid "Delete Object" msgstr "ลบวัตถุ" -# AI Translated -msgid "Delete All Objects" -msgstr "ลบวัตถุทั้งหมด" - # AI Translated msgid "Reset Project" msgstr "รีเซ็ตโปรเจกต์" @@ -8269,10 +8505,6 @@ msgstr "รีเซ็ตโปรเจกต์" msgid "The selected object couldn't be split." msgstr "ไม่สามารถแยกวัตถุที่เลือกได้" -# AI Translated -msgid "Split to Objects" -msgstr "แยกเป็นวัตถุ" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "ปิดการใช้งานการวางอัตโนมัติเพื่อรักษาตำแหน่ง z หรือไม่\n" @@ -9329,9 +9561,6 @@ msgstr "เมื่อเลื่อนแถบเลเยอร์ในต msgid "Dimmed layer brightness" msgstr "ความสว่างของเลเยอร์ที่หรี่" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -9868,6 +10097,126 @@ msgstr "สำหรับ \"%1%\" ให้เพิ่ม \"%2%\" เป็น msgid "Simply switch to \"%1%\"" msgstr "เพียงเปลี่ยนเป็น \"%1%\"" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "อื่นๆ" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "ยกเลิกงานแล้ว" @@ -10884,9 +11233,6 @@ msgstr "โปรไฟล์กระบวนการที่เข้าก msgid "Printable space" msgstr "พื้นที่ที่สามารถพิมพ์ได้" -msgid "Printer Agent" -msgstr "ตัวแทนเครื่องพิมพ์" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "เลือกการใช้งานตัวแทนเครือข่ายสำหรับการสื่อสารของเครื่องพิมพ์ ตัวแทนที่มีอยู่จะได้รับการลงทะเบียนเมื่อเริ่มต้น" @@ -12670,6 +13016,9 @@ msgstr "ใช้ 3MF แทน G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "เปิดใช้งานหากเครื่องพิมพ์รับไฟล์ 3MF เป็นงานพิมพ์ เมื่อเปิดใช้งาน OrcaSlicer จะส่งไฟล์ที่สไลซ์แล้วเป็น .gcode.3mf แทนไฟล์ .gcode ธรรมดา" +msgid "Printer Agent" +msgstr "ตัวแทนเครื่องพิมพ์" + msgid "Select the network agent implementation for printer communication." msgstr "เลือกการใช้งานตัวแทนเครือข่ายสำหรับการสื่อสารของเครื่องพิมพ์" @@ -16827,9 +17176,6 @@ msgstr "รอยแตกร้าวที่มีขนาดเล็กก msgid "Slicing Mode" msgstr "โหมดการแบ่งส่วน" -msgid "Other" -msgstr "อื่นๆ" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "ใช้ \"เลขคู่\" สำหรับโมเดลเครื่องบิน 3DLabPrint ใช้ \"ปิดรู\" เพื่อปิดรูทั้งหมดในโมเดล" @@ -17759,9 +18105,6 @@ msgstr "ความกว้างของเส้นใหญ่เกิน msgid " not in range " msgstr "ไม่อยู่ในช่วง" -msgid "Export 3MF" -msgstr "ส่งออก 3MF" - msgid "This exports the project as a 3MF file." msgstr "ส่งออกโครงการเป็น 3MF" @@ -17777,9 +18120,6 @@ msgstr "โหลดข้อมูลการแบ่งส่วน" msgid "Load cached slicing data from directory." msgstr "โหลดข้อมูลการแบ่งส่วนแคชจากไดเรกทอรี" -msgid "Export STL" -msgstr "ส่งออก STL" - msgid "Export the objects as single STL." msgstr "ส่งออกวัตถุเป็น STL เดี่ยว" @@ -18436,9 +18776,6 @@ msgstr "ไฟล์นี้มีดัชนีจุดยอดที่ไ msgid "This OBJ file couldn't be read because it's empty." msgstr "ไฟล์ OBJ นี้ไม่สามารถอ่านได้เนื่องจากไฟล์ว่างเปล่า" -msgid "Max Volumetric Speed Calibration" -msgstr "ปรับเทียบความเร็วปริมาตรสูงสุด" - msgid "Manage Result" msgstr "จัดการผลลัพธ์" @@ -19286,9 +19623,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "หมายเหตุ: ค่าที่สูงอาจทำให้เกิดการเลื่อนเลเยอร์ (>%s)" -msgid "Flow Ratio Calibration" -msgstr "การปรับเทียบอัตราส่วนการไหล" - msgid "Calibration Test Type" msgstr "ประเภทการทดสอบการสอบเทียบ" diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index a2704b0fa4..76290a0261 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: 2026-08-21 23:18+0300\n" "Last-Translator: GlauTech\n" "Language-Team: \n" @@ -4204,6 +4204,21 @@ msgstr "OrcaSlicer da aynı ruhla, PrusaSlicer, BambuStudio, SuperSlicer ve Cura msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Bugün OrcaSlicer, 3D baskı topluluğunda en yaygın kullanılan ve en etkin geliştirilen açık kaynaklı dilimleyicidir. Yeniliklerinin birçoğu diğer dilimleyiciler tarafından da benimsendi ve bu da onu tüm sektör için itici bir güç haline getirdi." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Yazıcı" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "AMS Malzeme Ayarı" @@ -5393,10 +5408,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "Değer %s aralık dışında. Geçerli aralık %d ile %d arasındadır." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"%s%% mi yoksa %s %s mi?" +msgid "Is it %s%% or %s %s?" +msgstr "%s%% mi yoksa %s %s mi?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5759,9 +5775,6 @@ msgctxt "Noun" msgid "Print" msgstr "Yazdır" -msgid "Printer" -msgstr "Yazıcı" - msgid "Time Estimation" msgstr "Zaman Tahmini" @@ -7496,6 +7509,234 @@ msgstr "Bu iletişim kutusunu bir daha gösterme" msgid "Please refer to Wiki before use->" msgstr "Lütfen kullanmadan önce Wiki'ye bakın->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "STL'yi dışa aktar" + +msgid "Export 3MF" +msgstr "3MF'yi dışa aktar" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Maksimum Hacimsel Hız Kalibrasyonu" + +msgid "Pressure Advance Calibration" +msgstr "" + +# AI Translated +msgid "Flow Ratio Calibration" +msgstr "Akış Oranı Kalibrasyonu" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "Tüm Nesneleri Sil" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "Nesnelere Ayır" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "3D Fare bağlantısı kesildi." @@ -8356,10 +8597,6 @@ msgstr "" msgid "Delete Object" msgstr "Nesneyi Sil" -# AI Translated -msgid "Delete All Objects" -msgstr "Tüm Nesneleri Sil" - # AI Translated msgid "Reset Project" msgstr "Projeyi Sıfırla" @@ -8367,10 +8604,6 @@ msgstr "Projeyi Sıfırla" msgid "The selected object couldn't be split." msgstr "Seçilen nesne bölünemedi." -# AI Translated -msgid "Split to Objects" -msgstr "Nesnelere Ayır" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Z konumunu korumak için Otomatik düşürme devre dışı bırakılsın mı?\n" @@ -9457,9 +9690,6 @@ msgstr "Dilimlenmiş önizlemede katman kaydırıcısı gezdirilirken, geçerli msgid "Dimmed layer brightness" msgstr "Karartılmış katman parlaklığı" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -10012,6 +10242,126 @@ msgstr "\"%1%\" için \"%2%\"yi yeni ön ayar olarak ekleyin" msgid "Simply switch to \"%1%\"" msgstr "Kolayca \"%1%\"e geçin" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Diğer" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Görev iptal edildi" @@ -11048,9 +11398,6 @@ msgstr "Uyumlu süreç profilleri" msgid "Printable space" msgstr "Plaka Ayarı" -msgid "Printer Agent" -msgstr "Yazıcı Aracısı" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Yazıcı iletişimi için ağ aracısı uygulamasını seçin. Kullanılabilir aracılar başlangıçta kaydedilir." @@ -12893,6 +13240,9 @@ msgstr "G-code yerine 3MF kullan" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Yazıcı, baskı işi olarak 3MF dosyası kabul ediyorsa bunu etkinleştirin. Etkinleştirildiğinde Orca Slicer, dilimlenmiş dosyayı düz bir .gcode dosyası yerine .gcode.3mf olarak gönderir." +msgid "Printer Agent" +msgstr "Yazıcı Aracısı" + msgid "Select the network agent implementation for printer communication." msgstr "Yazıcı iletişimi için ağ aracısı uygulamasını seçin." @@ -17143,9 +17493,6 @@ msgstr "Üçgen mesh dilimleme sırasında 2x boşluk kapatma yarıçapından k msgid "Slicing Mode" msgstr "Dilimleme modu" -msgid "Other" -msgstr "Diğer" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "3DLabPrint uçak modelleri için \"Çift-tek\" seçeneğini kullanın. Modeldeki tüm delikleri kapatmak için \"Delikleri kapat\"ı kullanın." @@ -18089,9 +18436,6 @@ msgstr "çok büyük çizgi genişliği " msgid " not in range " msgstr " aralıkta değil " -msgid "Export 3MF" -msgstr "3MF'yi dışa aktar" - msgid "This exports the project as a 3MF file." msgstr "Projeyi 3MF olarak dışa aktarın." @@ -18107,9 +18451,6 @@ msgstr "Dilimleme verilerini yükle" msgid "Load cached slicing data from directory." msgstr "Önbelleğe alınmış dilimleme verilerini dizinden yükle." -msgid "Export STL" -msgstr "STL'yi dışa aktar" - msgid "Export the objects as single STL." msgstr "Nesneleri tek STL olarak dışa aktarın." @@ -18768,9 +19109,6 @@ msgstr "Dosya geçersiz köşe dizini içeriyor." msgid "This OBJ file couldn't be read because it's empty." msgstr "Bu OBJ dosyası boş olduğundan okunamadı." -msgid "Max Volumetric Speed Calibration" -msgstr "Maksimum Hacimsel Hız Kalibrasyonu" - msgid "Manage Result" msgstr "Sonucu Yönet" @@ -19624,10 +19962,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "NOT: Yüksek değerler Katman kaymasına neden olabilir (>%s)" -# AI Translated -msgid "Flow Ratio Calibration" -msgstr "Akış Oranı Kalibrasyonu" - # AI Translated msgid "Calibration Test Type" msgstr "Kalibrasyon Testi Tipi" diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po index 584eebdb53..d603945e0d 100644 --- a/localization/i18n/uk/OrcaSlicer_uk.po +++ b/localization/i18n/uk/OrcaSlicer_uk.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: orcaslicerua\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: 2026-07-17 16:25+0300\n" "Last-Translator: Andrij Mizyk <andm1zyk@proton.me>\n" "Language-Team: Ukrainian\n" @@ -4121,6 +4121,21 @@ msgstr "OrcaSlicer народився в тому ж дусі, спираючи msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Сьогодні OrcaSlicer — це найпоширеніший і найактивніше розвинений слайсер з відкритим кодом у спільноті 3D-друку. Багато його нововведень перейняли інші слайсери, що робить його рушійною силою для всієї галузі." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Принтер" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "Налаштування матеріалів AMS" @@ -5341,10 +5356,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "Значення %s знаходиться за межами діапазону. Дійсний діапазон від %d до %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"Це %s%% або %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "Це %s%% або %s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5715,9 +5731,6 @@ msgctxt "Noun" msgid "Print" msgstr "Друк" -msgid "Printer" -msgstr "Принтер" - msgid "Time Estimation" msgstr "Оцінка часу" @@ -7470,6 +7483,232 @@ msgstr "Більше не показувати це діалогове вікн msgid "Please refer to Wiki before use->" msgstr "Зверніться до Вікі перед використанням->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "Експортувати STL" + +msgid "Export 3MF" +msgstr "Експортувати 3MF" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Максимальна калібрування об’ємної швидкості" + +msgid "Pressure Advance Calibration" +msgstr "" + +# AI Translated +msgid "Flow Ratio Calibration" +msgstr "Калібрування коефіцієнта потоку" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +msgid "Delete All Objects" +msgstr "Видалити всі обʼєкти" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +msgid "Split to Objects" +msgstr "Розділити по обʼєктах" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "3D-миша відʼєднана." @@ -8369,18 +8608,12 @@ msgstr "" msgid "Delete Object" msgstr "Видалити обʼєкт" -msgid "Delete All Objects" -msgstr "Видалити всі обʼєкти" - msgid "Reset Project" msgstr "Скинути проєкт" msgid "The selected object couldn't be split." msgstr "Вибраний обʼєкт не може бути поділений." -msgid "Split to Objects" -msgstr "Розділити по обʼєктах" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Вимкнути авто-кидання, щоб зберегти розташування по осі Z?\n" @@ -9470,9 +9703,6 @@ msgstr "Під час прокручування повзунка шарів у msgid "Dimmed layer brightness" msgstr "Яскравість затемнених шарів" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -10016,6 +10246,126 @@ msgstr "Для \"%1%\" додайте \"%2%\" як новий пресет" msgid "Simply switch to \"%1%\"" msgstr "Просто перейдіть на \"%1%\"" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Інший" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Завдання скасовано" @@ -11090,9 +11440,6 @@ msgstr "Сумісні профілі процесів" msgid "Printable space" msgstr "Місце для друку" -msgid "Printer Agent" -msgstr "Агент принтера" - # AI Translated msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Виберіть реалізацію мережевого агента для звʼязку з принтером. Доступні агенти реєструються під час запуску." @@ -12964,6 +13311,9 @@ msgstr "Використовувати 3MF замість G-коду" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Увімкніть, якщо принтер приймає файл 3MF як завдання друку. Якщо увімкнено, Orca Slicer надсилає нарізаний файл як .gcode.3mf замість звичайного файлу .gcode." +msgid "Printer Agent" +msgstr "Агент принтера" + # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Виберіть реалізацію мережевого агента для звʼязку з принтером." @@ -17333,9 +17683,6 @@ msgstr "Під час розрізання тріщини на трикутну msgid "Slicing Mode" msgstr "Режим нарізки" -msgid "Other" -msgstr "Інший" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "Використовуйте «парний-непарний» для моделей літаків 3DLabPrint. Використовуйте «Закрити отвори», щоб закрити всі отвори в моделі." @@ -18298,9 +18645,6 @@ msgstr "надто велика ширина лінії " msgid " not in range " msgstr " не в зоні " -msgid "Export 3MF" -msgstr "Експортувати 3MF" - msgid "This exports the project as a 3MF file." msgstr "Експортувати проєкт як 3MF." @@ -18316,9 +18660,6 @@ msgstr "Завантажити дані про нарізку" msgid "Load cached slicing data from directory." msgstr "Завантажити кешовані дані нарізки з каталогу" -msgid "Export STL" -msgstr "Експортувати STL" - msgid "Export the objects as single STL." msgstr "Експортувати обʼєкти як єдиний STL." @@ -18990,9 +19331,6 @@ msgstr "У файлі містяться недійсний індекс вер msgid "This OBJ file couldn't be read because it's empty." msgstr "Цей файл формату OBJ не може бути прочитаний через те, що він порожній." -msgid "Max Volumetric Speed Calibration" -msgstr "Максимальна калібрування об’ємної швидкості" - msgid "Manage Result" msgstr "Керування результатом" @@ -19878,10 +20216,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "ПРИМІТКА: високі значення можуть спричинити зсув шарів (>%s)" -# AI Translated -msgid "Flow Ratio Calibration" -msgstr "Калібрування коефіцієнта потоку" - msgid "Calibration Test Type" msgstr "Тип тесту калібрування" diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po index 5b52863751..8e884a71af 100644 --- a/localization/i18n/vi/OrcaSlicer_vi.po +++ b/localization/i18n/vi/OrcaSlicer_vi.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: 2025-10-02 17:43+0700\n" "Last-Translator: \n" "Language-Team: hainguyen.ts13@gmail.com\n" @@ -4375,6 +4375,21 @@ msgstr "OrcaSlicer khởi đầu với cùng tinh thần đó, kế thừa từ msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "Ngày nay, OrcaSlicer là phần mềm slice mã nguồn mở được sử dụng rộng rãi nhất và phát triển tích cực nhất trong cộng đồng in 3D. Nhiều đổi mới của nó đã được các phần mềm slice khác áp dụng, khiến nó trở thành động lực thúc đẩy cả ngành." +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "Máy in" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "Cài đặt vật liệu AMS" @@ -5646,10 +5661,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "Giá trị %s nằm ngoài phạm vi. Phạm vi hợp lệ từ %d đến %d." #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"Là %s%% hay %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "Là %s%% hay %s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -6037,9 +6053,6 @@ msgctxt "Noun" msgid "Print" msgstr "In" -msgid "Printer" -msgstr "Máy in" - msgid "Time Estimation" msgstr "Ước tính thời gian" @@ -7868,6 +7881,234 @@ msgstr "Không hiển thị hộp thoại này nữa" msgid "Please refer to Wiki before use->" msgstr "Vui lòng tham khảo Wiki trước khi dùng->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "Xuất STL" + +msgid "Export 3MF" +msgstr "Xuất 3MF" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "Hiệu chỉnh tốc độ thể tích tối đa" + +msgid "Pressure Advance Calibration" +msgstr "" + +# AI Translated +msgid "Flow Ratio Calibration" +msgstr "Hiệu chỉnh tỷ lệ lưu lượng" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "Xóa tất cả vật thể" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "Tách thành các vật thể" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "Chuột 3D đã ngắt kết nối." @@ -8774,10 +9015,6 @@ msgstr "" msgid "Delete Object" msgstr "Xóa vật thể" -# AI Translated -msgid "Delete All Objects" -msgstr "Xóa tất cả vật thể" - # AI Translated msgid "Reset Project" msgstr "Đặt lại dự án" @@ -8785,10 +9022,6 @@ msgstr "Đặt lại dự án" msgid "The selected object couldn't be split." msgstr "Đối tượng đã chọn không thể được tách." -# AI Translated -msgid "Split to Objects" -msgstr "Tách thành các vật thể" - # AI Translated msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "Tắt Tự động thả để giữ nguyên vị trí Z?\n" @@ -9952,9 +10185,6 @@ msgstr "Khi kéo thanh trượt lớp trong bản xem trước đã slice, kết msgid "Dimmed layer brightness" msgstr "Độ sáng của lớp bị làm mờ" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -10529,6 +10759,126 @@ msgstr "Cho \"%1%\", thêm \"%2%\" như một preset mới" msgid "Simply switch to \"%1%\"" msgstr "Đơn giản chuyển sang \"%1%\"" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "Khác" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "Tác vụ đã hủy" @@ -11608,10 +11958,6 @@ msgstr "Hồ sơ quy trình tương thích" msgid "Printable space" msgstr "Không gian in" -# AI Translated -msgid "Printer Agent" -msgstr "Tác nhân máy in" - # AI Translated msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "Chọn cách triển khai tác nhân mạng cho việc giao tiếp với máy in. Các tác nhân khả dụng được đăng ký khi khởi động." @@ -13535,6 +13881,10 @@ msgstr "Dùng 3MF thay cho G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Bật tùy chọn này nếu máy in nhận file 3MF làm tác vụ in. Khi bật, Orca Slicer sẽ gửi file đã slice dưới dạng .gcode.3mf thay vì file .gcode thuần." +# AI Translated +msgid "Printer Agent" +msgstr "Tác nhân máy in" + # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Chọn cách triển khai tác nhân mạng cho việc giao tiếp với máy in." @@ -17864,9 +18214,6 @@ msgstr "Vết nứt nhỏ hơn 2x bán kính đóng khe được lấp trong sli msgid "Slicing Mode" msgstr "Chế độ slice" -msgid "Other" -msgstr "Khác" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "Sử dụng \"Chẵn-lẻ\" cho model máy bay 3DLabPrint. Sử dụng \"Đóng lỗ\" để đóng tất cả các lỗ trong model." @@ -18827,9 +19174,6 @@ msgstr "độ rộng đường quá lớn " msgid " not in range " msgstr " không trong phạm vi " -msgid "Export 3MF" -msgstr "Xuất 3MF" - msgid "This exports the project as a 3MF file." msgstr "Xuất dự án dưới dạng 3MF." @@ -18845,9 +19189,6 @@ msgstr "Nạp dữ liệu slice" msgid "Load cached slicing data from directory." msgstr "Nạp dữ liệu slice đã lưu từ thư mục." -msgid "Export STL" -msgstr "Xuất STL" - msgid "Export the objects as single STL." msgstr "Xuất các đối tượng dưới dạng STL đơn." @@ -19523,9 +19864,6 @@ msgstr "File chứa chỉ số đỉnh không hợp lệ." msgid "This OBJ file couldn't be read because it's empty." msgstr "File OBJ này không thể đọc được vì nó trống." -msgid "Max Volumetric Speed Calibration" -msgstr "Hiệu chỉnh tốc độ thể tích tối đa" - msgid "Manage Result" msgstr "Quản lý kết quả" @@ -20409,10 +20747,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "LƯU Ý: Giá trị cao có thể gây dịch lớp (>%s)" -# AI Translated -msgid "Flow Ratio Calibration" -msgstr "Hiệu chỉnh tỷ lệ lưu lượng" - # AI Translated msgid "Calibration Test Type" msgstr "Loại bài kiểm tra hiệu chỉnh" diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po index caceab04a5..a59dda34ba 100644 --- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po +++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Slic3rPE\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: 2026-06-11 12:37-0300\n" "Last-Translator: Handle <mail@bysb.net>\n" "Language-Team: \n" @@ -4011,6 +4011,21 @@ msgstr "OrcaSlicer 也始于同样的精神,汲取了 PrusaSlicer、BambuStudi msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "如今,OrcaSlicer 是 3D 打印社区中使用最广泛、开发最活跃的开源切片软件。它的许多创新已被其他切片软件采用,成为推动整个行业发展的力量。" +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "打印机" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "AMS 材料设置" @@ -5186,10 +5201,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "值 %s 超出了范围,有效的范围是从 %d 到 %d 。" #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"%s%%还是%s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "%s%%还是%s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5549,9 +5565,6 @@ msgctxt "Noun" msgid "Print" msgstr "打印" -msgid "Printer" -msgstr "打印机" - msgid "Time Estimation" msgstr "时间预估" @@ -7270,6 +7283,231 @@ msgstr "不再显示此对话框?" msgid "Please refer to Wiki before use->" msgstr "使用前请参考 Wiki->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "导出STL文件" + +msgid "Export 3MF" +msgstr "导出 3MF" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "最大体积流量校准" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "流量比校准" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +msgid "Delete All Objects" +msgstr "删除所有对象" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +msgid "Split to Objects" +msgstr "拆分为对象" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "3D鼠标断连。" @@ -8091,18 +8329,12 @@ msgstr "" msgid "Delete Object" msgstr "删除对象" -msgid "Delete All Objects" -msgstr "删除所有对象" - msgid "Reset Project" msgstr "重置项目" msgid "The selected object couldn't be split." msgstr "选中的模型不可分裂。" -msgid "Split to Objects" -msgstr "拆分为对象" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "禁用自动落板以保留 Z 轴定位?\n" @@ -9151,9 +9383,6 @@ msgstr "在切片预览中拖动图层滑块时,将当前图层下方的图层 msgid "Dimmed layer brightness" msgstr "调暗图层的亮度" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -9690,6 +9919,126 @@ msgstr "为“%1%”,添加“%2%”为一个新预设" msgid "Simply switch to \"%1%\"" msgstr "直接切换到“%1%”" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "其他" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "任务已取消" @@ -10673,9 +11022,6 @@ msgstr "兼容的切片配置" msgid "Printable space" msgstr "可打印区域" -msgid "Printer Agent" -msgstr "打印机代理" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "为打印机通信选择网络代理。可用的代理将在启动时列出。" @@ -12466,6 +12812,9 @@ msgstr "使用 3MF 代替 G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "如果打印机接受 3MF 文件作为打印任务,请启用此选项。启用后,Orca Slicer 将以 .gcode.3mf 格式发送切片文件,而不是普通的 .gcode 文件。" +msgid "Printer Agent" +msgstr "打印机代理" + msgid "Select the network agent implementation for printer communication." msgstr "选择打印机通信的网络代理实施。" @@ -16585,9 +16934,6 @@ msgstr "在三角形网格切片过程中,小于2倍间隙闭合半径的裂 msgid "Slicing Mode" msgstr "切片模式" -msgid "Other" -msgstr "其他" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "对3DLabPrint的飞机模型使用 \"奇偶\"。使用 \"闭孔 \"来关闭模型上的所有孔。" @@ -17509,9 +17855,6 @@ msgstr "线宽过大" msgid " not in range " msgstr " 不在合理范围内" -msgid "Export 3MF" -msgstr "导出 3MF" - msgid "This exports the project as a 3MF file." msgstr "导出项目为 3MF。" @@ -17527,9 +17870,6 @@ msgstr "导入切片数据" msgid "Load cached slicing data from directory." msgstr "从目录导入缓存的切片数据" -msgid "Export STL" -msgstr "导出STL文件" - msgid "Export the objects as single STL." msgstr "将对象导出为单个STL。" @@ -18188,9 +18528,6 @@ msgstr "该文件包含无效的顶点索引。" msgid "This OBJ file couldn't be read because it's empty." msgstr "无法读取该OBJ文件,因为该文件内容为空。" -msgid "Max Volumetric Speed Calibration" -msgstr "最大体积流量校准" - msgid "Manage Result" msgstr "管理结果" @@ -19036,9 +19373,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "注意:较高的值可能会导致层移位 (>%s)" -msgid "Flow Ratio Calibration" -msgstr "流量比校准" - msgid "Calibration Test Type" msgstr "校准测试类型" diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po index 3cbf960b59..e891121bc1 100644 --- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po +++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-11 17:15+0800\n" +"POT-Creation-Date: 2026-09-14 12:09+0800\n" "PO-Revision-Date: 2025-11-28 13:48-0600\n" "Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n" "Language-Team: \n" @@ -4113,6 +4113,21 @@ msgstr "OrcaSlicer 也源於同樣的精神,汲取了 PrusaSlicer、BambuStudi msgid "Today, OrcaSlicer is the most widely used and actively developed open-source slicer in the 3D printing community. Many of its innovations have been adopted by other slicers, making it a driving force for the entire industry." msgstr "如今,OrcaSlicer 是 3D 列印社群中使用最廣泛、開發最活躍的開源切片軟體。它的許多創新已被其他切片軟體採用,使其成為推動整個產業的動力。" +msgid "Script plugin skipped." +msgstr "" + +msgid "Script plugin finished." +msgstr "" + +msgid "Printer" +msgstr "列印裝置" + +msgid "Recent Projects" +msgstr "" + +msgid "Go to Plate" +msgstr "" + msgid "AMS Materials Setting" msgstr "AMS 線材設定" @@ -5315,10 +5330,11 @@ msgid "Value %s is out of range. The valid range is from %d to %d." msgstr "數值 %s 超出範圍。有效範圍是從 %d 到 %d。" #, c-format, boost-format -msgid "" -"Is it %s%% or %s %s?" -msgstr "" -"是 %s%% 還是 %s %s?" +msgid "Is it %s%% or %s %s?" +msgstr "是 %s%% 還是 %s %s?" + +msgid "%" +msgstr "%" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -5678,9 +5694,6 @@ msgctxt "Noun" msgid "Print" msgstr "列印" -msgid "Printer" -msgstr "列印裝置" - msgid "Time Estimation" msgstr "時間預估" @@ -7407,6 +7420,233 @@ msgstr "不要再顯示此提示" msgid "Please refer to Wiki before use->" msgstr "使用前請參考 Wiki ->" +msgid "Plates are a filament (FFF) feature." +msgstr "" + +msgid "Open the 3D view first." +msgstr "" + +msgid "Select an object first." +msgstr "" + +msgid "Slice and Preview" +msgstr "" + +msgid "Slice & Export" +msgstr "" + +msgid "Go to layer (percent)" +msgstr "" + +msgid "Commands" +msgstr "" + +msgid "Go to tab..." +msgstr "" + +msgid "Load Project" +msgstr "" + +msgid "Save Project As" +msgstr "" + +msgid "Mode: Simple" +msgstr "" + +msgid "Mode: Advanced" +msgstr "" + +msgid "Mode: Expert" +msgstr "" + +msgid "Toggle Developer Mode" +msgstr "" + +msgid "Developer mode enabled." +msgstr "" + +msgid "Developer mode disabled." +msgstr "" + +msgid "Export STL" +msgstr "匯出為 STL 檔案" + +msgid "Export 3MF" +msgstr "匯出為 3MF 檔案" + +msgid "Export Sliced File" +msgstr "" + +msgid "Export All Sliced Files" +msgstr "" + +msgid "Max Volumetric Speed Calibration" +msgstr "最大體積流量校正" + +msgid "Pressure Advance Calibration" +msgstr "" + +msgid "Flow Ratio Calibration" +msgstr "流量比例校正" + +msgid "Retraction Calibration" +msgstr "" + +msgid "Cornering Calibration" +msgstr "" + +msgid "Input Shaping Frequency Calibration" +msgstr "" + +msgid "Input Shaping Damping Calibration" +msgstr "" + +msgid "VFA Calibration" +msgstr "" + +msgid "View: Top" +msgstr "" + +msgid "View: Bottom" +msgstr "" + +msgid "View: Front" +msgstr "" + +msgid "View: Rear" +msgstr "" + +msgid "View: Left" +msgstr "" + +msgid "View: Right" +msgstr "" + +msgid "View: Isometric" +msgstr "" + +msgid "View: Default" +msgstr "" + +msgid "Fit Bed to View" +msgstr "" + +msgid "Toggle Perspective" +msgstr "" + +# AI Translated +msgid "Delete All Objects" +msgstr "刪除所有物件" + +msgid "Mirror X" +msgstr "" + +msgid "Mirror Y" +msgstr "" + +msgid "Mirror Z" +msgstr "" + +# AI Translated +msgid "Split to Objects" +msgstr "分割為物件" + +msgid "Split to Parts" +msgstr "" + +msgid "Center Selected on Plate" +msgstr "" + +msgid "Drop to Bed" +msgstr "" + +msgid "Scale to Fit Print Volume" +msgstr "" + +msgid "Increase Instances" +msgstr "" + +msgid "Decrease Instances" +msgstr "" + +msgid "Auto-Arrange" +msgstr "" + +msgid "Auto-Orient" +msgstr "" + +msgid "Add Plate" +msgstr "" + +msgid "Cannot add another plate (maximum reached)." +msgstr "" + +msgid "Duplicate Plate" +msgstr "" + +msgid "Cannot duplicate a plate (maximum reached)." +msgstr "" + +msgid "Cannot delete the only plate." +msgstr "" + +msgid "Rename Plate" +msgstr "" + +msgid "Toggle Plate Lock" +msgstr "" + +msgid "No plates available." +msgstr "" + +msgid "Synchronize Filament List from AMS" +msgstr "" + +msgid "Connect a printer to synchronize the AMS filament list." +msgstr "" + +msgid "Open Preset Bundle" +msgstr "" + +msgid "Sign in to sync presets." +msgstr "" + +msgid "Export All Objects as STLs" +msgstr "" + +msgid "Export All Objects as DRC (one file)" +msgstr "" + +msgid "Export All Objects as DRCs" +msgstr "" + +msgid "Export Toolpaths as OBJ" +msgstr "" + +msgid "About OrcaSlicer" +msgstr "" + +msgid "Open Wiki" +msgstr "" + +msgid "Open YouTube Channel" +msgstr "" + +msgid "Open Plugins" +msgstr "" + +msgid "Refresh Plugins" +msgstr "" + +msgid "Install Plugin" +msgstr "" + +msgid "Install Local Plugin" +msgstr "" + +msgid "Unknown command." +msgstr "" + msgid "3D Mouse disconnected." msgstr "3D 滑鼠已中斷連線。" @@ -8252,10 +8492,6 @@ msgstr "" msgid "Delete Object" msgstr "刪除物件" -# AI Translated -msgid "Delete All Objects" -msgstr "刪除所有物件" - # AI Translated msgid "Reset Project" msgstr "重設專案" @@ -8263,10 +8499,6 @@ msgstr "重設專案" msgid "The selected object couldn't be split." msgstr "選取的模型不可分割。" -# AI Translated -msgid "Split to Objects" -msgstr "分割為物件" - msgid "Disable Auto-Drop to preserve Z positioning?\n" msgstr "停用自動落板以保留 Z 定位?\n" @@ -9324,9 +9556,6 @@ msgstr "在切片預覽中拖曳層滑桿時,將目前層以下的各層算繪 msgid "Dimmed layer brightness" msgstr "變暗層的亮度" -msgid "%" -msgstr "%" - # AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" @@ -9863,6 +10092,126 @@ msgstr "為「%1%」,增加「%2%」為一個新預設" msgid "Simply switch to \"%1%\"" msgstr "直接切換到「%1%」" +msgid "Expert" +msgstr "" + +msgid "Simple" +msgstr "" + +msgid "Search actions" +msgstr "" + +#, c-format, boost-format +msgid "Search %s actions" +msgstr "" + +msgid "Recent" +msgstr "" + +msgid "Other" +msgstr "其他" + +#, c-format, boost-format +msgid "No actions match (Total: %s)" +msgstr "" + +msgid "No actions yet" +msgstr "" + +msgid "No tabs match" +msgstr "" + +msgid "No tabs" +msgstr "" + +#, c-format, boost-format +msgid "Showing %s of %s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s actions" +msgstr "" + +#, c-format, boost-format +msgid "%s tabs" +msgstr "" + +#, c-format, boost-format +msgid "%s matches" +msgstr "" + +#, c-format, boost-format +msgid "Favourites are full (%s max)" +msgstr "" + +#, c-format, boost-format +msgid "Go to %s%% of the layer range" +msgstr "" + +msgid "Enter a layer percentage (0-100)" +msgstr "" + +#, c-format, boost-format +msgid "Go to layer %% (0-100)" +msgstr "" + +msgid "Go to tab" +msgstr "" + +#, c-format, boost-format +msgid "Favourite %s (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Pin to favourites (%s)" +msgstr "" + +#, c-format, boost-format +msgid "Unpin from favourites (%s)" +msgstr "" + +msgid "Remove from favourites" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Unpin" +msgstr "" + +msgid "Wiki (F1)" +msgstr "" + +msgid "No wiki page for this action" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a Developer setting. Enable Developer mode to edit it?" +msgstr "" + +msgid "Developer setting" +msgstr "" + +#, c-format, boost-format +msgid "\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?" +msgstr "" + +msgid "Switch settings mode" +msgstr "" + +#, c-format, boost-format +msgid "Run \"%s\"?" +msgstr "" + +msgid "Run plugin" +msgstr "" + +msgid "Don't ask again for this action" +msgstr "" + msgid "Task canceled" msgstr "任務已取消" @@ -10879,9 +11228,6 @@ msgstr "相容的切片設定" msgid "Printable space" msgstr "可列印區域" -msgid "Printer Agent" -msgstr "列印裝置代理" - msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." msgstr "選擇列印裝置通訊的網路代理實施。可用代理在啟動時註冊。" @@ -12670,6 +13016,9 @@ msgstr "使用 3MF 取代 G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "若列印裝置接受 3MF 檔案作為列印作業,請啟用此選項。啟用後,Orca Slicer 會將切片後的檔案以 .gcode.3mf 形式傳送,而非單純的 .gcode 檔案。" +msgid "Printer Agent" +msgstr "列印裝置代理" + msgid "Select the network agent implementation for printer communication." msgstr "選擇用於列印裝置通訊的網路代理實作。" @@ -16781,9 +17130,6 @@ msgstr "在三角網格切片過程中,寬度小於 2 倍間隙閉合半徑的 msgid "Slicing Mode" msgstr "切片模式" -msgid "Other" -msgstr "其他" - msgid "Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model." msgstr "針對 3DLabPrint 飛機模型,請選擇『奇偶』模式。若需閉合模型中的所有孔洞,請啟用『閉合孔洞』選項。" @@ -17697,9 +18043,6 @@ msgstr "線寬過大" msgid " not in range " msgstr " 不在合理的區間" -msgid "Export 3MF" -msgstr "匯出為 3MF 檔案" - msgid "This exports the project as a 3MF file." msgstr "將專案匯出為 3MF 檔案。" @@ -17715,9 +18058,6 @@ msgstr "載入切片資料" msgid "Load cached slicing data from directory." msgstr "從資料夾載入快取的切片資料" -msgid "Export STL" -msgstr "匯出為 STL 檔案" - msgid "Export the objects as single STL." msgstr "將所有物件匯出為單一 STL 檔案。" @@ -18375,9 +18715,6 @@ msgstr "檔案包含無效的頂點索引。" msgid "This OBJ file couldn't be read because it's empty." msgstr "無法讀取此 OBJ 檔案,因為它是空的。" -msgid "Max Volumetric Speed Calibration" -msgstr "最大體積流量校正" - msgid "Manage Result" msgstr "管理結果" @@ -19223,9 +19560,6 @@ msgstr "" msgid "NOTE: High values may cause Layer shift (>%s)" msgstr "注意:較高的值可能會導致層移位 (>%s)" -msgid "Flow Ratio Calibration" -msgstr "流量比例校正" - msgid "Calibration Test Type" msgstr "校正測試類型" diff --git a/resources/web/data/text.js b/resources/web/data/text.js index 0c803c0ada..95342acdaf 100644 --- a/resources/web/data/text.js +++ b/resources/web/data/text.js @@ -118,40 +118,6 @@ var LangText = { orca10: "Not connected", orca11: "Connected", orca12: "Note: When Stealth Mode is enabled, your user profiles will not be backed up to Orca Cloud.", - sd_search: "Search actions", - sd_clear: "Clear", - sd_search_n: "Search %s actions", - sd_showing: "Showing", - sd_of: "of", - sd_actions: "actions", - sd_recent: "Recent", - sd_plugins: "Plugins", - sd_other: "Other", - sd_matches: "matches", - sd_tabs: "tabs", - sd_no_match: "No actions match", - sd_total: "Total", - sd_no_actions: "No actions yet", - sd_no_tabs_match: "No tabs match", - sd_no_tabs: "No tabs", - sd_go_to_pct: "Go to %s% of the layer range", - sd_enter_pct: "Enter a layer percentage (0-100)", - sd_go_layer_ph: "Go to layer % (0-100)", - sd_go_tab_ph: "Go to tab", - sd_favs_full: "Favourites are full", - sd_max: "max", - sd_pin_fav: "Pin to favourites (Ctrl+B)", - sd_unpin_fav: "Unpin from favourites (Ctrl+B)", - sd_remove_fav: "Remove from favourites", - sd_favourite: "Favourite", - sd_move_left: "Move left", - sd_move_right: "Move right", - sd_unpin: "Unpin", - sd_mode_advanced: "Advanced", - sd_mode_expert: "Expert", - sd_mode_develop: "Developer", - sd_wiki: "Wiki", - sd_no_wiki: "No wiki page for this action", }, ca_ES: { t1: "Benvingut a Orca Slicer", diff --git a/resources/web/dialog/SpeedDial/index.html b/resources/web/dialog/SpeedDial/index.html index 0137a2fcf2..e258f34f86 100644 --- a/resources/web/dialog/SpeedDial/index.html +++ b/resources/web/dialog/SpeedDial/index.html @@ -7,7 +7,6 @@ <link rel="stylesheet" href="./style.css" /> <link rel="stylesheet" type="text/css" href="../css/theme.css" /> <script type="text/javascript" src="../../include/globalapi.js"></script> - <script type="text/javascript" src="../../data/text.js"></script> <script src="../../js/fuzzy-search.js"></script> <script src="./speeddial.js"></script> </head> diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index d5615bd3f7..64550c9e5c 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -31,20 +31,23 @@ var SCORE_CONTIGUOUS = 100000; var SCORE_TITLE = 2000; var SCORE_GROUP = 1000; -// Localized lookup for strings this page builds at runtime. text.js (loaded before this script) -// defines LangText; a missing entry falls back to the English literal. Extra args replace -// successive %s placeholders. +// Localized lookup for strings this page builds at runtime. The host injects the translated table +// as a document-start user script (SpeedDialWebDialog::add_user_scripts); the English literal is a +// fallback for the node vm test / before the injection runs. Extra args replace successive %s +// placeholders; %% collapses to a literal % (the C++ table escapes percent signs for gettext). +var UI_STRINGS = (typeof ORCA_UI_STRINGS !== "undefined" && ORCA_UI_STRINGS) || {}; + function T(key, fallback) { - var table = (typeof LangText !== "undefined" && LangText) || null; - var lang = "en"; - try { lang = localStorage.getItem(LANG_COOKIE_NAME) || "en"; } catch (e) {} - var s = table && table[lang] && table[lang][key] !== undefined ? table[lang][key] : - table && table.en && table.en[key] !== undefined ? table.en[key] : fallback; + var s = UI_STRINGS[key] !== undefined ? UI_STRINGS[key] : fallback; for (var i = 2; i < arguments.length; i++) s = s.replace("%s", arguments[i]); - return s; + return s.split("%%").join("%"); } +// Platform shortcut prefixes ("Alt+"/"⌥+", "Ctrl+"/"⌘+"), injected alongside the strings. +function shortcutAlt() { return UI_STRINGS.shortcut_alt || "Alt+"; } +function shortcutCtrl() { return UI_STRINGS.shortcut_ctrl || "Ctrl+"; } + // ---- windowed list render ---------------------------------------------------- // The command list is rendered in windows (append-on-scroll) so a huge settings pool doesn't build // the whole DOM per keystroke. Rows are exactly ROW_H tall (matches .row min-height 44px; see --row-h, @@ -255,8 +258,9 @@ function favDigitFromEvent(e) { } function resultCountText(total, shown, query) { - var n = total + " " + T("sd_actions", "actions"); - return (query || "").trim() ? T("sd_showing", "Showing") + " " + shown + " " + T("sd_of", "of") + " " + n : n; + return (query || "").trim() ? + T("sd_result_count", "Showing %s of %s actions", shown, total) : + T("sd_result_count_all", "%s actions", total); } // Display label for a notebook tab. Trim any stray whitespace; pages added with an empty title @@ -575,7 +579,7 @@ window.HandleStudio = function (payload) { var fid = payload.id; if (fid && FAVS.indexOf(fid) !== -1) FAVS.splice(FAVS.indexOf(fid), 1); render({ resize: true, keepScroll: true }); - flashHint(T("sd_favs_full", "Favourites are full") + " (" + (payload.limit || K_FAV_LIMIT) + " " + T("sd_max", "max") + ")"); + flashHint(T("sd_favs_full", "Favourites are full (%s max)", (payload.limit || K_FAV_LIMIT))); } }; @@ -638,7 +642,8 @@ function pinSvg(on) { function setPinState(pin, on) { pin.classList.toggle("on", on); pin.innerHTML = pinSvg(on); - pin.title = on ? T("sd_unpin_fav", "Unpin from favourites (Ctrl+B)") : T("sd_pin_fav", "Pin to favourites (Ctrl+B)"); + pin.title = on ? T("sd_unpin_fav", "Unpin from favourites (%s)", shortcutCtrl() + "B") : + T("sd_pin_fav", "Pin to favourites (%s)", shortcutCtrl() + "B"); } // ---- render ------------------------------------------------------------------ @@ -672,8 +677,9 @@ function renderFav() { var badge = document.createElement("span"); badge.className = "fav-slot"; badge.textContent = slot; - badge.title = slot === "0" ? T("sd_favourite", "Favourite") + " 10 (Alt+0)" : - T("sd_favourite", "Favourite") + " " + slot + " (Alt+" + slot + ")"; + // Slot "0" is the 10th favourite (Alt/Option+0). + var slot_num = slot === "0" ? "10" : slot; + badge.title = T("sd_fav_slot", "Favourite %s (%s)", slot_num, shortcutAlt() + slot); tile.appendChild(badge); } // Direct removal: a hover-revealed ✕ in the tile's corner. click() stops propagation so it @@ -924,7 +930,7 @@ function renderCommandsList() { matchIndex = {}; if (!total) { - renderEmpty(showList ? (T("sd_no_match", "No actions match") + " (" + T("sd_total", "Total") + ": " + ACTIONS.length + ")") + renderEmpty(showList ? T("sd_no_match_total", "No actions match (Total: %s)", ACTIONS.length) : T("sd_no_actions", "No actions yet")); renderEnd = 0; builtKey = buildKey() + "|0"; @@ -983,7 +989,7 @@ function renderTabList() { listEl.className = "dial-list"; if (countEl) { countEl.hidden = false; - countEl.textContent = q ? list.length + " " + T("sd_matches", "matches") : list.length + " " + T("sd_tabs", "tabs"); + countEl.textContent = q ? T("sd_tab_match_count", "%s matches", list.length) : T("sd_tab_count", "%s tabs", list.length); } list.forEach(function (t, i) { listEl.appendChild(renderTabRow(t, i)); }); } @@ -1029,7 +1035,7 @@ function renderDetail() { var link = document.createElement("button"); link.type = "button"; link.className = "detail-wiki"; - link.textContent = T("sd_wiki", "Wiki") + " (F1)"; + link.textContent = T("sd_wiki_f1", "Wiki (F1)"); link.onclick = function (ev) { ev.stopPropagation(); SendMessage({ command: "open_wiki", id: a.id }); }; detailEl.appendChild(link); } diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index 3f751e6913..1687e28258 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -346,6 +346,27 @@ void ActionRegistry::init() upsert(NativeCommands::make_action(c)); } +void ActionRegistry::relocalize_builtins() +{ + assert(wxThread::IsMain()); + if (!m_started) + return; + + // Drop only the built-in commands; plugins and the dynamically materialised families are + // either unlocalized or rebuilt per snapshot. remove() only erases the map entry, and upsert() + // re-seeds favourites/stats from config, so key-based ids keep their pinned state. + std::vector<std::string> stale; + for (const auto& [id, action] : m_actions) + if (action->source_key() == kOrcaSourceKey && action->kind == AppActionKind::Command) + stale.push_back(id); + for (const std::string& id : stale) + remove(id); + + NativeCommands::rebuild_catalog(); + for (const NativeCommand& c : NativeCommands::catalog()) + upsert(NativeCommands::make_action(c)); +} + void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange change) { assert(wxThread::IsMain()); diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index 026e1bcee5..045de70915 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -159,6 +159,12 @@ public: // updates together. void init(); + // Rebuilds the built-in command actions in the current UI locale. The command catalog copies + // translated titles/groups at construction, so after a live language switch the stored titles + // are stale until this runs. Ids are key-based and upsert re-seeds persisted state, so + // favourites/run history survive. UI thread only. No-op before init(). + void relocalize_builtins(); + // Takes ownership, seeds persisted state, then inserts the action or replaces // the action with the same id. A null action is ignored. void upsert(std::unique_ptr<AppAction> action); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index d67d3db1ba..5d4be5e9f0 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -4607,6 +4607,12 @@ void GUI_App::recreate_GUI(const wxString &msg_name) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI enter"; m_is_recreating_gui = true; + // The palette injects its translated strings once, at creation; drop the cached dialog so the + // next open rebuilds it in the current locale (and can't outlive the old mainframe). + if (m_speed_dial_dialog) { + m_speed_dial_dialog->Destroy(); + m_speed_dial_dialog = nullptr; + } mainframe->shutdown(); ProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE); @@ -8523,6 +8529,9 @@ void GUI_App::open_preferences(size_t open_on_tab, const std::string& highlight_ this->plater_->get_current_canvas3D()->force_set_focus(); return; } + // Built-in Speed Dial command titles are copied from the catalog at init and don't follow a + // live locale switch; rebuild them in the new language before the GUI (and palette) rebuilds. + m_action_registry.relocalize_builtins(); } if (need_recreate_gui) diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp index b1f517e12e..ea833acae5 100644 --- a/src/slic3r/GUI/NativeCommands.cpp +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -282,16 +282,17 @@ std::vector<NativeCommand> build_command_catalog() add_with_icon("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), "calib_sf", [](const std::string&) { return calib_command(CalibKind::VFA); }); // ---- View ---- + // Titles are built with _u8L here (not via a variable) so xgettext can extract them. for (auto [key, dir, title] : - std::initializer_list<std::tuple<const char*, const char*, const char*>>{{"view_top", "top", "View: Top"}, - {"view_bottom", "bottom", "View: Bottom"}, - {"view_front", "front", "View: Front"}, - {"view_rear", "rear", "View: Rear"}, - {"view_left", "left", "View: Left"}, - {"view_right", "right", "View: Right"}, - {"view_iso", "iso", "View: Isometric"}}) { + std::initializer_list<std::tuple<const char*, const char*, std::string>>{{"view_top", "top", _u8L("View: Top")}, + {"view_bottom", "bottom", _u8L("View: Bottom")}, + {"view_front", "front", _u8L("View: Front")}, + {"view_rear", "rear", _u8L("View: Rear")}, + {"view_left", "left", _u8L("View: Left")}, + {"view_right", "right", _u8L("View: Right")}, + {"view_iso", "iso", _u8L("View: Isometric")}}) { std::string k = key, d = dir; - add(k, Slic3r::GUI::I18N::translate_utf8(title), _u8L("View"), + add(k, title, _u8L("View"), [d](const std::string&) { return view_command(wxGetApp().plater(), d); }); } add("view_default", _u8L("View: Default"), _u8L("View"), [](const std::string&) { @@ -626,12 +627,24 @@ std::vector<NativeCommand> build_command_catalog() return out; } +std::vector<NativeCommand>& catalog_storage() +{ + static std::vector<NativeCommand> commands = build_command_catalog(); + return commands; +} + } // namespace const std::vector<NativeCommand>& NativeCommands::catalog() { - static const std::vector<NativeCommand> commands = build_command_catalog(); - return commands; + return catalog_storage(); +} + +void NativeCommands::rebuild_catalog() +{ + // build_command_catalog() re-runs _u8L under the current locale, so replacing the storage + // refreshes every translated title/group after a language switch. + catalog_storage() = build_command_catalog(); } std::unique_ptr<AppAction> NativeCommands::make_action(const NativeCommand& command) diff --git a/src/slic3r/GUI/NativeCommands.hpp b/src/slic3r/GUI/NativeCommands.hpp index 32ae6ea898..993b37f564 100644 --- a/src/slic3r/GUI/NativeCommands.hpp +++ b/src/slic3r/GUI/NativeCommands.hpp @@ -24,9 +24,13 @@ struct NativeCommand }; namespace NativeCommands { -// The full built-in command catalog, built once on first use. UI thread only. +// The full built-in command catalog. Built on first use and reused; call rebuild_catalog() after a +// live UI language switch so the translated titles/groups match the new locale. UI thread only. const std::vector<NativeCommand>& catalog(); +// Rebuilds the catalog in the current locale. UI thread only. +void rebuild_catalog(); + // Dispatches `key` to its runner (unknown keys return a quiet Info). UI thread only. AppActionRunResult run(const std::string& key, const std::string& param = {}); diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index e8dc321555..5fafac4a1a 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -67,6 +67,53 @@ void focus_webview(wxWebView* browser, bool page_ready) browser->RunScript("focusInput();"); } +// Localized strings for the Speed Dial page, injected as a document-start user script. The page's +// T() reads window.ORCA_UI_STRINGS, so these flow through the same .po pipeline as the rest of the +// UI (the JS literals are only a fallback before the script runs / in the node vm test). +// %% is a literal '%': T() collapses it after substituting %s. Keep the shortcut tokens out of the +// translated text so the platform prefix (Alt+/⌥+, Ctrl+/⌘+) stays correct. +nlohmann::json speed_dial_ui_strings() +{ + const std::string alt = GUI::shortkey_alt_prefix(); + const std::string ctrl = GUI::shortkey_ctrl_prefix(); + return { + {"shortcut_alt", alt}, + {"shortcut_ctrl", ctrl}, + + {"sd_search", _u8L("Search actions")}, + {"sd_clear", _u8L("Clear")}, + {"sd_search_n", _u8L("Search %s actions")}, + {"sd_recent", _u8L("Recent")}, + {"sd_plugins", _u8L("Plugins")}, + {"sd_other", _u8L("Other")}, + {"sd_no_match_total", _u8L("No actions match (Total: %s)")}, + {"sd_no_actions", _u8L("No actions yet")}, + {"sd_no_tabs_match", _u8L("No tabs match")}, + {"sd_no_tabs", _u8L("No tabs")}, + {"sd_result_count", _u8L("Showing %s of %s actions")}, + {"sd_result_count_all", _u8L("%s actions")}, + {"sd_tab_count", _u8L("%s tabs")}, + {"sd_tab_match_count", _u8L("%s matches")}, + {"sd_favs_full", _u8L("Favourites are full (%s max)")}, + {"sd_go_to_pct", _u8L("Go to %s%% of the layer range")}, + {"sd_enter_pct", _u8L("Enter a layer percentage (0-100)")}, + {"sd_go_layer_ph", _u8L("Go to layer %% (0-100)")}, + {"sd_go_tab_ph", _u8L("Go to tab")}, + {"sd_fav_slot", _u8L("Favourite %s (%s)")}, + {"sd_pin_fav", _u8L("Pin to favourites (%s)")}, + {"sd_unpin_fav", _u8L("Unpin from favourites (%s)")}, + {"sd_remove_fav", _u8L("Remove from favourites")}, + {"sd_move_left", _u8L("Move left")}, + {"sd_move_right", _u8L("Move right")}, + {"sd_unpin", _u8L("Unpin")}, + {"sd_mode_advanced", _u8L("Advanced")}, + {"sd_mode_expert", _u8L("Expert")}, + {"sd_mode_develop", _u8L("Developer")}, + {"sd_wiki_f1", _u8L("Wiki (F1)")}, + {"sd_no_wiki", _u8L("No wiki page for this action")}, + }; +} + } // namespace SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent) @@ -99,6 +146,18 @@ SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent) SpeedDialWebDialog::~SpeedDialWebDialog() { m_alive->store(false, std::memory_order_release); } +// Document-start hook: hand the page its translated strings before speeddial.js runs, so the first +// paint is already localized. The table is built when the dialog is created; a live language switch +// rebuilds the GUI (and with it this dialog), so the next open re-injects the new locale. +void SpeedDialWebDialog::add_user_scripts() +{ + if (wxWebView* wv = browser()) { + const std::string js = "window.ORCA_UI_STRINGS = " + + speed_dial_ui_strings().dump(-1, ' ', false, nlohmann::json::error_handler_t::ignore) + ";"; + wv->AddUserScript(wxString::FromUTF8(js)); + } +} + void SpeedDialWebDialog::request_show() { if (IsShown()) { diff --git a/src/slic3r/GUI/SpeedDialDialog.hpp b/src/slic3r/GUI/SpeedDialDialog.hpp index 98a32674a5..f044652089 100644 --- a/src/slic3r/GUI/SpeedDialDialog.hpp +++ b/src/slic3r/GUI/SpeedDialDialog.hpp @@ -17,6 +17,7 @@ public: void request_show(); private: + void add_user_scripts() override; void on_script_message(const nlohmann::json& payload) override; void handle_web_command(const nlohmann::json& payload); void resize_to_content(int height); From 9ed853734350da80453846488834a7b4a3071717 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Mon, 14 Sep 2026 15:32:51 +0800 Subject: [PATCH 23/29] Rounded corner for the dialog --- resources/web/dialog/SpeedDial/style.css | 1 + src/slic3r/GUI/SpeedDialDialog.cpp | 37 ++++++++++++++++++++++++ src/slic3r/GUI/SpeedDialDialog.hpp | 7 +++++ 3 files changed, 45 insertions(+) diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index 8afc33d98c..5c29e68cdb 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -21,6 +21,7 @@ body { flex-direction: column; background: var(--panel, var(--orca-bg, #fff)); border: 1px solid var(--border, var(--orca-border, #ddd)); + border-radius: 7px; overflow: hidden; /* why: no height cap here - the launcher reports its true natural height to C++, which sizes the popup to match (HTML is source of truth). The list's own max-height is what diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index 5fafac4a1a..aa857d0963 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -11,7 +11,9 @@ #include <algorithm> +#include <wx/dcmemory.h> #include <wx/display.h> +#include <wx/region.h> #include <wx/sizer.h> #include <wx/stattext.h> #include <wx/utils.h> @@ -142,6 +144,7 @@ SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent) SetSizer(sizer); SetClientSize(FromDIP(wxSize(kPopupWidth, kPopupMinHeight))); } + apply_rounded_shape(); } SpeedDialWebDialog::~SpeedDialWebDialog() { m_alive->store(false, std::memory_order_release); } @@ -168,6 +171,7 @@ void SpeedDialWebDialog::request_show() Show(); Raise(); + apply_rounded_shape(); if (m_page_ready) send_actions(); // Grab focus now and again on wxEVT_ACTIVATE; grabbing directly on the WebKit widget is @@ -245,6 +249,39 @@ void SpeedDialWebDialog::resize_to_content(int height) const int height_dip = std::max(kPopupMinHeight, std::min(height, max_dip)); SetClientSize(FromDIP(wxSize(kPopupWidth, height_dip))); Layout(); + apply_rounded_shape(); +} + +// Rounded corners: the webview paints an opaque rectangle, so round the whole top-level window +// with a shape region (same mask trick as FilamentPickerDialog). Binary edges, no anti-aliasing. +void SpeedDialWebDialog::apply_rounded_shape() +{ + const wxSize size = GetSize(); + if (size.GetWidth() <= 0 || size.GetHeight() <= 0) + return; + + m_shape_bmp.Create(size.GetWidth(), size.GetHeight(), 32); + if (!m_shape_bmp.IsOk()) + return; + + wxMemoryDC dc; + dc.SelectObject(m_shape_bmp); + dc.SetBackground(wxBrush(wxColour(0, 0, 0))); + dc.Clear(); + dc.SetBrush(wxBrush(wxColour(255, 255, 255, 255))); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRoundedRectangle(0, 0, size.GetWidth(), size.GetHeight(), FromDIP(m_corner_radius)); + dc.SelectObject(wxNullBitmap); + + wxRegion region(m_shape_bmp, wxColour(0, 0, 0)); + if (region.IsOk()) + SetShape(region); +} + +void SpeedDialWebDialog::on_dpi_changed(const wxRect&) +{ + apply_rounded_shape(); + Refresh(); } void SpeedDialWebDialog::run_action(const std::string& id, const std::string& title, const std::string& param) diff --git a/src/slic3r/GUI/SpeedDialDialog.hpp b/src/slic3r/GUI/SpeedDialDialog.hpp index f044652089..e7a4060e16 100644 --- a/src/slic3r/GUI/SpeedDialDialog.hpp +++ b/src/slic3r/GUI/SpeedDialDialog.hpp @@ -7,6 +7,8 @@ #include <memory> #include <string> +#include <wx/bitmap.h> + namespace Slic3r { namespace GUI { class SpeedDialWebDialog : public WebViewHostDialog @@ -25,8 +27,13 @@ private: void open_wiki(const std::string& id); void send_actions(); void search_tabs(); + void apply_rounded_shape(); + void on_dpi_changed(const wxRect& suggested_rect) override; bool m_page_ready{false}; + // Rounded corners via a window shape region, since the webview itself is opaque. + int m_corner_radius{7}; + wxBitmap m_shape_bmp; // Guards the CallAfter in on_script_message across dialog destruction, same as // PluginsDialog::m_alive (PluginsDialog.hpp:249). std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true); From e1d3c900300076e0bef70e452baa234ae353a18c Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Mon, 14 Sep 2026 18:08:34 +0800 Subject: [PATCH 24/29] Fixes for window rounding --- src/slic3r/GUI/SpeedDialDialog.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index aa857d0963..08f2c45b86 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -124,7 +124,7 @@ SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent) wxEmptyString, wxDefaultPosition, wxDefaultSize, - wxBORDER_NONE | wxFRAME_NO_TASKBAR | wxFRAME_FLOAT_ON_PARENT) + wxBORDER_NONE | wxFRAME_NO_TASKBAR | wxFRAME_FLOAT_ON_PARENT | wxFRAME_SHAPED) { SetBackgroundColour(bg_color()); Bind(wxEVT_ACTIVATE, [this](wxActivateEvent& event) { @@ -144,6 +144,12 @@ SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent) SetSizer(sizer); SetClientSize(FromDIP(wxSize(kPopupWidth, kPopupMinHeight))); } + // Re-cut the shape region whenever layout changes the client size; SetShape itself + // does not generate size events, so this cannot recurse. + Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { + event.Skip(); + apply_rounded_shape(); + }); apply_rounded_shape(); } @@ -256,7 +262,8 @@ void SpeedDialWebDialog::resize_to_content(int height) // with a shape region (same mask trick as FilamentPickerDialog). Binary edges, no anti-aliasing. void SpeedDialWebDialog::apply_rounded_shape() { - const wxSize size = GetSize(); + // BORDER_NONE means the window is all client area, so the client size is the shape size. + const wxSize size = GetClientSize(); if (size.GetWidth() <= 0 || size.GetHeight() <= 0) return; From f76e4b1e02885e987f9600190b904d0b36d46f99 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Wed, 16 Sep 2026 12:15:27 +0800 Subject: [PATCH 25/29] Fixed centering of icons. Disabled zooming in/out on Windows. Added default icons --- resources/images/action_default.svg | 2 + resources/web/dialog/SpeedDial/speeddial.js | 18 ++++-- .../web/dialog/SpeedDial/speeddial.test.js | 11 ++++ resources/web/dialog/SpeedDial/style.css | 5 +- src/slic3r/GUI/NativeCommands.cpp | 56 ++++++++++--------- src/slic3r/GUI/SpeedDialDialog.cpp | 4 ++ tests/slic3rutils/test_action_source.cpp | 17 ++++-- 7 files changed, 76 insertions(+), 37 deletions(-) create mode 100644 resources/images/action_default.svg diff --git a/resources/images/action_default.svg b/resources/images/action_default.svg new file mode 100644 index 0000000000..118f4792a6 --- /dev/null +++ b/resources/images/action_default.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M14.5,1.5v12a1,1,0,0,1-1,1H1.5a1,1,0,0,1-1-1V1.5a1,1,0,0,1,1-1h12a1,1,0,0,1,1,1Z" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><polyline points="4,5 6.5,7.5 4,10" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/><line x1="8" y1="10" x2="11" y2="10" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/></svg> diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 64550c9e5c..96725c8a8c 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -294,13 +294,21 @@ function actionIcon(a) { return (a && a.icon) ? a.icon : ""; } -// Put a pictogram into a tile (search row, favourites tile, or tab row). No icon leaves the tile -// blank. `mono` marks the white tab-strip glyphs, which the CSS recolors to the shared gray. +// Pictogram shown when an action carries none (plugins, icon-less commands/settings). A dedicated +// theme-neutral glyph (resources/images/action_default.svg: frame + ">_" prompt), so no tile is +// blank and no existing action's icon is borrowed. +var DEFAULT_ICON = "action_default"; + +// SVG base name a tile actually renders: the action's own icon, else the placeholder. Pure. +function tileIcon(a) { + return actionIcon(a) || DEFAULT_ICON; +} + +// Put a pictogram into a tile (search row, favourites tile, or tab row). `mono` marks the white +// tab-strip glyphs, which the CSS recolors to the shared gray. function fillTile(tile, a, mono) { tile.textContent = ""; - var icon = actionIcon(a); - if (!icon) - return; + var icon = tileIcon(a); var img = document.createElement("img"); img.className = mono ? "tile-icon tab-mono" : "tile-icon"; img.src = ICON_BASE + icon + ".svg"; diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 38f7c343ae..4c2f606843 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -281,6 +281,17 @@ assert.equal(ctx.actionIcon({ id: "x", title: "Plugin action" }), "", assert.equal(ctx.actionIcon(null), "", "a null action (tab row) renders a blank tile"); +// tileIcon: the base name a tile renders - the action's own icon when present, else the placeholder. +assert.equal(ctx.tileIcon({ id: "x", title: "Slice", icon: "media_play" }), "media_play", + "an action with an icon keeps it"); +assert.equal(ctx.tileIcon({ id: "x", title: "Go to tab...", icon: "" }), "action_default", + "an empty icon falls back to the placeholder"); +assert.equal(ctx.tileIcon({ id: "x", title: "Plugin action" }), "action_default", + "a missing icon falls back to the placeholder"); +assert.equal(ctx.DEFAULT_ICON, "action_default", "the placeholder is the dedicated default glyph"); +assert.ok(fs.existsSync(__dirname + "/../../../images/action_default.svg"), + "the placeholder SVG ships alongside the page's other icons"); + // needsModeSwitch: a setting is gated only when its required mode outranks the user's current mode. assert.equal(ctx.needsModeSwitch({ mode: "advanced" }, "simple"), true, "Advanced is gated in Simple mode"); assert.equal(ctx.needsModeSwitch({ mode: "expert" }, "simple"), true, "Expert is gated in Simple mode"); diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index 5c29e68cdb..4373ff7067 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -331,12 +331,15 @@ body { border: 1px solid var(--speed-tile-border, #d8d8d8); } -/* Native SVG pictogram in a tile; blank tiles (no icon) have no child. */ +/* Native SVG pictogram in a tile. The Orca icon grid draws 1px strokes from 0.5..14.5 of the 16 + viewBox, so the visible glyph is 0..15 and, centered as-is, leaves one extra pixel on the right/ + bottom (the "Save Project leans left" look). Shift by half a pixel to center the visible artwork. */ .tile-icon { width: 16px; height: 16px; display: block; pointer-events: none; + transform: translate(.5px, .5px); } /* Tab-strip glyphs are drawn white for the dark tab bar; recolor to the shared #949494 gray so diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp index ea833acae5..2e24f14326 100644 --- a/src/slic3r/GUI/NativeCommands.cpp +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -236,12 +236,12 @@ std::vector<NativeCommand> build_command_catalog() }); // ---- Export pipeline ---- - add_with_icon("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), "menu_export_gcode", [](const std::string&) { + add_with_icon("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), "custom-gcode_gcode", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_gcode(false); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_stl", _u8L("Export STL"), _u8L("Slice & Export"), "menu_export_stl", [](const std::string&) { + add_with_icon("export_stl", _u8L("Export STL"), _u8L("Slice & Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(); return AppActionRunResult{AppActionRunResult::Level::Success}; @@ -251,35 +251,37 @@ std::vector<NativeCommand> build_command_catalog() plater->export_core_3mf(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"), "menu_export_sliced_file", [](const std::string&) { + add_with_icon("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_gcode_3mf(false); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), "menu_export_sliced_file", [](const std::string&) { + add_with_icon("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_gcode_3mf(true); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // ---- Calibration ---- - add_with_icon("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), "calib_sf", + // The tab-strip calib_sf glyph is drawn white for the dark tab bar and vanishes on the palette's + // light tile, so each wizard borrows the matching settings-group icon instead (gray + accent green). + add_with_icon("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), "param_temperature", [](const std::string&) { return calib_command(CalibKind::Temperature); }); - add_with_icon("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"), "calib_sf", + add_with_icon("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"), "param_volumetric_speed", [](const std::string&) { return calib_command(CalibKind::MaxVolumetric); }); - add_with_icon("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"), "calib_sf", + add_with_icon("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance", [](const std::string&) { return calib_command(CalibKind::PressureAdvance); }); - add_with_icon("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"), "calib_sf", + add_with_icon("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance", [](const std::string&) { return calib_command(CalibKind::FlowRatio); }); - add_with_icon("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"), "calib_sf", + add_with_icon("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"), "param_retraction", [](const std::string&) { return calib_command(CalibKind::Retraction); }); - add_with_icon("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"), "calib_sf", + add_with_icon("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"), "param_precision", [](const std::string&) { return calib_command(CalibKind::Cornering); }); - add_with_icon("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"), "calib_sf", + add_with_icon("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"), "param_resonance_avoidance", [](const std::string&) { return calib_command(CalibKind::InputShapingFreq); }); - add_with_icon("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), "calib_sf", + add_with_icon("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), "param_resonance_avoidance", [](const std::string&) { return calib_command(CalibKind::InputShapingDamp); }); - add_with_icon("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), "calib_sf", [](const std::string&) { return calib_command(CalibKind::VFA); }); + add_with_icon("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), "param_speed", [](const std::string&) { return calib_command(CalibKind::VFA); }); // ---- View ---- // Titles are built with _u8L here (not via a variable) so xgettext can extract them. @@ -322,10 +324,10 @@ std::vector<NativeCommand> build_command_catalog() }); // ---- Object ---- - add_with_icon("obj_delete", _u8L("Delete Selected"), _u8L("Object"), "menu_delete", [](const std::string&) { + add_with_icon("obj_delete", _u8L("Delete Selected"), _u8L("Object"), "delete", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->remove_selected(); }); }); - add_with_icon("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"), "menu_remove", [](const std::string&) { + add_with_icon("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"), "delete", [](const std::string&) { return object_op( wxGetApp().plater(), [](Plater* p) { return p->can_delete_all(); }, [](Plater* p) { p->delete_all_objects_from_model(); }); }); @@ -440,7 +442,7 @@ std::vector<NativeCommand> build_command_catalog() plater->duplicate_plate(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("plate_delete", _u8L("Delete Plate"), _u8L("Plate"), "menu_delete", [](const std::string&) { + add_with_icon("plate_delete", _u8L("Delete Plate"), _u8L("Plate"), "delete", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (!is_fff_plater(plater)) return plate_unavailable(); @@ -511,7 +513,7 @@ std::vector<NativeCommand> build_command_catalog() }); // ---- Import ---- - add_with_icon("import_file", _u8L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), _u8L("Import"), "menu_import", [](const std::string&) { + add_with_icon("import_file", _u8L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), _u8L("Import"), "menu_open", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) { #ifdef __APPLE__ plater->add_model(); @@ -521,39 +523,39 @@ std::vector<NativeCommand> build_command_catalog() } return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("import_zip_archive", _u8L("Import ZIP Archive"), _u8L("Import"), "menu_import", [](const std::string&) { + add_with_icon("import_zip_archive", _u8L("Import ZIP Archive"), _u8L("Import"), "menu_open", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->import_zip_archive(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("import_configs", _u8L("Import Configs"), _u8L("Import"), "menu_import", [](const std::string&) { + add_with_icon("import_configs", _u8L("Import Configs"), _u8L("Import"), "menu_open", [](const std::string&) { if (MainFrame* mf = wxGetApp().mainframe) mf->load_config_file(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // ---- Export extras ---- - add_with_icon("export_stl_multi", _u8L("Export All Objects as STLs"), _u8L("Export"), "menu_export_stl", [](const std::string&) { + add_with_icon("export_stl_multi", _u8L("Export All Objects as STLs"), _u8L("Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(false, false, true); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_drc_single", _u8L("Export All Objects as DRC (one file)"), _u8L("Export"), "menu_export_stl", [](const std::string&) { + add_with_icon("export_drc_single", _u8L("Export All Objects as DRC (one file)"), _u8L("Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(false, false, false, FT_DRC); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_drc_multi", _u8L("Export All Objects as DRCs"), _u8L("Export"), "menu_export_stl", [](const std::string&) { + add_with_icon("export_drc_multi", _u8L("Export All Objects as DRCs"), _u8L("Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(false, false, true, FT_DRC); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_toolpaths_obj", _u8L("Export Toolpaths as OBJ"), _u8L("Export"), "menu_export_toolpaths", [](const std::string&) { + add_with_icon("export_toolpaths_obj", _u8L("Export Toolpaths as OBJ"), _u8L("Export"), "custom-gcode_gcode", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_toolpaths_to_obj(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_config", _u8L("Export Preset Bundle"), _u8L("Export"), "menu_export_config", [](const std::string&) { + add_with_icon("export_config", _u8L("Export Preset Bundle"), _u8L("Export"), "save", [](const std::string&) { if (MainFrame* mf = wxGetApp().mainframe) mf->export_config(); return AppActionRunResult{AppActionRunResult::Level::Success}; @@ -568,7 +570,7 @@ std::vector<NativeCommand> build_command_catalog() wxGetApp().ShowUserGuide(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("help_open_config_folder", _u8L("Show Configuration Folder"), _u8L("Help"), "folder-closed", [](const std::string&) { + add_with_icon("help_open_config_folder", _u8L("Show Configuration Folder"), _u8L("Help"), "open_project", [](const std::string&) { Slic3r::GUI::desktop_open_datadir_folder(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); @@ -589,11 +591,11 @@ std::vector<NativeCommand> build_command_catalog() } return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("help_check_updates", _u8L("Check for Updates"), _u8L("Help"), "ams_refresh_normal", [](const std::string&) { + add_with_icon("help_check_updates", _u8L("Check for Updates"), _u8L("Help"), "refresh", [](const std::string&) { wxGetApp().check_new_version_sf(true, 1); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("help_about", _u8L("About OrcaSlicer"), _u8L("Help"), "OrcaSlicer_about", [](const std::string&) { + add_with_icon("help_about", _u8L("About OrcaSlicer"), _u8L("Help"), "OrcaSlicer_gradient_circle", [](const std::string&) { Slic3r::GUI::about(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index 08f2c45b86..3de0c137bb 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -144,6 +144,10 @@ SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent) SetSizer(sizer); SetClientSize(FromDIP(wxSize(kPopupWidth, kPopupMinHeight))); } + // WebView2's browser accelerator keys include Ctrl +/-/0 and Ctrl+wheel zoom, which would resize + // the page inside the fixed-size popup. No-op on the other backends (wxWidgets 3.3 base virtual). + if (wxWebView* wv = browser()) + wv->EnableBrowserAcceleratorKeys(false); // Re-cut the shape region whenever layout changes the client size; SetShape itself // does not generate size events, so this cannot recurse. Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index 1743f09ab3..0eb5edde41 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -171,9 +171,10 @@ TEST_CASE("Native command catalog has unique keys and present titles", "[ActionS } } -// Every command's tile pictogram is the SVG the matching GUI control already uses; an absent icon -// means a blank tile (like the tab picker). Guard representative names and that every non-empty -// value resolves to a shipped file, so a rename/typo cannot leave broken images in the palette. +// Every command's tile pictogram is a theme-neutral SVG (the matching GUI control's icon, or the +// equivalent settings-group icon); an absent icon gets the page's generic placeholder. Guard +// representative names and that every non-empty value resolves to a shipped file, so a rename/typo +// cannot leave broken images in the palette. TEST_CASE("Native command icons resolve to shipped SVGs", "[ActionSource][SpeedDial]") { const std::vector<Slic3r::GUI::NativeCommand>& commands = Slic3r::GUI::NativeCommands::catalog(); @@ -193,9 +194,17 @@ TEST_CASE("Native command icons resolve to shipped SVGs", "[ActionSource][SpeedD Expected{"save_project", "menu_save"}, Expected{"sync_ams", "ams_fila_sync"}, Expected{"mode_simple", "advanced"}, - Expected{"calib_temperature", "calib_sf"}, + Expected{"calib_temperature", "param_temperature"}, + Expected{"calib_cornering", "param_precision"}, Expected{"plate_add", "toolbar_add_plate"}, Expected{"add_primitive_cube", "menu_obj_cube"}, + // These previously pointed at blank placeholder SVGs or theme-broken ones. + Expected{"obj_delete", "delete"}, + Expected{"export_gcode", "custom-gcode_gcode"}, + Expected{"import_file", "menu_open"}, + Expected{"help_open_config_folder", "open_project"}, + Expected{"help_check_updates", "refresh"}, + Expected{"help_about", "OrcaSlicer_gradient_circle"}, Expected{"go_to_tab", ""}}) { const std::string* icon = icon_of(e.key); INFO(e.key); From b01d18bba3d1b50b79230ba1dc11c6c082bdfaa6 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Wed, 16 Sep 2026 13:06:16 +0800 Subject: [PATCH 26/29] Tab autocomplete. Better search integration of categories and action names --- resources/web/dialog/SpeedDial/index.html | 5 +- resources/web/dialog/SpeedDial/speeddial.js | 199 ++++++++++++++++-- .../web/dialog/SpeedDial/speeddial.test.js | 49 +++++ resources/web/dialog/SpeedDial/style.css | 35 +++ 4 files changed, 266 insertions(+), 22 deletions(-) diff --git a/resources/web/dialog/SpeedDial/index.html b/resources/web/dialog/SpeedDial/index.html index e258f34f86..a9070b972e 100644 --- a/resources/web/dialog/SpeedDial/index.html +++ b/resources/web/dialog/SpeedDial/index.html @@ -19,7 +19,10 @@ <span class="plugin-search-icon" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/></svg> </span> - <input class="plugin-search-input" id="q" type="text" placeholder="Search actions" spellcheck="false" autocomplete="off" aria-label="Search actions" /> + <span class="plugin-search-field"> + <span class="search-ghost" id="ghost" hidden aria-hidden="true"><span class="search-ghost-typed" id="ghostTyped"></span><span class="search-ghost-suffix" id="ghostSuffix"></span></span> + <input class="plugin-search-input" id="q" type="text" placeholder="Search actions" spellcheck="false" autocomplete="off" aria-label="Search actions" /> + </span> <button class="plugin-search-clear" id="clear" type="button" title="Clear" aria-label="Clear search" hidden> <svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><line x1="5" y1="5" x2="11" y2="11"/><line x1="11" y1="5" x2="5" y2="11"/></svg> </button> diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 96725c8a8c..41e180a3cd 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -30,6 +30,9 @@ var MODE_RANK = { simple: 0, advanced: 1, expert: 2, develop: 3 }; var SCORE_CONTIGUOUS = 100000; var SCORE_TITLE = 2000; var SCORE_GROUP = 1000; +// A whole-query match in a single field must outrank any multi-token match distributed across fields. +// Larger than the largest plausible sum of per-token scores (SCORE_CONTIGUOUS * token count). +var SCORE_PHRASE = 10000000; // Localized lookup for strings this page builds at runtime. The host injects the translated table // as a document-start user script (SpeedDialWebDialog::add_user_scripts); the English literal is a @@ -68,8 +71,11 @@ var sectionStarts = null; var sectionTotal = 0; var sectionRendered = 0; -// search-cache: the normalized (folded+lowercased) needle for the current query pass. +// search-cache: the normalized (folded+lowercased) needle for the current query pass, plus the +// whitespace-separated tokens and their compiled whole-word regexes for the multi-token path. var searchNeedle = ""; +var searchTokens = []; +var searchTokenRes = []; // Palette phase: 'commands' (one unified search over actions/commands/settings, recents on empty // query), 'percent' ("Go to layer" second phase: enter a 0-100 percentage), 'tab' ("Go to tab..." @@ -82,6 +88,9 @@ var tabOptions = []; // [{id,title}] - notebook pages, fetched on entering // element handles, assigned in OnInit (kept null so load-time touches no DOM) var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, countEl = null, detailEl = null; +var ghostEl = null, ghostTypedEl = null, ghostSuffixEl = null; +// The completion currently offered as ghost text ({suffix, word, id}), or null. Tab accepts it. +var activeCompletion = null; // ---- pure helpers (no DOM; unit-tested) ------------------------------------- // Pre-normalized haystacks, cached on the action object. The fold is length-preserving (1:1 per @@ -106,20 +115,20 @@ function sourceNorm(a) { return a._sn; } -// Match one pre-normalized field vs the current needle. Returns {score, ranges, contiguous} when the -// needle is present, else null. wwRe is a compiled whole-word (\b-bounded) regex for the current needle. -// A whole-word hit is preferred - it highlights the full word (e.g. "orient" in "Auto-Orient", not the -// stray "o" of "Auto") and marks a perfect match. Otherwise FuzzyRangesNorm (which now prefers the +// Match one pre-normalized field vs one pre-normalized needle. Returns {score, ranges, contiguous} +// when the needle is present, else null. wwRe is a compiled whole-word (\b-bounded) regex for the +// needle. A whole-word hit is preferred - it highlights the full word (e.g. "orient" in "Auto-Orient", +// not the stray "o" of "Auto") and marks a perfect match. Otherwise FuzzyRangesNorm (which prefers the // most-contiguous run) is used. score is higher for an earlier start and fewer gaps; contiguous marks // a perfect match - the whole needle landed as one unbroken run. -function fieldMatchScore(norm, wwRe) { - if (!searchNeedle) return null; +function fieldMatchScore(norm, needle, wwRe) { + if (!needle) return null; if (wwRe) { var m = wwRe.exec(norm || ""); if (m) return { score: 1000 - m.index * 10, ranges: [[m.index, m.index + m[0].length]], contiguous: true }; } - var r = FuzzyRangesNorm(norm || "", searchNeedle); + var r = FuzzyRangesNorm(norm || "", needle); if (!r) return null; var gaps = 0, len = 0; for (var i = 0; i < r.length; i++) { @@ -127,7 +136,57 @@ function fieldMatchScore(norm, wwRe) { gaps += r[i][0] - r[i - 1][1]; len += r[i][1] - r[i][0]; } - return { score: 1000 - r[0][0] * 10 - gaps * 10, ranges: r, contiguous: r.length === 1 && len === searchNeedle.length }; + return { score: 1000 - r[0][0] * 10 - gaps * 10, ranges: r, contiguous: r.length === 1 && len === needle.length }; +} + +// Split a query into normalized (folded+lowercased) whitespace-separated tokens. Empty for a blank +// query. These drive the multi-token path: every token must match some field, but different tokens +// may match different fields (the title, the group, or the source breadcrumb). +function queryTokens(query) { + var norm = NormText(String(query || "").trim(), false); + return norm ? norm.split(/\s+/).filter(Boolean) : []; +} + +// Whole-word regex for one normalized token, same shape fieldMatchScore expects. +function tokenWordRe(token) { + return new RegExp("\\b" + EscapeRegExp(token) + "\\b"); +} + +// One token's best match across an action's three fields, keeping the per-field ranges so the caller +// can highlight each matched word. Returns {score, title, group, source} (ranges or null per field), or +// null when no field contains the token. Score mirrors scoreFields' tiers: contiguous > fuzzy, then +// title > group > source. +function tokenMatch(a, token, wwRe) { + var t = fieldMatchScore(titleNorm(a), token, wwRe); + var g = fieldMatchScore(groupNorm(a), token, wwRe); + var s = fieldMatchScore(sourceNorm(a), token, wwRe); + if (!t && !g && !s) return null; + var score = Math.max( + t ? (t.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_TITLE + t.score : -Infinity, + g ? (g.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_GROUP + g.score : -Infinity, + s ? (s.contiguous ? SCORE_CONTIGUOUS : 0) + s.score : -Infinity + ); + return { score: score, title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null }; +} + +// Merge per-field token ranges into sorted, coalesced ranges for highlighting. Overlapping or adjacent +// runs (the same word matched by two tokens) collapse to one span. Null when nothing matched. +function mergeRanges(ranges) { + var flat = []; + (ranges || []).forEach(function (rs) { + if (rs) rs.forEach(function (r) { flat.push([r[0], r[1]]); }); + }); + if (!flat.length) return null; + flat.sort(function (a, b) { return a[0] - b[0] || a[1] - b[1]; }); + var out = [flat[0].slice()]; + for (var i = 1; i < flat.length; i++) { + var last = out[out.length - 1]; + if (flat[i][0] <= last[1]) + last[1] = Math.max(last[1], flat[i][1]); + else + out.push(flat[i].slice()); + } + return out; } // Combine the per-field match scores into one comparable value, or null when no field matched. @@ -151,12 +210,21 @@ function scoreFields(t, g, s) { // by relevance (not by action type). Sets matchIndex so rows highlight their match ranges. The query // is normalized ONCE per pass - FuzzyRangesNorm then runs against each action's pre-normalized // haystack, so per-keystroke cost is a cheap scan (no per-char normalize/regex). +// +// Two match modes, phrase preferred: +// - phrase: the whole trimmed query as one ordered subsequence in a SINGLE field (as before). +// - tokens: every whitespace-separated word must match SOME field, but different words may match +// different fields. This is what lets "speed acceleration inner" find "Inner wall" whose path is +// "Process : Speed : Acceleration" (title + source breadcrumb together). +// A phrase match always outranks a distributed token match. function searchActions(actions, query) { var q = (query || "").trim(); var list = actions || []; matchIndex = {}; - if (!q) { searchNeedle = ""; return list.slice(0); } + if (!q) { searchNeedle = ""; searchTokens = []; searchTokenRes = []; return list.slice(0); } searchNeedle = NormText(q, false); + searchTokens = queryTokens(q); + searchTokenRes = searchTokens.map(tokenWordRe); // Mode keywords ("advanced"/"expert"/"developer") are a union, not a filter: the normal text // search still runs on the full query, and every setting requiring a named mode is appended. var modes = modeFilterFromQuery(q); @@ -168,17 +236,36 @@ function searchActions(actions, query) { var seen = {}; for (var i = 0; i < list.length; i++) { var a = list[i]; - var t = fieldMatchScore(titleNorm(a), wwRe); - var g = fieldMatchScore(groupNorm(a), wwRe); - var s = fieldMatchScore(sourceNorm(a), wwRe); - var score = scoreFields(t, g, s); - if (score === null) continue; + var t = fieldMatchScore(titleNorm(a), searchNeedle, wwRe); + var g = fieldMatchScore(groupNorm(a), searchNeedle, wwRe); + var s = fieldMatchScore(sourceNorm(a), searchNeedle, wwRe); + var phrase = scoreFields(t, g, s); + var score, ranges; + if (phrase !== null) { + score = phrase + SCORE_PHRASE; + ranges = { title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null }; + } else { + // Require every token; a token that matches nothing drops the action immediately. Ranges + // from all matching tokens are merged per field so each matched word highlights. + var sum = 0, titleR = null, groupR = null, sourceR = null, all = true; + for (var k = 0; k < searchTokens.length; k++) { + var m = tokenMatch(a, searchTokens[k], searchTokenRes[k]); + if (!m) { all = false; break; } + sum += m.score; + if (m.title) (titleR || (titleR = [])).push(m.title); + if (m.group) (groupR || (groupR = [])).push(m.group); + if (m.source) (sourceR || (sourceR = [])).push(m.source); + } + if (!all) continue; + score = sum; + ranges = { title: mergeRanges(titleR), group: mergeRanges(groupR), source: mergeRanges(sourceR) }; + } // Ranges are per-field against the ACTUAL text drawn: title for the row-name, and group (or // source when group is empty) for the eyebrow - so highlight offsets stay aligned to the label. matchIndex[a.id] = { - title: t ? t.ranges : null, - group: g ? g.ranges : null, - source: s ? s.ranges : null, + title: ranges.title, + group: ranges.group, + source: ranges.source, useEyebrowGroup: !!(a.group) }; scored.push({ a: a, s: score }); @@ -206,6 +293,38 @@ function searchActions(actions, query) { return result; } +// Candidate words for inline completion, in reading order. Whitespace and the breadcrumb separator +// split them, so "Process : Speed : Acceleration" yields Process/Speed/Acceleration. Pure. +function completionWords(text) { + return String(text || "").split(/[\s:]+/).filter(Boolean); +} + +// The inline completion for the query's LAST token: scan the top-ranked results' name first, then +// their path breadcrumb, and return the first word that extends the typed token as a prefix. Headlines +// the most likely word without committing to a result. Returns {suffix, word, id} or null. Pure so the +// node-vm test can exercise it; the caller appends `suffix` to the input. +function completionFor(query, list) { + var raw = String(query || ""); + var parts = raw.match(/(\S+)\s*$/); + if (!parts) return null; + var prefix = NormText(parts[1], false); + if (!prefix) return null; + var top = (list || []).slice(0, 10); + for (var i = 0; i < top.length; i++) { + var a = top[i]; + var fields = [a.title, a.group, a.source]; + for (var f = 0; f < fields.length; f++) { + var words = completionWords(fields[f]); + for (var w = 0; w < words.length; w++) { + var norm = NormText(words[w], false); + if (norm.length > prefix.length && norm.indexOf(prefix) === 0) + return { suffix: words[w].slice(prefix.length), word: words[w], id: a.id }; + } + } + } + return null; +} + // Pure: how many rows must be materialized to cover the given starting index plus `size` more. // Clamped to the total; used to decide "render the next window" on scroll / arrow-nav. function revealTarget(total, fromIndex, size) { @@ -361,12 +480,15 @@ function commandList(actions, recents, query) { var all = actions || []; if (shouldRenderActionList(query)) { if (searchCache && searchCache.actions === all && searchCache.query === query) { - matchIndex = searchCache.matchIndex; - searchNeedle = searchCache.needle; + matchIndex = searchCache.matchIndex; + searchNeedle = searchCache.needle; + searchTokens = searchCache.tokens; + searchTokenRes = searchCache.tokenRes; return searchCache.list; } var found = searchActions(all, query); - searchCache = { actions: all, query: query, list: found, matchIndex: matchIndex, needle: searchNeedle }; + searchCache = { actions: all, query: query, list: found, matchIndex: matchIndex, needle: searchNeedle, + tokens: searchTokens, tokenRes: searchTokenRes }; return found; } var rec = recents || []; @@ -1049,10 +1171,28 @@ function renderDetail() { } } +// Refresh the muted inline completion shown at the end of the search field. Only offered in the +// commands phase, with the caret at the end of a non-empty, non-trailing-space input that isn't +// scrolled (so the overlay lines up with the real caret). The ghost is the typed text (hidden, to +// reserve its width) followed by the suggested suffix, so it sits exactly after the caret. +function updateGhost() { + activeCompletion = null; + if (!ghostEl || !qEl) return; + var eligible = phase === "commands" && qEl.value && qEl.selectionStart === qEl.value.length && + !/\s$/.test(qEl.value) && qEl.scrollWidth <= qEl.clientWidth; + var comp = eligible ? completionFor(query, currentList()) : null; + if (!comp) { ghostEl.hidden = true; return; } + ghostTypedEl.textContent = qEl.value; + ghostSuffixEl.textContent = comp.suffix; + ghostEl.hidden = false; + activeCompletion = comp; +} + function render(opts) { renderFav(); renderList(); renderDetail(); + updateGhost(); // Pin toggles don't move the selection, so they pass keepScroll to avoid snapping the list // back to a row that is currently off-screen. if (!(opts && opts.keepScroll)) @@ -1203,6 +1343,7 @@ function focusInput() { setTimeout(function () { if (qEl) qEl.focus(); }, 0); } // ---- init -------------------------------------------------------------------- function OnInit() { qEl = $("q"); listEl = $("list"); favEl = $("favBar"); clearEl = $("clear"); eyeEl = $("favEyebrow"); countEl = $("count"); detailEl = $("detail"); + ghostEl = $("ghost"); ghostTypedEl = $("ghostTyped"); ghostSuffixEl = $("ghostSuffix"); // text.js's TranslatePage() targets jQuery `.trans` nodes; this page has none and defines its own // `$`, so don't call it. Runtime strings go through T() instead. qEl.placeholder = T("sd_search", "Search actions"); @@ -1222,6 +1363,9 @@ function OnInit() { query = qEl.value; sel = { zone: "list", i: 0 }; syncClearButton(); render({ resize: true, resetScroll: true }); }); + // Caret moves without a value change (click / arrow keys) can enable or invalidate the ghost. + qEl.addEventListener("keyup", updateGhost); + qEl.addEventListener("click", updateGhost); // Windowed reveal: as the list scrolls, materialize the next window (append-only, no rebuild) so the // DOM stays bounded to what's near the viewport. Guarded to the commands phase (tabs/percent are tiny). listEl.addEventListener("scroll", function () { @@ -1250,6 +1394,19 @@ function OnInit() { else flashHint(T("sd_no_wiki", "No wiki page for this action")); return; } + // Accept the inline completion: append the suggested word's suffix. Text only - the list + // selection is left where it is; Enter still runs the highlighted result. Shift+Tab is left + // alone so keyboard focus traversal still works. + if (e.key === "Tab" && phase === "commands" && activeCompletion && + !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) { + e.preventDefault(); + qEl.value += activeCompletion.suffix; + query = qEl.value; + syncClearButton(); + render({ resize: true, resetScroll: true }); + qEl.focus(); + return; + } // Pin/unpin the highlighted action: Ctrl/Cmd+B. Commands phase only (tabs/percent aren't pinnable). if (phase === "commands" && (e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === "b") { diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 4c2f606843..ea9517cfa6 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -117,6 +117,55 @@ const negativePool = [ assert.equal(ctx.searchActions(negativePool, "ornt").length, 1, "a low-score fuzzy match is not mistaken for no match"); +// Multi-token cross-field search: each whitespace-separated word must match SOME searchable field, +// but different words may match different fields. "inner" is the title while "speed" and +// "acceleration" live in the source breadcrumb, so the query as a whole is never contiguous in one +// field - the old single-needle match found nothing for this. +const crossPool = [ + { id: "acc", title: "Inner wall", source: "Process : Speed : Acceleration", group: "", input: "" }, + { id: "spd", title: "Inner wall", source: "Process : Speed : Other layers speed", group: "", input: "" }, + { id: "other", title: "Outer wall", source: "Process : Quality : Walls", group: "", input: "" } +]; +assert.deepEqual( + ctx.searchActions(crossPool, "speed acceleration inner").map(function (a) { return a.id; }), + ["acc"], + "words may match different fields and every word is required" +); +assert.deepEqual( + ctx.searchActions(crossPool, "speed inner").map(function (a) { return a.id; }).sort(), + ["acc", "spd"], + "a two-word title+source query matches every setting under Speed" +); +assert.deepEqual( + ctx.searchActions(crossPool, "quality inner").map(function (a) { return a.id; }), + [], + "an action is dropped when any one word matches no field" +); +// Highlighting merges the per-field ranges the tokens produced. +ctx.searchActions(crossPool, "speed acceleration inner"); +assert.deepEqual(ctx.matchIndex.acc.title, [[0, 5]], "the title token highlights in the title"); +assert.deepEqual(ctx.matchIndex.acc.source, [[10, 15], [18, 30]], "each path token highlights in the breadcrumb"); + +assert.deepEqual(ctx.queryTokens(" Speed Acceleration "), ["speed", "acceleration"], + "a query splits into normalized whitespace-separated tokens"); +assert.deepEqual(ctx.queryTokens(""), [], "an empty query has no tokens"); + +// Inline completion: the LAST token is completed to a word in the top-ranked result, name first then +// breadcrumb. Pure - the caller appends `suffix`. +const compPool = [ + { id: "acc", title: "Inner wall", source: "Process : Speed : Acceleration", group: "", input: "" }, + { id: "smooth", title: "Smooth", source: "Process : Speed : Other layers speed", group: "", input: "" } +]; +assert.equal(ctx.completionFor("speed acc", ctx.searchActions(compPool, "speed acc")).suffix, "eleration", + "the last token completes to the next word in the breadcrumb"); +assert.equal(ctx.completionFor("inner w", ctx.searchActions(compPool, "inner w")).suffix, "all", + "the title is preferred over the breadcrumb for completion"); +assert.equal(ctx.completionFor("inner wall", ctx.searchActions(compPool, "inner wall")), null, + "an already-complete word has nothing to add"); +assert.equal(ctx.completionFor("", compPool), null, "an empty query has no completion"); +assert.equal(ctx.completionFor("zzz", ctx.searchActions(compPool, "zzz")), null, + "a query with no match has no completion"); + // actionCategory: a command/dynamic action's group is its category; a setting uses the top-level // source segment; every plugin shares one header; a category-less action falls back to "Other". assert.equal(ctx.actionCategory({ id: "c", group: "Help", source: "OrcaSlicer", kind: "command" }), "Help", diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index 4373ff7067..0beae39931 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -225,6 +225,7 @@ body { .plugin-search-input { flex: 1; min-width: 0; + padding: 0; background: transparent; border: 0; outline: 0; @@ -232,6 +233,40 @@ body { font: inherit; } +/* Wraps the input so the ghost completion can sit exactly at the caret. The ghost mirrors the typed + text in an invisible run (reserving its width) followed by the muted suggested suffix. */ +.plugin-search-field { + position: relative; + flex: 1; + min-width: 0; + display: flex; + align-items: center; +} + +.search-ghost { + position: absolute; + inset: 0; + display: flex; + align-items: center; + overflow: hidden; + white-space: pre; + font: inherit; + pointer-events: none; +} + +.search-ghost[hidden] { + display: none; +} + +.search-ghost-typed { + visibility: hidden; +} + +.search-ghost-suffix { + color: var(--muted, var(--orca-muted, #6b7280)); + opacity: .7; +} + .plugin-search-clear { display: inline-flex; align-items: center; From 075a84093f99c7d17d6fa830b26b186e98f73db2 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Wed, 16 Sep 2026 16:02:39 +0800 Subject: [PATCH 27/29] Fixes issue with showing hidden settings in speed dial. --- resources/web/dialog/SpeedDial/speeddial.js | 34 +++++++++------ .../web/dialog/SpeedDial/speeddial.test.js | 12 ++++++ src/slic3r/GUI/ActionRegistry.cpp | 39 +++++++++++------ src/slic3r/GUI/ActionRegistry.hpp | 3 ++ src/slic3r/GUI/OptionsGroup.cpp | 24 ++++++++--- src/slic3r/GUI/SettingsIndex.cpp | 15 ++++++- src/slic3r/GUI/SettingsIndex.hpp | 29 ++++++++++++- src/slic3r/GUI/Tab.cpp | 42 ++++++++++++++++--- src/slic3r/GUI/Tab.hpp | 10 +++++ tests/slic3rutils/test_action_source.cpp | 30 +++++++++++++ 10 files changed, 198 insertions(+), 40 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 41e180a3cd..f63e831a9e 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -114,6 +114,13 @@ function sourceNorm(a) { a._sn = NormText(a.source || "", false); return a._sn; } +// Search-only alias for the descriptive name when the title differs (e.g. "Reverse on even" vs +// "Overhang reversal"). Never rendered, so no highlight ranges. +function fullNorm(a) { + if (a._fn === undefined) + a._fn = NormText(a.full_label || "", false); + return a._fn; +} // Match one pre-normalized field vs one pre-normalized needle. Returns {score, ranges, contiguous} // when the needle is present, else null. wwRe is a compiled whole-word (\b-bounded) regex for the @@ -152,19 +159,19 @@ function tokenWordRe(token) { return new RegExp("\\b" + EscapeRegExp(token) + "\\b"); } -// One token's best match across an action's three fields, keeping the per-field ranges so the caller -// can highlight each matched word. Returns {score, title, group, source} (ranges or null per field), or -// null when no field contains the token. Score mirrors scoreFields' tiers: contiguous > fuzzy, then -// title > group > source. +// One token's best match across an action's searchable fields, keeping per-field ranges for +// highlighting. Returns {score, title, group, source}, or null if no field matches. function tokenMatch(a, token, wwRe) { var t = fieldMatchScore(titleNorm(a), token, wwRe); var g = fieldMatchScore(groupNorm(a), token, wwRe); var s = fieldMatchScore(sourceNorm(a), token, wwRe); - if (!t && !g && !s) return null; + var f = fieldMatchScore(fullNorm(a), token, wwRe); + if (!t && !g && !s && !f) return null; var score = Math.max( t ? (t.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_TITLE + t.score : -Infinity, g ? (g.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_GROUP + g.score : -Infinity, - s ? (s.contiguous ? SCORE_CONTIGUOUS : 0) + s.score : -Infinity + s ? (s.contiguous ? SCORE_CONTIGUOUS : 0) + s.score : -Infinity, + f ? (f.contiguous ? SCORE_CONTIGUOUS : 0) + f.score : -Infinity ); return { score: score, title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null }; } @@ -189,11 +196,9 @@ function mergeRanges(ranges) { return out; } -// Combine the per-field match scores into one comparable value, or null when no field matched. -// Ranking tiers, strongest first: -// tier (contiguous/perfect vs fuzzy) > field (title > group > source) > start/gaps. -// The additive weights keep every contiguous match above every fuzzy one regardless of field. -function scoreFields(t, g, s) { +// Combine per-field scores into one value, or null when nothing matched. +// Ranking: contiguous > fuzzy, then title > group > source/full alias, then start/gaps. +function scoreFields(t, g, s, f) { var best = null; function consider(m, weight) { if (!m) return; @@ -203,6 +208,7 @@ function scoreFields(t, g, s) { consider(t, SCORE_TITLE); consider(g, SCORE_GROUP); consider(s, 0); + consider(f, 0); return best; } @@ -216,6 +222,7 @@ function scoreFields(t, g, s) { // - tokens: every whitespace-separated word must match SOME field, but different words may match // different fields. This is what lets "speed acceleration inner" find "Inner wall" whose path is // "Process : Speed : Acceleration" (title + source breadcrumb together). +// full_label is searchable too but never highlighted, since it is not rendered. // A phrase match always outranks a distributed token match. function searchActions(actions, query) { var q = (query || "").trim(); @@ -239,7 +246,8 @@ function searchActions(actions, query) { var t = fieldMatchScore(titleNorm(a), searchNeedle, wwRe); var g = fieldMatchScore(groupNorm(a), searchNeedle, wwRe); var s = fieldMatchScore(sourceNorm(a), searchNeedle, wwRe); - var phrase = scoreFields(t, g, s); + var f = fieldMatchScore(fullNorm(a), searchNeedle, wwRe); + var phrase = scoreFields(t, g, s, f); var score, ranges; if (phrase !== null) { score = phrase + SCORE_PHRASE; @@ -312,7 +320,7 @@ function completionFor(query, list) { var top = (list || []).slice(0, 10); for (var i = 0; i < top.length; i++) { var a = top[i]; - var fields = [a.title, a.group, a.source]; + var fields = [a.title, a.group, a.source, a.full_label]; for (var f = 0; f < fields.length; f++) { var words = completionWords(fields[f]); for (var w = 0; w < words.length; w++) { diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index ea9517cfa6..33d7d0e17f 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -117,6 +117,18 @@ const negativePool = [ assert.equal(ctx.searchActions(negativePool, "ornt").length, 1, "a low-score fuzzy match is not mistaken for no match"); +// A setting whose displayed title is the page row label keeps the descriptive ConfigOptionDef name as +// a search-only alias, so the old wording still finds it without being shown. +const aliasPool = [ + { id: "rev", title: "Reverse on even", full_label: "Overhang reversal", source: "Process : Quality : Overhangs", group: "", input: "" } +]; +assert.deepEqual(ctx.searchActions(aliasPool, "overhang reversal").map(function (a) { return a.id; }), ["rev"], + "the descriptive full_label is searchable even though the title shows the row label"); +assert.equal(ctx.matchIndex.rev.title, null, + "an alias-only match does not highlight the displayed title"); +assert.deepEqual(ctx.searchActions(aliasPool, "reversal").map(function (a) { return a.id; }), ["rev"], + "a token that exists only in the full_label still matches"); + // Multi-token cross-field search: each whitespace-separated word must match SOME searchable field, // but different words may match different fields. "inner" is the title while "speed" and // "acceleration" live in the source breadcrumb, so the query as a whole is never contiguous in one diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index 1687e28258..880c8e1fba 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -561,10 +561,24 @@ void ActionRegistry::materialize_setting_actions() std::unordered_set<std::string> seen; for (const Search::Option& opt : options) { + // The row's live state drives both the hidden filter and the title (labels can change at + // runtime, e.g. brim_width -> "Brim ear radius"). Hidden rows are skipped, not marked seen. + Tab* tab = wxGetApp().get_tab(opt.type); + Tab::SettingRowState row; + if (tab) + row = tab->setting_row_state(opt.opt_key()); + if (!row.visible) + continue; + const std::string id = SettingAction::id_for(opt.opt_key(), opt.type); seen.insert(id); - const std::wstring label_w = opt.label_local.empty() ? opt.label : opt.label_local; + // The page draws Line::label; the descriptive ConfigOptionDef name stays a search-only alias + // ("overhang reversal" still finds "Reverse on even"). + const std::string search_label = boost::nowide::narrow(opt.label_local.empty() ? opt.label : opt.label_local); + std::string title = into_u8(Search::resolve_setting_title(from_u8(opt.display_label), row.label, row.multi)); + if (title.empty()) + title = search_label; // Eyebrow/source = the full settings path "Process : Quality : Layers" (localized). The JS // renders group || source and searches source + " " + group, so putting the whole path in @@ -575,22 +589,22 @@ void ActionRegistry::materialize_setting_actions() if (!opt.group_local.empty()) path += L" : " + opt.group_local; - // title = the option leaf name (last label segment); group stays empty so the source path - // (above) is the single display/search breadcrumb rather than being duplicated. - auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, boost::nowide::narrow(label_w), std::string(), - opt.category, boost::nowide::narrow(path), opt.mode); + // title = the label the settings row draws; group stays empty so the source path (above) is + // the single display/search breadcrumb rather than being duplicated. + auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, title, std::string(), opt.category, + boost::nowide::narrow(path), opt.mode); + if (title != search_label) + action->full_label = search_label; // Tile pictogram = the icon of the setting's own group header (e.g. Advanced -> param_advanced), // the one shown next to it in the page. Fall back to the page/category icon for groups // without one. Keys are the English titles the GUI registers. action->icon = opt.group_icon; - if (action->icon.empty() && !opt.category.empty()) { - if (Tab* tab = wxGetApp().get_tab(opt.type); tab) { - const auto& icons = tab->get_category_icon_map(); - auto it = icons.find(wxString(opt.category)); - if (it != icons.end()) - action->icon = it->second; - } + if (action->icon.empty() && !opt.category.empty() && tab) { + const auto& icons = tab->get_category_icon_map(); + auto it = icons.find(wxString(opt.category)); + if (it != icons.end()) + action->icon = it->second; } // Footer description + wiki affordance; only settings whose row declared a wiki path have one. @@ -745,6 +759,7 @@ nlohmann::json ActionRegistry::snapshot() auto action_to_json = [](const AppAction* a) { return nlohmann::json({{"id", a->id()}, {"title", a->title()}, + {"full_label", a->full_label}, {"source", a->source_name()}, {"group", a->group}, {"kind", a->kind == AppActionKind::Plugin ? "plugin" : "command"}, diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index 045de70915..b192a8ccb2 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -85,6 +85,9 @@ struct AppAction ConfigOptionMode required_mode = comSimple; // Description shown in the Speed Dial's footer strip (SettingActions: the localized tooltip). std::string tooltip; + // Search-only alias when the title differs from the descriptive ConfigOptionDef name (e.g. title + // "Reverse on even", full_label "Overhang reversal"). Empty when the two agree. + std::string full_label; // Full wiki URL, when the action has one (SettingActions whose row declared a label_path). std::string help_url; diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index d4f3f1d62f..84e6fc43c3 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -1,6 +1,7 @@ #include "OptionsGroup.hpp" #include "ConfigExceptions.hpp" #include "Plater.hpp" +#include "SettingsIndex.hpp" #include "GUI_App.hpp" #include "MainFrame.hpp" #include "OG_CustomCtrl.hpp" @@ -244,11 +245,24 @@ void OptionsGroup::append_line(const Line& line) { m_lines.emplace_back(line); - // Record each option's wiki path (Line::label_path) so the Speed Dial can offer an "open wiki" - // affordance for it. Settings tabs only; the searcher already exists by the time tabs are built. - if (m_use_custom_ctrl && !line.label_path.empty()) - for (const auto& opt : line.get_options()) - wxGetApp().sidebar().settings_index().set_path(opt.opt_id, static_cast<Preset::Type>(config_type()), line.label_path); + // Feed the searcher the row's wiki path (Line::label_path, for the Speed Dial's "open wiki" + // affordance) and the label the row actually draws, so a setting action is named like the page. + if (m_use_custom_ctrl) { + Search::SettingsIndex& index = wxGetApp().sidebar().settings_index(); + const Preset::Type type = static_cast<Preset::Type>(config_type()); + const bool multi = line.get_options().size() > 1; + for (const auto& opt : line.get_options()) { + if (!line.label_path.empty()) + index.set_path(opt.opt_id, type, line.label_path); + // Mirror the sub-label OG_CustomCtrl draws for a multi-option row, so the palette + // names each field like the page does. + const std::string& leaf_src = opt.opt.label; + const wxString leaf = (leaf_src == L_CONTEXT("Top", "Layers") || leaf_src == L_CONTEXT("Bottom", "Layers")) ? + _L_CONTEXT(leaf_src, "Layers") : + _(leaf_src); + index.set_line_label(opt.opt_id, type, Search::compose_display_label(line.label, leaf, multi)); + } + } if (line.full_width && (line.widget != nullptr || !line.get_extra_widgets().empty())) return; diff --git a/src/slic3r/GUI/SettingsIndex.cpp b/src/slic3r/GUI/SettingsIndex.cpp index 6b0a01893d..994ef496d6 100644 --- a/src/slic3r/GUI/SettingsIndex.cpp +++ b/src/slic3r/GUI/SettingsIndex.cpp @@ -55,9 +55,13 @@ static Option make_option(const std::string &key, Preset::Type type, const wxStr category = wxString::Format("%s %d", "Extruder", atoi(opt_idx.c_str()) + 1); } - return Option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(), + Option option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(), gc.group.ToStdWstring(), _(gc.group).ToStdWstring(), into_u8(gc.icon), gc.category.ToStdWstring(), - GUI::Tab::translate_category(category, type).ToStdWstring(), false, mode, tooltip, gc.path}; + GUI::Tab::translate_category(category, type).ToStdWstring(), false, mode, tooltip, gc.path, + // The settings page draws Line::label; carrying it lets the Speed Dial name a setting + // the way the page does. `label`/`label_local` stay the search-oriented name. + into_u8(gc.line_label)}; + return option; } void SettingsIndex::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode) @@ -225,5 +229,12 @@ void SettingsIndex::set_path(const std::string &opt_key, Preset::Type type, cons m_groups_and_categories[get_key(opt_key, type)].path = path; } +void SettingsIndex::set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label) +{ + if (label.IsEmpty()) + return; + m_groups_and_categories[get_key(opt_key, type)].line_label = label; +} + } // namespace Search } // namespace Slic3r diff --git a/src/slic3r/GUI/SettingsIndex.hpp b/src/slic3r/GUI/SettingsIndex.hpp index 5a2f91fa51..a31d2109e0 100644 --- a/src/slic3r/GUI/SettingsIndex.hpp +++ b/src/slic3r/GUI/SettingsIndex.hpp @@ -26,10 +26,31 @@ struct GroupAndCategory { wxString group; wxString category; - wxString icon; // icon of the group's own header, or empty - std::string path; // wiki path (Line::label_path) of the option's line, or empty + wxString icon; // icon of the group's own header, or empty + wxString line_label; // label the settings row actually draws (Line::label), or empty + std::string path; // wiki path (Line::label_path) of the option's line, or empty }; +// Title for a setting: the row label, qualified with the field leaf when the row packs several +// options (e.g. "Cool Plate \u2013 First layer"). Pure; inputs are already localized. +inline wxString compose_display_label(const wxString& line_label, const wxString& leaf_label, bool multi) +{ + if (line_label.empty()) + return leaf_label; + if (!multi || leaf_label.empty() || leaf_label == line_label) + return line_label; + return line_label + L" \u2013 " + leaf_label; // en dash separator +} + +// Title to show. A single-option row uses its live label, which can be renamed at runtime +// (brim_width -> "Brim ear radius"); otherwise fall back to the precomposed label. +inline wxString resolve_setting_title(const wxString& precomposed, const wxString& live_label, bool live_multi) +{ + if (!live_multi && !live_label.empty()) + return live_label; + return precomposed; +} + struct Option { // bool operator<(const Option& other) const { return other.label > this->label; } @@ -50,6 +71,7 @@ struct Option ConfigOptionMode mode{comSimple}; // option's visibility threshold; drives the Speed Dial's mode prompt std::string tooltip; // localized ConfigOptionDef::tooltip, or empty std::string wiki_path; // Line::label_path for the option's row, or empty + std::string display_label; // label the settings row draws (localized); empty falls back to label std::string opt_key() const; }; @@ -75,6 +97,9 @@ public: void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, const wxString &icon = wxEmptyString); void set_path(const std::string &opt_key, Preset::Type type, const std::string &path); + // Record the label the option's row draws, so the Speed Dial names a setting like the page + // (ConfigOptionDef::label/full_label is a search name, not the row text). + void set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label); const std::vector<Option> &options() const { return m_options; } const std::vector<Option> &all_options() const { return m_all_modes; } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index a3afa75b50..6f958266cc 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1736,16 +1736,46 @@ void Tab::toggle_option(const std::string& opt_key, bool toggle, int opt_index/* void Tab::toggle_line(const std::string &opt_key, bool toggle, int opt_index) { - if (!m_active_page) return; - Line *line = m_active_page->get_line(opt_key, opt_index); - if (line) line->toggle_visible = toggle; + // Apply to every page that owns the option, not just m_active_page. ConfigManipulation runs while + // each tab updates at preset load, so the Speed Dial sees the same visibility regardless of page. + for (const PageShp& page : m_pages) { + if (!page) continue; + if (Line *line = page->get_line(opt_key, opt_index)) + line->toggle_visible = toggle; + } }; void Tab::set_option_label(const std::string &opt_key, const wxString &label, int opt_index) { - if (!m_active_page) return; - Line *line = m_active_page->get_line(opt_key, opt_index); - if (line) line->set_label(label); + // Same as toggle_line: a runtime rename (brim_width -> "Brim ear radius") must reach every page + // so the Speed Dial titles the setting before the page has been shown. + for (const PageShp& page : m_pages) { + if (!page) continue; + if (Line *line = page->get_line(opt_key, opt_index)) + line->set_label(label); + } +} + +Tab::SettingRowState Tab::setting_row_state(const std::string &opt_id) const +{ + bool found = false; + for (const PageShp& page : m_pages) { + if (!page) continue; + for (const ConfigOptionsGroupShp& group : page->m_optgroups) { + if (!group) continue; + for (const Line& line : group->get_lines()) { + for (const Option& opt : line.get_options()) { + if (opt.opt_id != opt_id) + continue; + if (line.toggle_visible) // shown on any owning page is enough + return {true, line.label, line.get_options().size() > 1}; + found = true; + } + } + } + } + // Never registered on a page -> visible, but with no row label to contribute. + return {!found, wxString(), false}; } // To be called by custom widgets, load a value into a config, diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 30a06b4eb5..b2f450f9c3 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -401,6 +401,16 @@ public: void toggle_option(const std::string &opt_key, bool toggle, int opt_index = -1); void toggle_line(const std::string &opt_key, bool toggle, int opt_index = -1); // BBS: hide some line void set_option_label(const std::string &opt_key, const wxString &label, int opt_index = -1); + + // Live state of the settings row that owns an option, read from the built pages. + struct SettingRowState + { + bool visible{true}; // false when ConfigManipulation hides the row + wxString label; // Line::label the row draws (may change at runtime) + bool multi{false}; // row packs several options, so label is precomposed + }; + SettingRowState setting_row_state(const std::string &opt_id) const; + wxSizer* description_line_widget(wxWindow* parent, ogStaticText** StaticText, wxString text = wxEmptyString); bool current_preset_is_dirty() const; bool saved_preset_is_dirty() const; diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index 0eb5edde41..eec38d7291 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -2,6 +2,7 @@ #include "slic3r/GUI/ActionRegistry.hpp" #include "slic3r/GUI/NativeCommands.hpp" +#include "slic3r/GUI/SettingsIndex.hpp" #include <boost/filesystem.hpp> @@ -284,6 +285,35 @@ TEST_CASE("Native command catalog covers the Add menus", "[ActionSource][SpeedDi } } +// A setting action is named like its settings row, not the ConfigOptionDef label: the row's +// Line::label, plus the field leaf when the row packs several options. +TEST_CASE("Setting display labels mirror the settings row", "[ActionSource][SpeedDial]") +{ + using Slic3r::Search::compose_display_label; + + // Single-option row: the row label is the whole title. + CHECK(compose_display_label(L"Reverse on even", L"Reverse on even", false) == L"Reverse on even"); + // No recorded row label falls back to the field leaf. + CHECK(compose_display_label(L"", L"Outer wall", false) == L"Outer wall"); + // Multi-option row: qualify with the leaf so the plate-temperature fields are distinct. + CHECK(compose_display_label(L"Cool Plate", L"First layer", true) == wxString(L"Cool Plate \u2013 First layer")); + CHECK(compose_display_label(L"Cool Plate", L"Other layers", true) == wxString(L"Cool Plate \u2013 Other layers")); + // A leaf equal to the row label is not repeated. + CHECK(compose_display_label(L"Skirt loops", L"Skirt loops", true) == L"Skirt loops"); + + using Slic3r::Search::resolve_setting_title; + + // A single-option row's live label wins, so a runtime rename is reflected. + CHECK(resolve_setting_title(L"Brim width", L"Brim ear radius", false) == L"Brim ear radius"); + // Multi-option rows keep their precomposed "row – field" label (the leaf disambiguates them). + CHECK(resolve_setting_title(L"Cool Plate \u2013 First layer", L"Cool Plate", true) == + wxString(L"Cool Plate \u2013 First layer")); + // No live row label (option not on a built page) keeps the precomposed label. + CHECK(resolve_setting_title(L"Reverse on even", L"", false) == L"Reverse on even"); + // Neither present: empty, so the caller falls back to the descriptive label. + CHECK(resolve_setting_title(L"", L"", false).IsEmpty()); +} + // A setting whose mode is above the user's current mode must be prompted before it can be edited. // Developer settings (comDevelop) are above every non-developer mode, so they always prompt then. TEST_CASE("Settings above the current mode require a switch", "[ActionSource][SpeedDial]") From 988108c8ef238fc025b4e29b09a68743105ecfe0 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Wed, 16 Sep 2026 18:31:39 +0800 Subject: [PATCH 28/29] Fix flatpak test --- .github/workflows/build_all.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index f8d6bb8235..481c49d0a7 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -449,10 +449,13 @@ jobs: # the bounds checks are compiled in, so a stripped exe still catches them. find "$d/build_flatpak/tests" -type f -perm -u+x -exec strip --strip-unneeded {} + 2>/dev/null || true # At runtime the tests read tests/ (TEST_DATA_DIR), scripts/, and under - # resources/ the shipped profiles (PROFILES_DIR) and the printers/ maps. + # resources/ the shipped profiles (PROFILES_DIR), the printers/ maps, and + # the icon SVGs (NativeCommands icon names are checked against them). find "$d" -mindepth 1 -maxdepth 1 -type d \ ! -name tests ! -name build_flatpak ! -name scripts ! -name resources -exec rm -rf {} + - find "$d/resources" -mindepth 1 -maxdepth 1 ! -name profiles ! -name printers -exec rm -rf {} + + find "$d/resources" -mindepth 1 -maxdepth 1 ! -name profiles ! -name printers ! -name images -exec rm -rf {} + + # Only the SVGs are read; the png/ico/icns/gif assets are ~35MB of dead weight. + find "$d/resources/images" -mindepth 1 -maxdepth 1 ! -name '*.svg' -exec rm -rf {} + 2>/dev/null || true tar -cf flatpak-test-asset.tar flatpak_app "$d" - name: Upload flatpak test asset uses: actions/upload-artifact@v7 From 4ea50a33e42811bb704653e3f87fee7e9371b4ad Mon Sep 17 00:00:00 2001 From: Lam Wei Lun <weilun.lam@gmail.com> Date: Fri, 18 Sep 2026 19:07:03 +0800 Subject: [PATCH 29/29] Testing macOS fixes --- resources/web/dialog/SpeedDial/speeddial.js | 69 +++++++++++++++---- .../web/dialog/SpeedDial/speeddial.test.js | 14 ++++ resources/web/dialog/SpeedDial/style.css | 33 ++++++++- src/slic3r/GUI/ActionRegistry.cpp | 16 ++++- src/slic3r/GUI/ActionRegistry.hpp | 5 ++ src/slic3r/GUI/GUI_Utils.hpp | 2 + src/slic3r/GUI/GUI_UtilsMac.mm | 17 +++++ src/slic3r/GUI/SpeedDialDialog.cpp | 67 +++++++++++++----- src/slic3r/GUI/SpeedDialDialog.hpp | 4 +- 9 files changed, 191 insertions(+), 36 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index f63e831a9e..c5caf0d423 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -9,6 +9,7 @@ // - action.input "percent"/"tab" -> NativeCommands catalog (phases handled in activateEntry) // - action.icon SVG base name -> AppAction::icon / resources/images/<name>.svg // - action.desc/wiki -> AppAction::tooltip / help_url (footer detail strip) +// - payload.tooltip_expanded -> ActionRegistry::tooltip_expanded (persisted footer state) // - action list is frecency-sorted -> ActionRegistry::snapshot() // ---- state (populated by the C++ bridge via window.HandleStudio) ---- @@ -17,9 +18,12 @@ var FAVS = []; // [id...] var RECENTS = []; // [{id,title,source,group,kind,input,icon,mode}] - last-N launched var query = ""; var sel = { zone: "list", i: 0 }; // zone: 'list' | 'fav' -var lastResizeHeight = 0; var matchIndex = {}; +// Global tooltip expansion, seeded from C++ (persisted in the speed_dial config section). Collapsing +// hides the footer description + wiki link for every action; the arrow remains to expand again. +var TOOLTIP_EXPANDED = true; + // The user's current settings mode (from the C++ payload) plus the rank order of the modes. Each // action carries the mode it requires, so "would this need a switch?" is a rank comparison. var USER_MODE = "simple"; @@ -548,6 +552,10 @@ function actionHasWiki(a) { return !!(a && a.wiki); } // Whether the action has anything for the footer strip to show (a description or a wiki link). function actionHasDetail(a) { return !!(a && ((a.desc && a.desc.length) || a.wiki)); } +// The expand/collapse arrow is offered for actions that carry a description. Collapsing is a global +// (persisted) preference, so even a short tooltip gets the control. +function detailToggleVisible(a) { return !!(a && a.desc && a.desc.length); } + function foldLabel(s) { return String(s || "").toLowerCase().replace(/[^a-z0-9]+/g, ""); } // Title-case a source for display: "GCODE OPTIMIZER"/"iRoNiNg pRo" -> "Gcode Optimizer"/"Ironing Pro". @@ -634,7 +642,8 @@ function stateFromPayload(payload) { actions: payload.actions || [], favourites: payload.favourites || [], recent: payload.recent || [], - userMode: payload.user_mode || "simple" + userMode: payload.user_mode || "simple", + tooltipExpanded: payload.tooltip_expanded !== false }; } @@ -690,12 +699,12 @@ window.HandleStudio = function (payload) { FAVS = next.favourites; RECENTS = next.recent; USER_MODE = next.userMode; + TOOLTIP_EXPANDED = next.tooltipExpanded; // A fresh payload re-opens the main phase; C++ never rehydrates the transient phase/query state. phase = "commands"; tabOptions = []; query = ""; sel = { zone: "list", i: 0 }; - lastResizeHeight = 0; // why: builtKey caches phase|query so renderCommandsList can skip a rebuild on arrow-nav. // It survives a re-open (which never goes through exitPhase), so without a reset the cached // empty-query key would skip the rebuild and leave stale list content. @@ -1153,23 +1162,26 @@ function currentDetailAction() { return id ? byId(id) : null; } -// Footer detail strip: the selected action's description plus, when it has a wiki page, a link that -// opens it (same path as F1). Shown only when the highlighted action has something to say, so -// selecting a command with no description hides the strip. +// Footer detail strip: the selected action's description, its wiki link and the expand/collapse +// arrow. Shown whenever the highlighted action has a description or a wiki page; expanding is a +// persisted global preference, so the arrow stays available to collapse/expand every tooltip. function renderDetail() { if (!detailEl) return; var a = currentDetailAction(); + var hasDesc = detailToggleVisible(a); var show = phase === "commands" && actionHasDetail(a); detailEl.hidden = !show; detailEl.innerHTML = ""; if (!show) return; - if (a && a.desc) { + if (hasDesc && TOOLTIP_EXPANDED) { var desc = document.createElement("div"); desc.className = "detail-desc"; desc.textContent = a.desc; detailEl.appendChild(desc); } - if (a && a.wiki) { + // The wiki link is part of the expanded detail, so collapsing hides it too. An action with only a + // wiki (no description) has nothing to collapse, so its link always shows. + if (a.wiki && (!hasDesc || TOOLTIP_EXPANDED)) { var link = document.createElement("button"); link.type = "button"; link.className = "detail-wiki"; @@ -1177,6 +1189,25 @@ function renderDetail() { link.onclick = function (ev) { ev.stopPropagation(); SendMessage({ command: "open_wiki", id: a.id }); }; detailEl.appendChild(link); } + if (hasDesc) { + var toggle = document.createElement("button"); + toggle.type = "button"; + toggle.className = "detail-toggle"; + toggle.setAttribute("aria-expanded", TOOLTIP_EXPANDED ? "true" : "false"); + var label = TOOLTIP_EXPANDED ? T("sd_hide_details", "Hide details") : T("sd_show_details", "Show details"); + toggle.title = label; + toggle.setAttribute("aria-label", label); + toggle.innerHTML = '<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" ' + + 'stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + + '<polyline points="4,6 8,10 12,6"/></svg>'; + toggle.onclick = function (ev) { + ev.stopPropagation(); + TOOLTIP_EXPANDED = !TOOLTIP_EXPANDED; + SendMessage({ command: "set_tooltip_expanded", expanded: TOOLTIP_EXPANDED }); + render({ resize: true }); + }; + detailEl.appendChild(toggle); + } } // Refresh the muted inline completion shown at the end of the search field. Only offered in the @@ -1230,10 +1261,14 @@ function requestResize() { var launcher = document.querySelector(".launcher"); if (!launcher) return; - var height = Math.ceil(launcher.getBoundingClientRect().height); - if (!height || height === lastResizeHeight) + // Include any overflow (WebKit can report a -webkit-box border box a fraction short of its + // content), so the window never clips the footer's last line. + var height = Math.ceil(Math.max(launcher.getBoundingClientRect().height, launcher.scrollHeight)); + if (!height) return; - lastResizeHeight = height; + // Always (re)send rather than caching: a resize can be measured but dropped (e.g. while the + // window is being shown) and an unchanged-size cache would then suppress every retry. C++ + // SetClientSize is a no-op on an unchanged size, so this is cheap. SendMessage({ command: "resize", height: height }); }, 0); } @@ -1448,7 +1483,7 @@ function OnInit() { e.preventDefault(); sel = nextSel(sel, e.key, list.length, favs.length); // 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. render({ resize: true }); } else if (e.key === "Enter") { e.preventDefault(); @@ -1462,5 +1497,15 @@ function OnInit() { } }); + // Keep the dialog sized to the content: any reflow that lands after a render (tooltip + // expand/collapse or clamped-box settling, font metrics, list reveal) re-measures. Without this + // a later reflow left the window a few pixels short and clipped the footer's last line. + if (typeof ResizeObserver !== "undefined") { + new ResizeObserver(function () { requestResize(); }).observe(document.querySelector(".launcher")); + } + // Font metrics can swap after first layout; re-measure once they settle. + if (document.fonts && document.fonts.ready && document.fonts.ready.then) + document.fonts.ready.then(function () { requestResize(); }); + SendMessage({ command: "request_actions" }); } diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 33d7d0e17f..03099df91b 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -422,4 +422,18 @@ assert.equal(ctx.actionHasDetail({ id: "a", desc: "", wiki: false }), false, "em assert.equal(ctx.actionHasDetail({ id: "a" }), false, "an action with neither hides the footer"); assert.equal(ctx.actionHasDetail(null), false, "no selected action hides the footer"); +// detailToggleVisible: the expand/collapse arrow is offered only when there is a description to +// toggle. Collapse is a global preference, so the control stays for short tooltips too. +assert.equal(ctx.detailToggleVisible({ id: "a", desc: "Layer height" }), true, "a description offers the toggle"); +assert.equal(ctx.detailToggleVisible({ id: "a", desc: "" }), false, "an empty description offers no toggle"); +assert.equal(ctx.detailToggleVisible({ id: "a", wiki: true }), false, "a wiki-only action has nothing to collapse"); +assert.equal(ctx.detailToggleVisible({ id: "a" }), false, "an action with no description offers no toggle"); +assert.equal(ctx.detailToggleVisible(null), false, "no selected action offers no toggle"); + +// stateFromPayload: the footer expansion is a persisted global and defaults to expanded when the +// C++ payload omits it (first run / older config). +assert.equal(ctx.stateFromPayload({}).tooltipExpanded, true, "expansion defaults to true when absent"); +assert.equal(ctx.stateFromPayload({ tooltip_expanded: false }).tooltipExpanded, false, "a collapsed payload is honored"); +assert.equal(ctx.stateFromPayload({ tooltip_expanded: true }).tooltipExpanded, true, "an expanded payload is honored"); + console.log("ok"); diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index 0beae39931..3aaeddb460 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -476,8 +476,8 @@ body { text-align: center; } -/* Footer detail strip: the selected action's description plus its wiki link. Auto-sizes to the - content; the description is clamped below. */ +/* Footer detail strip: the selected action's description, its wiki link and the expand/collapse + arrow. Auto-sizes to the content; the description is clamped to 6 lines. */ .dial-detail { flex: 0 0 auto; display: flex; @@ -500,7 +500,7 @@ body { color: var(--muted, var(--orca-muted, #6b7280)); display: -webkit-box; -webkit-box-orient: vertical; - -webkit-line-clamp: 3; + -webkit-line-clamp: 6; overflow: hidden; overflow-wrap: anywhere; } @@ -520,3 +520,30 @@ body { .detail-wiki:hover { text-decoration: underline; } + +/* Expand/collapse arrow for the tooltip. Pinned right so it stays put whether or not the + description/wiki are visible; the chevron points up (collapse) when expanded. */ +.detail-toggle { + flex: 0 0 auto; + margin-left: auto; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + padding: 2px; + background: transparent; + color: var(--muted, var(--orca-muted, #6b7280)); + cursor: pointer; +} + +.detail-toggle:hover { + color: var(--text, var(--orca-fg, #1b1c1e)); +} + +.detail-toggle svg { + transition: transform .12s ease; +} + +.detail-toggle[aria-expanded="true"] svg { + transform: rotate(180deg); +} diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index 880c8e1fba..6dd9b0b542 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -726,6 +726,19 @@ void ActionRegistry::suppress_ask(const std::string& id) write_section("ask_suppressed", nlohmann::json(arr)); } +bool ActionRegistry::tooltip_expanded() const +{ + assert(wxThread::IsMain()); + const nlohmann::json j = read_section("tooltip_expanded", nlohmann::json(true)); + return j.is_boolean() ? j.get<bool>() : true; +} + +void ActionRegistry::set_tooltip_expanded(bool expanded) +{ + assert(wxThread::IsMain()); + write_section("tooltip_expanded", nlohmann::json(expanded)); +} + // ---- snapshot --------------------------------------------------------------- nlohmann::json ActionRegistry::snapshot() @@ -810,7 +823,8 @@ nlohmann::json ActionRegistry::snapshot() return {{"actions", std::move(actions)}, {"favourites", std::move(favourites)}, {"recent", std::move(recent_json)}, - {"user_mode", mode_key(wxGetApp().get_mode())}}; + {"user_mode", mode_key(wxGetApp().get_mode())}, + {"tooltip_expanded", tooltip_expanded()}}; } // ---- tab options (enumerate the MainFrame notebook's current pages) ---------- diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index b192a8ccb2..61cdbffd25 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -196,6 +196,11 @@ public: bool should_ask(const std::string& id) const; void suppress_ask(const std::string& id); + // Footer expand/collapse preference. Global (applies to every action) and persisted; absent + // means expanded, so a fresh config picks the richer default with no migration. + bool tooltip_expanded() const; + void set_tooltip_expanded(bool expanded); + // Flat, frecency-sorted snapshot for the webview: // {actions:[...], favourites:[...], recent:[...]} (recent = last-N launched by recency). nlohmann::json snapshot(); diff --git a/src/slic3r/GUI/GUI_Utils.hpp b/src/slic3r/GUI/GUI_Utils.hpp index 85790516ee..dae0305f1d 100644 --- a/src/slic3r/GUI/GUI_Utils.hpp +++ b/src/slic3r/GUI/GUI_Utils.hpp @@ -478,6 +478,8 @@ int get_dpi_for_window(const wxWindow *window); #ifdef __WXOSX__ void dataview_remove_insets(wxDataViewCtrl* dv); void staticbox_remove_margin(wxStaticBox* sb); +// Clip a top-level window (and its webview) to a rounded rect with a native layer. +void set_window_corner_radius(wxWindow* win, int radius); #endif #ifdef __WXGTK__ diff --git a/src/slic3r/GUI/GUI_UtilsMac.mm b/src/slic3r/GUI/GUI_UtilsMac.mm index 01501f6265..453229e429 100644 --- a/src/slic3r/GUI/GUI_UtilsMac.mm +++ b/src/slic3r/GUI/GUI_UtilsMac.mm @@ -1,5 +1,6 @@ #include <unistd.h> #include <sys/sysctl.h> +#import <Cocoa/Cocoa.h> #import <wx/osx/cocoa/dataview.h> #import "GUI_Utils.hpp" @@ -21,6 +22,22 @@ void staticbox_remove_margin(wxStaticBox* sb) { [nativeBox setBorderWidth:0]; } +// wxOSX SetShape only clears the window background; it cannot clip to a region. Clipping the +// window's view layer to a rounded rect is what actually rounds the opaque webview inside. +void set_window_corner_radius(wxWindow* win, int radius) { + if (!win) + return; + NSView* view = (NSView*)win->GetHandle(); + if (!view) + return; + NSWindow* window = [view window]; + [window setOpaque:NO]; + [window setBackgroundColor:[NSColor clearColor]]; + [view setWantsLayer:YES]; + [[view layer] setCornerRadius:radius]; + [[view layer] setMasksToBounds:YES]; +} + bool is_debugger_present() // Returns true if the current process is being debugged (either // running under the debugger or has a debugger attached post facto). diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index 3de0c137bb..7be8bee123 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -113,6 +113,8 @@ nlohmann::json speed_dial_ui_strings() {"sd_mode_develop", _u8L("Developer")}, {"sd_wiki_f1", _u8L("Wiki (F1)")}, {"sd_no_wiki", _u8L("No wiki page for this action")}, + {"sd_show_details", _u8L("Show details")}, + {"sd_hide_details", _u8L("Hide details")}, }; } @@ -148,8 +150,8 @@ SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent) // the page inside the fixed-size popup. No-op on the other backends (wxWidgets 3.3 base virtual). if (wxWebView* wv = browser()) wv->EnableBrowserAcceleratorKeys(false); - // Re-cut the shape region whenever layout changes the client size; SetShape itself - // does not generate size events, so this cannot recurse. + // Re-cut the shape whenever layout changes the client size. wxOSX SetShape resizes the + // NSWindow, which fires this synchronously; apply_rounded_shape() guards re-entry. Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { event.Skip(); apply_rounded_shape(); @@ -224,7 +226,9 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload) if (id.is_string()) ids.push_back(id.get<std::string>()); wxGetApp().action_registry().reorder_favourites(ids); - } else if (command == "run_action") + } else if (command == "set_tooltip_expanded") + wxGetApp().action_registry().set_tooltip_expanded(payload.value("expanded", true)); + else if (command == "run_action") run_action(payload.value("id", ""), payload.value("title", ""), payload.value("param", "")); else if (command == "open_wiki") open_wiki(payload.value("id", "")); @@ -259,34 +263,58 @@ void SpeedDialWebDialog::resize_to_content(int height) const int height_dip = std::max(kPopupMinHeight, std::min(height, max_dip)); SetClientSize(FromDIP(wxSize(kPopupWidth, height_dip))); Layout(); +#ifdef __WXOSX__ + // WKWebView can lag the dialog's new client size; force the viewport to match so the page is + // never painted (and clipped by the rounded layer) below the footer. + if (wxWebView* wv = browser()) { + const wxSize client = GetClientSize(); + if (wv->GetSize() != client) + wv->SetSize(client); + } +#endif apply_rounded_shape(); } -// Rounded corners: the webview paints an opaque rectangle, so round the whole top-level window -// with a shape region (same mask trick as FilamentPickerDialog). Binary edges, no anti-aliasing. +// Rounded corners: the webview paints an opaque rectangle, so round the whole top-level window. +// GTK/MSW use a shape region (same mask trick as FilamentPickerDialog, binary edges, no +// anti-aliasing); macOS clips the native view layer instead, since SetShape cannot shape there. void SpeedDialWebDialog::apply_rounded_shape() { + // wxOSX SetShape resizes the NSWindow (setContentSize 10x10 then back), which synchronously + // fires wxEVT_SIZE -> apply_rounded_shape() -> SetShape() and recurses until the stack + // overflows. GTK/MSW set a region without resizing, so they are unaffected. + if (m_applying_shape) + return; + // BORDER_NONE means the window is all client area, so the client size is the shape size. const wxSize size = GetClientSize(); if (size.GetWidth() <= 0 || size.GetHeight() <= 0) return; + m_applying_shape = true; + +#ifdef __WXOSX__ + // wxOSX ignores the region (it only clears the window background), so round the native view. + set_window_corner_radius(this, FromDIP(m_corner_radius)); +#else m_shape_bmp.Create(size.GetWidth(), size.GetHeight(), 32); - if (!m_shape_bmp.IsOk()) - return; + if (m_shape_bmp.IsOk()) { + wxMemoryDC dc; + dc.SelectObject(m_shape_bmp); + dc.SetBackground(wxBrush(wxColour(0, 0, 0))); + dc.Clear(); + dc.SetBrush(wxBrush(wxColour(255, 255, 255, 255))); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRoundedRectangle(0, 0, size.GetWidth(), size.GetHeight(), FromDIP(m_corner_radius)); + dc.SelectObject(wxNullBitmap); - wxMemoryDC dc; - dc.SelectObject(m_shape_bmp); - dc.SetBackground(wxBrush(wxColour(0, 0, 0))); - dc.Clear(); - dc.SetBrush(wxBrush(wxColour(255, 255, 255, 255))); - dc.SetPen(*wxTRANSPARENT_PEN); - dc.DrawRoundedRectangle(0, 0, size.GetWidth(), size.GetHeight(), FromDIP(m_corner_radius)); - dc.SelectObject(wxNullBitmap); + wxRegion region(m_shape_bmp, wxColour(0, 0, 0)); + if (region.IsOk()) + SetShape(region); + } +#endif - wxRegion region(m_shape_bmp, wxColour(0, 0, 0)); - if (region.IsOk()) - SetShape(region); + m_applying_shape = false; } void SpeedDialWebDialog::on_dpi_changed(const wxRect&) @@ -379,7 +407,8 @@ void SpeedDialWebDialog::send_actions() {"actions", std::move(snap["actions"])}, {"favourites", std::move(snap["favourites"])}, {"recent", std::move(snap["recent"])}, - {"user_mode", std::move(snap["user_mode"])}}); + {"user_mode", std::move(snap["user_mode"])}, + {"tooltip_expanded", std::move(snap["tooltip_expanded"])}}); } }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/SpeedDialDialog.hpp b/src/slic3r/GUI/SpeedDialDialog.hpp index e7a4060e16..8ae745b446 100644 --- a/src/slic3r/GUI/SpeedDialDialog.hpp +++ b/src/slic3r/GUI/SpeedDialDialog.hpp @@ -31,9 +31,11 @@ private: void on_dpi_changed(const wxRect& suggested_rect) override; bool m_page_ready{false}; - // Rounded corners via a window shape region, since the webview itself is opaque. + // Rounded corners (shape region on GTK/MSW, native layer on macOS), since the webview is opaque. int m_corner_radius{7}; wxBitmap m_shape_bmp; + // wxOSX SetShape resizes the window, which re-enters apply_rounded_shape() through wxEVT_SIZE. + bool m_applying_shape{false}; // Guards the CallAfter in on_script_message across dialog destruction, same as // PluginsDialog::m_alive (PluginsDialog.hpp:249). std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);