🎉 Add customizable shortcuts (#10237)

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
This commit is contained in:
Eva Marco 2026-07-24 10:36:32 +02:00 committed by GitHub
parent caacd482af
commit 5de06e8f77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
87 changed files with 3950 additions and 878 deletions

View File

@ -59,7 +59,9 @@
[:release-notes-viewed {:optional true} [:release-notes-viewed {:optional true}
[::sm/text {:max 100}]] [::sm/text {:max 100}]]
[:notifications {:optional true} schema:props-notifications] [:notifications {:optional true} schema:props-notifications]
[:workspace-visited {:optional true} ::sm/boolean]]) [:workspace-visited {:optional true} ::sm/boolean]
[:custom-shortcuts {:optional true}
[:map-of {:gen/max 10} :keyword [:map-of :keyword :string]]]])
(def schema:profile (def schema:profile
[:map {:title "Profile"} [:map {:title "Profile"}

View File

@ -175,7 +175,10 @@
:mcp :mcp
:background-blur :background-blur
:available-viewer-wasm :available-viewer-wasm
:stroke-per-side}) :stroke-path
:stroke-per-side
:custom-shortcuts})
(def all-flags (def all-flags
(set/union email login varia)) (set/union email login varia))

View File

@ -0,0 +1,21 @@
{
"~:v2-info-shown": true,
"~:newsletter-updates": false,
"~:onboarding-viewed": true,
"~:onboarding-questions": {
"~:role": "ux",
"~:start-with": "wireframing",
"~:expected-use": "personal",
"~:experience-design-tool": "sketch"
},
"~:onboarding-questions-answered": true,
"~:workspace-visited": true,
"~:renderer": "~:wasm",
"~:viewed-tutorial?": false,
"~:viewed-walkthrough?": false,
"~:release-notes-viewed": "0.0",
"~:nudge": {
"~:big": 10,
"~:small": 1
}
}

View File

@ -0,0 +1,27 @@
{
"~:v2-info-shown": true,
"~:newsletter-updates": false,
"~:onboarding-viewed": true,
"~:onboarding-questions": {
"~:role": "ux",
"~:start-with": "wireframing",
"~:expected-use": "personal",
"~:experience-design-tool": "sketch"
},
"~:onboarding-questions-answered": true,
"~:workspace-visited": true,
"~:custom-shortcuts": {
"~:workspace": {
"~:align-bottom": "ctrl+y",
"~:redo": ""
}
},
"~:renderer": "~:wasm",
"~:viewed-tutorial?": false,
"~:viewed-walkthrough?": false,
"~:release-notes-viewed": "0.0",
"~:nudge": {
"~:big": 10,
"~:small": 1
}
}

View File

@ -0,0 +1,27 @@
{
"~:v2-info-shown": true,
"~:newsletter-updates": false,
"~:onboarding-viewed": true,
"~:onboarding-questions": {
"~:role": "ux",
"~:start-with": "wireframing",
"~:expected-use": "personal",
"~:experience-design-tool": "sketch"
},
"~:onboarding-questions-answered": true,
"~:workspace-visited": true,
"~:custom-shortcuts": {
"~:workspace": {
"~:align-bottom": "ctrl+y",
"~:redo": ""
}
},
"~:renderer": "~:wasm",
"~:viewed-tutorial?": false,
"~:viewed-walkthrough?": false,
"~:release-notes-viewed": "0.0",
"~:nudge": {
"~:big": 10,
"~:small": 1
}
}

View File

@ -0,0 +1,328 @@
import { expect } from "@playwright/test";
import { readFile } from "node:fs/promises";
import { BaseWebSocketPage } from "./BaseWebSocketPage";
function decodeKeyword(value) {
if (typeof value === "string" && value.startsWith("~:")) {
return value.slice(2);
}
if (typeof value === "string" && value.startsWith("~$")) {
return value.slice(2);
}
return value;
}
function decodeTransit(data) {
if (Array.isArray(data)) {
if (data[0] === "^ ") {
const result = {};
for (let i = 1; i < data.length; i += 2) {
const key = decodeTransit(data[i]);
const value = decodeTransit(data[i + 1]);
result[key] = value;
}
return result;
}
return data.map(decodeTransit);
}
if (data !== null && typeof data === "object") {
const result = {};
for (const [key, value] of Object.entries(data)) {
result[decodeKeyword(key)] = decodeTransit(value);
}
return result;
}
return decodeKeyword(data);
}
function encodeKeyword(key) {
return `~:${key}`;
}
function encodeTransit(data) {
if (data === null || typeof data === "boolean" || typeof data === "number") {
return data;
}
if (typeof data === "string") {
return data;
}
if (Array.isArray(data)) {
return data.map(encodeTransit);
}
const result = {};
for (const [key, value] of Object.entries(data)) {
result[encodeKeyword(key)] = encodeTransit(value);
}
return result;
}
function decodeRequestBody(body) {
try {
return decodeTransit(JSON.parse(body));
} catch {
return null;
}
}
export class ShortcutsPage extends BaseWebSocketPage {
static async init(page) {
await super.init(page);
const profileText = await readFile(
"playwright/data/logged-in-user/get-profile-logged-in.json",
"utf-8",
);
const baseProfile = decodeTransit(JSON.parse(profileText));
let customShortcuts = null;
await page.route("**/api/main/methods/get-profile", (route) => {
const profile = JSON.parse(JSON.stringify(baseProfile));
if (customShortcuts) {
profile.props["custom-shortcuts"] = customShortcuts;
}
route.fulfill({
status: 200,
contentType: "application/transit+json",
body: JSON.stringify(encodeTransit(profile)),
});
});
await page.route(
"**/api/main/methods/update-profile-props",
async (route, request) => {
const decoded = decodeRequestBody(request.postData() ?? "{}");
if (decoded?.props && "custom-shortcuts" in decoded.props) {
customShortcuts = decoded.props["custom-shortcuts"];
}
route.fulfill({
status: 200,
contentType: "application/transit+json",
body: "{}",
});
},
);
await super.mockRPC(
page,
"get-teams",
"logged-in-user/get-teams-default.json",
);
}
static async initWithShortcuts(page) {
await super.init(page);
await super.mockRPCs(page, {
"get-profile": "logged-in-user/get-profile-logged-in.json",
"get-teams": "logged-in-user/get-teams-default.json",
"update-profile-props":
"logged-in-user/update-profile-with-shortcuts.json",
});
}
static async initWithNoShortcuts(page) {
await super.init(page);
await super.mockRPCs(page, {
"get-profile": "logged-in-user/get-profile-no-shortcuts.json",
"get-teams": "logged-in-user/get-teams-default.json",
"update-profile-props":
"logged-in-user/update-profile-with-shortcuts.json",
});
}
constructor(page) {
super(page);
this.shortcutsSection = page.getByRole("region", { name: /shortcuts/i });
this.searchInput = page.getByRole("textbox", { name: /shortcuts/i });
this.allTab = page.getByRole("tab", { name: "All" });
this.personalizedTab = page.getByRole("tab", { name: "Personalized" });
this.disabledTab = page.getByRole("tab", { name: "Disabled" });
this.restoreAllButton = page.getByRole("button", {
name: /restore all/i,
});
this.importExportButton = page.getByRole("button", {
name: /import\/export/i,
});
this.fileInput = page.locator('input[type="file"]');
}
async goToShortcuts() {
await this.page.goto("#/settings/shortcuts");
await expect(this.shortcutsSection).toBeVisible();
}
async searchForShortcut(term) {
await this.searchInput.fill(term);
}
async clearSearch() {
await this.searchInput.clear();
}
async clickTab(tabName) {
const tab = this.page.getByRole("tab", { name: tabName });
await tab.click();
}
async getShortcutRow(shortcutName) {
return this.page.getByRole("listitem", {
name: new RegExp(`^${shortcutName}$`, "i"),
includeHidden: true,
});
}
async expandSubsection(subsectionName) {
const subsectionButton = this.page.getByRole("button", {
name: subsectionName,
});
await subsectionButton.click();
}
async clickEditShortcut(shortcutName) {
const button = this.page.getByRole("button", {
name: new RegExp(`Edit ${shortcutName}`, "i"),
});
await button.click();
const recordingArea = this.page.getByText("Press the key combination");
await expect(recordingArea).toBeVisible();
await recordingArea.focus();
}
async pressKey(key) {
await this.page.keyboard.press(key);
}
async pressKeyCombo(keys) {
for (const key of keys) {
await this.page.keyboard.down(key);
}
for (const key of [...keys].reverse()) {
await this.page.keyboard.up(key);
}
}
async saveShortcut() {
const saveButton = this.page.getByRole("button", { name: /save/i });
await saveButton.click();
}
async cancelEdit() {
const cancelButton = this.page.getByRole("button", { name: /cancel/i });
await cancelButton.click();
}
async resetShortcut(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
const resetButton = row.getByRole("button", { name: /reset/i });
await resetButton.click();
}
async disableShortcut(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
await row
.getByRole("button", { name: new RegExp(`Edit ${shortcutName}`, "i") })
.click();
const disableButton = this.page.getByRole("button", { name: /disable/i });
await disableButton.click();
}
async expectShortcutCustomized(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
await expect(row).toHaveAttribute("data-customized", "true");
}
async expectShortcutNotCustomized(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
await expect(row).not.toHaveAttribute("data-customized", "true");
}
async expectShortcutHasConflict(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
await expect(row).toHaveAttribute("data-conflict", "true");
}
async expectShortcutVisible(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
await expect(row).toBeVisible();
}
async expectShortcutHidden(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
await expect(row).not.toBeVisible();
}
async restoreAllShortcuts() {
await this.restoreAllButton.click();
const confirmButton = this.page.getByRole("button", {
name: "Restore",
exact: true,
});
await confirmButton.click();
}
async exportShortcuts() {
await this.importExportButton.click();
const exportButton = this.page.getByRole("menuitem", { name: /export/i });
await exportButton.click();
}
async importShortcuts(jsonData) {
await this.importExportButton.click();
const importButton = this.page.getByRole("menuitem", { name: /import$/i });
await importButton.click();
const buffer = Buffer.from(JSON.stringify(jsonData), "utf-8");
await this.fileInput.setInputFiles({
name: "shortcuts.json",
mimeType: "application/json",
buffer,
});
}
async importShortcutsRaw(content) {
await this.importExportButton.click();
const importButton = this.page.getByRole("menuitem", { name: /import$/i });
await importButton.click();
const data =
typeof content === "string" ? content : JSON.stringify(content);
const buffer = Buffer.from(data, "utf-8");
await this.fileInput.setInputFiles({
name: "shortcuts.json",
mimeType: "application/json",
buffer,
});
}
async getExportedJson() {
const [download] = await Promise.all([
this.page.waitForEvent("download"),
this.exportShortcuts(),
]);
expect(download.suggestedFilename()).toBe("penpot-shortcuts.json");
const path = await download.path();
const content = await readFile(path, "utf-8");
return JSON.parse(content);
}
async expectShortcutDisabled(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
await expect(row).toHaveAttribute("data-customized", "true");
await expect(row.locator("use[href*='broken-link']")).toBeVisible();
}
}
export default ShortcutsPage;

View File

@ -0,0 +1,323 @@
import { test, expect } from "@playwright/test";
import ShortcutsPage from "../pages/ShortcutsPage";
const customShortcutsFlag = "enable-custom-shortcuts";
test.beforeEach(async ({ page }) => {
await ShortcutsPage.init(page);
await ShortcutsPage.mockConfigFlags(page, [customShortcutsFlag]);
});
test.describe("Shortcuts Settings Page", () => {
test("Shortcuts page loads correctly", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await expect(shortcutsPage.shortcutsSection).toBeVisible();
await expect(shortcutsPage.searchInput).toBeVisible();
});
test("Tabs are visible and clickable", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await expect(shortcutsPage.allTab).toBeVisible();
await expect(shortcutsPage.personalizedTab).toBeVisible();
await expect(shortcutsPage.disabledTab).toBeVisible();
await shortcutsPage.clickTab("Personalized");
await expect(shortcutsPage.personalizedTab).toHaveAttribute(
"aria-selected",
"true",
);
const personalizedPlacehonder = page.getByText(/Head to All to start/i);
await expect(personalizedPlacehonder).toBeVisible();
await shortcutsPage.clickTab("Disabled");
await expect(shortcutsPage.disabledTab).toHaveAttribute(
"aria-selected",
"true",
);
const disabledPlaceholder = page.getByText(/There are not disabled/i);
await expect(disabledPlaceholder).toBeVisible();
await shortcutsPage.clickTab("All");
await expect(shortcutsPage.allTab).toHaveAttribute("aria-selected", "true");
});
});
test.describe("Shortcut Customization", () => {
test("User can edit a shortcut", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
// Expand subsection
await shortcutsPage.expandSubsection("Alignment");
// Start edition of the shortcut
await shortcutsPage.clickEditShortcut("Align bottom");
// Press a new key combination
await shortcutsPage.pressKey("Control+y");
// Save the shortcut
await shortcutsPage.saveShortcut();
// Verify the shortcut is now customized
await shortcutsPage.expectShortcutCustomized("Align bottom");
});
});
test.describe("Shortcut Import", () => {
test("Import valid custom shortcuts", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.importShortcuts({
workspace: { "align-bottom": "ctrl+y" },
});
await shortcutsPage.expectShortcutCustomized("Align bottom");
await shortcutsPage.searchForShortcut("Align bottom");
await shortcutsPage.clickTab("Personalized");
await shortcutsPage.expectShortcutVisible("Align bottom");
});
test("Import invalid JSON syntax", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.importShortcutsRaw("{invalid");
await expect(
page.getByRole("alert").filter({ hasText: /Invalid data/i }),
).toBeVisible();
});
test("Import valid JSON with invalid schema", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.importShortcuts({
workspace: { "unknown-shortcut": "ctrl+y" },
});
await expect(
page.getByRole("alert").filter({ hasText: /Invalid data/i }),
).toBeVisible();
});
test("Import JSON without workspace context accepted", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.importShortcuts({
dashboard: { "toggle-theme": "alt+m" },
});
await expect(
page.getByRole("alert").filter({ hasText: /Invalid data/i }),
).not.toBeVisible();
});
test("Import JSON with unsupported shortcut format", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.importShortcuts({
workspace: { escape: 123 },
});
await expect(
page.getByRole("alert").filter({ hasText: /Invalid data/i }),
).toBeVisible();
});
test("Import conflicting shortcuts", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.importShortcuts({
workspace: {
"align-bottom": "alt+a",
},
});
await shortcutsPage.expectShortcutCustomized("Align bottom");
await shortcutsPage.expandSubsection("Alignment");
await shortcutsPage.expectShortcutDisabled("Align left");
const exported = await shortcutsPage.getExportedJson();
expect(exported.workspace).toMatchObject({
"align-bottom": "alt+a",
"align-left": "",
});
});
});
test.describe("Shortcut Export", () => {
test("Export customized shortcuts", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.expandSubsection("Alignment");
await shortcutsPage.clickEditShortcut("Align bottom");
await shortcutsPage.pressKey("Control+y");
await shortcutsPage.saveShortcut();
await shortcutsPage.expectShortcutCustomized("Align bottom");
const exported = await shortcutsPage.getExportedJson();
expect(exported).toHaveProperty("workspace");
expect(exported.workspace).toMatchObject({
"align-bottom": "ctrl+y",
});
});
test("Export includes disabled shortcuts", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.importShortcuts({
workspace: { "align-bottom": "" },
});
await shortcutsPage.searchForShortcut("Align bottom");
await shortcutsPage.expectShortcutDisabled("Align bottom");
const exported = await shortcutsPage.getExportedJson();
expect(exported.workspace).toMatchObject({
"align-bottom": "",
});
});
});
test.describe("Shortcut Conflict Detection", () => {
test("Detects conflict and disables old shortcut on save", async ({
page,
}) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.expandSubsection("Alignment");
await shortcutsPage.clickEditShortcut("Align bottom");
await shortcutsPage.pressKey("Alt+a");
await expect(
page.getByRole("alert").filter({ hasText: /Combination assigned to/i }),
).toBeVisible();
await shortcutsPage.saveShortcut();
await shortcutsPage.expectShortcutCustomized("Align bottom");
await shortcutsPage.expectShortcutDisabled("Align left");
const exported = await shortcutsPage.getExportedJson();
expect(exported.workspace).toMatchObject({
"align-bottom": "alt+a",
"align-left": "",
});
});
});
test.describe("Shortcut Reset", () => {
test("Reset after import restores defaults", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.importShortcuts({
workspace: { "align-bottom": "ctrl+y" },
});
await shortcutsPage.expectShortcutCustomized("Align bottom");
await shortcutsPage.restoreAllShortcuts();
await shortcutsPage.expectShortcutNotCustomized("Align bottom");
});
});
test.describe("Shortcut Persistence", () => {
test("Custom shortcuts persist after reload", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.expandSubsection("Alignment");
await shortcutsPage.clickEditShortcut("Align bottom");
await shortcutsPage.pressKey("Control+y");
await shortcutsPage.saveShortcut();
await shortcutsPage.expectShortcutCustomized("Align bottom");
await page.reload();
await shortcutsPage.goToShortcuts();
await shortcutsPage.expectShortcutCustomized("Align bottom");
});
});
test.describe("Cancel Shortcut Editing", () => {
test("Cancel editing does not save changes", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.expandSubsection("Alignment");
await shortcutsPage.clickEditShortcut("Align bottom");
await shortcutsPage.pressKey("Control+y");
await shortcutsPage.cancelEdit();
await shortcutsPage.expectShortcutNotCustomized("Align bottom");
});
});
test.describe("Duplicate Shortcut Prevention", () => {
test("Assigning same shortcut to two actions shows conflict and disables first", async ({
page,
}) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.expandSubsection("Alignment");
await shortcutsPage.clickEditShortcut("Align bottom");
await shortcutsPage.pressKey("Control+y");
await shortcutsPage.saveShortcut();
await shortcutsPage.expectShortcutCustomized("Align bottom");
await shortcutsPage.clickEditShortcut("Align left");
await shortcutsPage.pressKey("Control+y");
await expect(
page.getByRole("alert").filter({ hasText: /Combination assigned to/i }),
).toBeVisible();
await shortcutsPage.saveShortcut();
await shortcutsPage.expectShortcutCustomized("Align left");
const exported = await shortcutsPage.getExportedJson();
expect(exported.workspace).toMatchObject({
"align-left": "ctrl+y",
"align-bottom": "",
});
});
});
test.describe("Shortcut Round-Trip", () => {
test("Export, reset, import restores configuration", async ({ page }) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.expandSubsection("Alignment");
await shortcutsPage.clickEditShortcut("Align bottom");
await shortcutsPage.pressKey("Control+y");
await shortcutsPage.saveShortcut();
await shortcutsPage.expectShortcutCustomized("Align bottom");
const exported = await shortcutsPage.getExportedJson();
await shortcutsPage.restoreAllShortcuts();
await shortcutsPage.expectShortcutNotCustomized("Align bottom");
await shortcutsPage.importShortcuts(exported);
await shortcutsPage.expectShortcutCustomized("Align bottom");
const reExported = await shortcutsPage.getExportedJson();
expect(reExported).toEqual(exported);
});
});

View File

@ -17,31 +17,36 @@
(def shortcuts (def shortcuts
{:toggle-theme {:tooltip (ds/alt "M") {:toggle-theme {:tooltip (ds/alt "M")
:command (ds/a-mod "m") :command (ds/a-mod "m")
:subsections [:general-dashboard] :section [:dashboard]
:subsections [:generic]
:fn #(st/emit! (with-meta (du/toggle-theme) :fn #(st/emit! (with-meta (du/toggle-theme)
{::ev/origin "dashboard:shortcuts"}))}}) {::ev/origin "dashboard:shortcuts"}))}})
(def shortcuts-sidebar-navigation (def shortcuts-sidebar-navigation
{:go-to-drafts {:tooltip "G D" {:go-to-drafts {:tooltip "G D"
:command "g d" :command "g d"
:section [:dashboard]
:subsections [:navigation-dashboard] :subsections [:navigation-dashboard]
:fn #(st/emit! (dcm/go-to-dashboard-files :project-id :default))} :fn #(st/emit! (dcm/go-to-dashboard-files :project-id :default))}
:go-to-libs {:tooltip "G L" :go-to-libs {:tooltip "G L"
:command "g l" :command "g l"
:section [:dashboard]
:subsections [:navigation-dashboard] :subsections [:navigation-dashboard]
:fn #(st/emit! (dcm/go-to-dashboard-libraries))}}) :fn #(st/emit! (dcm/go-to-dashboard-libraries))}})
(def shortcut-search (def shortcut-search
{:go-to-search {:tooltip (ds/meta "F") {:go-to-search {:tooltip (ds/meta "F")
:command (ds/c-mod "f") :command (ds/c-mod "f")
:section [:dashboard]
:subsections [:navigation-dashboard] :subsections [:navigation-dashboard]
:fn #(st/emit! (dcm/go-to-dashboard-search))}}) :fn #(st/emit! (dcm/go-to-dashboard-search))}})
(def shortcut-create-new-project (def shortcut-create-new-project
{:create-new-project {:tooltip "+" {:create-new-project {:tooltip "+"
:command "+" :command "+"
:subsections [:general-dashboard] :section [:dashboard]
:subsections [:generic]
:fn #(st/emit! (dd/create-element))}}) :fn #(st/emit! (dd/create-element))}})
;; Shortcuts combinations for files, drafts, libraries and fonts sections ;; Shortcuts combinations for files, drafts, libraries and fonts sections

View File

@ -0,0 +1,77 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.data.dashboard.shortcuts.customize
(:require
[app.main.data.event :as ev]
[app.main.data.profile :as du]
[app.main.data.shortcuts :as ds]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn set-custom-shortcut
[shortcut-key new-command conflicting-key group-key]
(ptk/reify ::set-custom-shortcut
ptk/WatchEvent
(watch [_ state _]
(let [current-customs (get-in state [:profile :props :custom-shortcuts] {})
group-map (get current-customs group-key)
group-map (if (map? group-map) group-map {})
group-map (assoc group-map shortcut-key new-command)
;; clear conflicting shortcut in the same group
group-map (if conflicting-key
(assoc group-map conflicting-key "")
group-map)
new-customs (assoc current-customs group-key group-map)]
(rx/of
(ev/event {::ev/name "set-custom-shortcut"})
(du/update-profile-props {:custom-shortcuts new-customs})
(ds/rebind-shortcuts))))))
(defn reset-custom-shortcut
[shortcut-key default-command group-key]
(ptk/reify ::reset-custom-shortcut
ptk/WatchEvent
(watch [_ state _]
(let [current-customs (get-in state [:profile :props :custom-shortcuts] {})
group-map (get current-customs group-key)
group-map (if (map? group-map) group-map {})
;; Find any action that is currently using the default command as its custom shortcut
owner-key (when (and default-command (not= default-command ""))
(some (fn [[k v]]
(when (if (vector? default-command)
(some #{v} default-command)
(= v default-command))
k))
group-map))
;; Remove the shortcut being reset
group-map (dissoc group-map shortcut-key)
;; Disable the owner that was using the default command
group-map (if (and owner-key (not= owner-key shortcut-key))
(assoc group-map owner-key "")
group-map)
new-customs (if (empty? group-map)
(dissoc current-customs group-key)
(assoc current-customs group-key group-map))]
(rx/of
(ev/event {::ev/name "reset-custom-shortcut"})
(du/update-profile-props {:custom-shortcuts new-customs})
(ds/rebind-shortcuts))))))
(defn reset-all-custom-shortcuts
[]
(ptk/reify ::reset-all-custom-shortcuts
ptk/WatchEvent
(watch [_ _ _]
(rx/of (ev/event {::ev/name "reset-all-custom-shortcuts"})
(du/update-profile-props {:custom-shortcuts {}})
(ds/rebind-shortcuts)))))

View File

@ -121,6 +121,61 @@
[sc] [sc]
(str/split sc #"\+| ")))) (str/split sc #"\+| "))))
(defn command->tooltip
"Converts a Mousetrap command string (e.g. \"command+shift+z\") to
a human-readable display string in the same format used by :tooltip
fields (e.g. \"⌘⇧Z\" on macOS or \"Ctrl+Shift+Z\" on Windows).
Returns nil for empty or unbound commands."
[command]
(when (and command (not= command ""))
(let [is-macos? (cf/check-platform? :macos)
parts (str/split command #"\+")
display-part (fn [p]
(case p
"ctrl" (if is-macos? mac-control "Ctrl+")
"command" (if is-macos? mac-command "Ctrl+")
"alt" (if is-macos? mac-option "Alt+")
"shift" (if is-macos? mac-shift "Shift+")
"up" up-arrow
"down" down-arrow
"left" left-arrow
"right" right-arrow
"del" (if is-macos? mac-delete "Del")
"backspace" (if is-macos? mac-delete "Backspace")
"escape" (if is-macos? mac-esc "Escape")
"enter" (if is-macos? mac-enter "Enter")
"space" "Space"
"tab" tab
(str/upper p)))]
(str/join "" (map display-part parts)))))
(defn apply-custom-overrides
[shortcuts custom-overrides group-key]
(let [group-overrides (get custom-overrides group-key)]
(if (empty? group-overrides)
shortcuts
(reduce-kv
(fn [acc sc-key new-command]
(if (and (contains? acc sc-key)
(not (:disabled (get acc sc-key))))
(-> acc
(assoc-in [sc-key :command] new-command)
(update sc-key dissoc :show-command))
acc))
shortcuts
group-overrides))))
(defn build-command-index
[shortcuts]
(reduce-kv
(fn [acc sc-key sc-def]
(let [commands (:command sc-def)]
(if (vector? commands)
(reduce #(assoc %1 %2 sc-key) acc commands)
(assoc acc commands sc-key))))
{}
shortcuts))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Events ;; Events
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -171,17 +226,34 @@
(fnil conj (d/ordered-map))) (fnil conj (d/ordered-map)))
(defn push-shortcuts (defn push-shortcuts
[key shortcuts] [key shortcuts group-key & {:keys [merge-shortcuts]}]
(assert (keyword? key) "expected a keyword for `key`") (assert (keyword? key) "expected a keyword for `key`")
(let [shortcuts (check-shortcuts shortcuts)] (let [shortcuts (check-shortcuts shortcuts)]
(ptk/reify ::push-shortcuts (ptk/reify ::push-shortcuts
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update state :shortcuts conj* [key shortcuts])) (let [custom-overrides (get-in state [:profile :props :custom-shortcuts])
group-key (or group-key (:shortcuts-group-key state))
merge-sc (cond
(= merge-shortcuts :auto)
(let [[_ sc] (last (:shortcuts state))]
sc)
(map? merge-shortcuts)
merge-shortcuts)
effective (if merge-sc
(merge merge-sc shortcuts)
shortcuts)
effective (apply-custom-overrides effective custom-overrides group-key)]
(-> state
(update :shortcuts conj* [key effective])
(assoc :shortcuts-group-key group-key))))
ptk/EffectEvent ptk/EffectEvent
(effect [_ _ _] (effect [_ state _]
(reset! shortcuts))))) (let [[_ stored] (last (:shortcuts state))]
(reset! stored))))))
(defn pop-shortcuts (defn pop-shortcuts
[key] [key]
@ -193,5 +265,19 @@
ptk/EffectEvent ptk/EffectEvent
(effect [_ state _] (effect [_ state _]
(let [[_key shortcuts] (last (:shortcuts state))] (let [[_key shortcuts] (last (:shortcuts state))
(reset! shortcuts))))) custom-overrides (get-in state [:profile :props :custom-shortcuts])
group-key (:shortcuts-group-key state)
effective (apply-custom-overrides shortcuts custom-overrides group-key)]
(reset! effective)))))
(defn rebind-shortcuts
[]
(ptk/reify ::rebind-shortcuts
ptk/EffectEvent
(effect [_ state _]
(let [custom-overrides (get-in state [:profile :props :custom-shortcuts])
group-key (:shortcuts-group-key state)
merged (reduce merge (map val (:shortcuts state)))
effective (apply-custom-overrides merged custom-overrides group-key)]
(reset! effective)))))

View File

@ -7,6 +7,8 @@
(ns app.main.data.viewer.shortcuts (ns app.main.data.viewer.shortcuts
(:require (:require
[app.main.data.common :as dcm] [app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.profile :as du]
[app.main.data.shortcuts :as ds] [app.main.data.shortcuts :as ds]
[app.main.data.viewer :as dv] [app.main.data.viewer :as dv]
[app.main.store :as st])) [app.main.store :as st]))
@ -15,61 +17,80 @@
{:increase-zoom {:tooltip "+" {:increase-zoom {:tooltip "+"
:command "+" :command "+"
:subsections [:zoom-viewer] :subsections [:zoom-viewer]
:section [:viewer]
:fn #(st/emit! dv/increase-zoom)} :fn #(st/emit! dv/increase-zoom)}
:toggle-theme {:tooltip (ds/alt "M")
:command (ds/a-mod "m")
:section [:viewer]
:subsections [:generic]
:fn #(st/emit! (with-meta (du/toggle-theme)
{::ev/origin "viewer:shortcuts"}))}
:decrease-zoom {:tooltip "-" :decrease-zoom {:tooltip "-"
:command "-" :command "-"
:section [:viewer]
:subsections [:zoom-viewer] :subsections [:zoom-viewer]
:fn #(st/emit! dv/decrease-zoom)} :fn #(st/emit! dv/decrease-zoom)}
:select-all {:tooltip (ds/meta "A") :select-all {:tooltip (ds/meta "A")
:command (ds/c-mod "a") :command (ds/c-mod "a")
:subsections [:general-viewer] :section [:viewer]
:subsections [:generic]
:fn #(st/emit! (dv/select-all))} :fn #(st/emit! (dv/select-all))}
:reset-zoom {:tooltip (ds/shift "0") :reset-zoom {:tooltip (ds/shift "0")
:command "shift+0" :command "shift+0"
:section [:viewer]
:subsections [:zoom-viewer] :subsections [:zoom-viewer]
:fn #(st/emit! dv/reset-zoom)} :fn #(st/emit! dv/reset-zoom)}
:toggle-zoom-style {:tooltip "F" :toggle-zoom-style {:tooltip "F"
:command "f" :command "f"
:section [:viewer]
:subsections [:zoom-viewer] :subsections [:zoom-viewer]
:fn #(st/emit! dv/toggle-zoom-style)} :fn #(st/emit! dv/toggle-zoom-style)}
:toggle-fullscreen {:tooltip (ds/shift "F") :toggle-fullscreen {:tooltip (ds/shift "F")
:command ["shift+f" "alt+enter"] :command ["shift+f" "alt+enter"]
:section [:viewer]
:subsections [:zoom-viewer] :subsections [:zoom-viewer]
:fn #(st/emit! dv/toggle-fullscreen)} :fn #(st/emit! dv/toggle-fullscreen)}
:prev-frame {:tooltip ds/left-arrow :prev-frame {:tooltip ds/left-arrow
:command ["left" "up" "shift+enter" "pageup" "shift+space"] :command ["left" "up" "shift+enter" "pageup" "shift+space"]
:subsections [:general-viewer] :subsections [:generic]
:section [:viewer]
:fn #(st/emit! dv/select-prev-frame)} :fn #(st/emit! dv/select-prev-frame)}
:next-frame {:tooltip ds/right-arrow :next-frame {:tooltip ds/right-arrow
:command ["right" "down" "enter" "pagedown" "space"] :command ["right" "down" "enter" "pagedown" "space"]
:subsections [:general-viewer] :subsections [:generic]
:section [:viewer]
:fn #(st/emit! dv/select-next-frame)} :fn #(st/emit! dv/select-next-frame)}
:open-inspect {:tooltip "G I" :open-inspect {:tooltip "G I"
:command "g i" :command "g i"
:subsections [:navigation-viewer] :subsections [:navigation-viewer]
:section [:viewer]
:fn #(st/emit! (dv/go-to-section :inspect))} :fn #(st/emit! (dv/go-to-section :inspect))}
:open-comments {:tooltip "G C" :open-comments {:tooltip "G C"
:command "g c" :command "g c"
:subsections [:navigation-viewer] :subsections [:navigation-viewer]
:section [:viewer]
:fn #(st/emit! (dv/go-to-section :comments))} :fn #(st/emit! (dv/go-to-section :comments))}
:open-interactions {:tooltip "G V" :open-interactions {:tooltip "G V"
:command "g v" :command "g v"
:subsections [:navigation-viewer] :subsections [:navigation-viewer]
:section [:viewer]
:fn #(st/emit! (dv/go-to-section :interactions))} :fn #(st/emit! (dv/go-to-section :interactions))}
:open-workspace {:tooltip "G W" :open-workspace {:tooltip "G W"
:command "g w" :command "g w"
:subsections [:navigation-viewer] :subsections [:navigation-viewer]
:section [:viewer]
:fn #(st/emit! (dcm/go-to-workspace))}}) :fn #(st/emit! (dcm/go-to-workspace))}})
(defn get-tooltip [shortcut] (defn get-tooltip [shortcut]

View File

@ -69,32 +69,38 @@
{;; EDIT {;; EDIT
:undo {:tooltip (ds/meta "Z") :undo {:tooltip (ds/meta "Z")
:command (ds/c-mod "z") :command (ds/c-mod "z")
:section [:workspace]
:subsections [:edit] :subsections [:edit]
:fn #(emit-when-no-readonly dwu/undo)} :fn #(emit-when-no-readonly dwu/undo)}
:redo {:tooltip (ds/meta "Y") :redo {:tooltip (ds/meta "Y")
:command [(ds/c-mod "shift+z") (ds/c-mod "y")] :command [(ds/c-mod "shift+z") (ds/c-mod "y")]
:section [:workspace]
:subsections [:edit] :subsections [:edit]
:fn #(emit-when-no-readonly dwu/redo)} :fn #(emit-when-no-readonly dwu/redo)}
:clear-undo {:tooltip (ds/alt "Q") :clear-undo {:tooltip (ds/alt "Q")
:command "alt+q" :command "alt+q"
:subsections [:edit] :subsections [:edit]
:section [:workspace]
:fn #(emit-when-no-readonly dwu/reinitialize-undo)} :fn #(emit-when-no-readonly dwu/reinitialize-undo)}
:copy {:tooltip (ds/meta "C") :copy {:tooltip (ds/meta "C")
:command (ds/c-mod "c") :command (ds/c-mod "c")
:subsections [:edit] :subsections [:edit]
:section [:workspace]
:fn #(st/emit! (dw/copy-selected))} :fn #(st/emit! (dw/copy-selected))}
:copy-link {:tooltip (ds/shift (ds/alt "C")) :copy-link {:tooltip (ds/shift (ds/alt "C"))
:command "shift+alt+c" :command "shift+alt+c"
:subsections [:edit] :subsections [:edit]
:section [:workspace]
:fn #(st/emit! (dw/copy-link-to-clipboard))} :fn #(st/emit! (dw/copy-link-to-clipboard))}
:cut {:tooltip (ds/meta "X") :cut {:tooltip (ds/meta "X")
:command (ds/c-mod "x") :command (ds/c-mod "x")
:subsections [:edit] :subsections [:edit]
:section [:workspace]
:fn #(emit-when-no-readonly :fn #(emit-when-no-readonly
(dw/copy-selected) (dw/copy-selected)
(dw/delete-selected))} (dw/delete-selected))}
@ -103,48 +109,57 @@
:disabled true :disabled true
:command (ds/c-mod "v") :command (ds/c-mod "v")
:subsections [:edit] :subsections [:edit]
:section [:workspace]
:fn (constantly nil)} :fn (constantly nil)}
:paste-replace {:tooltip (ds/meta (ds/shift "V")) :paste-replace {:tooltip (ds/meta (ds/shift "V"))
:command (ds/c-mod "shift+v") :command (ds/c-mod "shift+v")
:subsections [:edit] :subsections [:edit]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/paste-from-clipboard {:replace? true}))} :fn #(emit-when-no-readonly (dw/paste-from-clipboard {:replace? true}))}
:copy-props {:tooltip (ds/meta (ds/alt "c")) :copy-props {:tooltip (ds/meta (ds/alt "c"))
:command (ds/c-mod "alt+c") :command (ds/c-mod "alt+c")
:subsections [:edit] :subsections [:edit]
:section [:workspace]
:fn #(st/emit! (dw/copy-selected-props))} :fn #(st/emit! (dw/copy-selected-props))}
:paste-props {:tooltip (ds/meta (ds/alt "v")) :paste-props {:tooltip (ds/meta (ds/alt "v"))
:command (ds/c-mod "alt+v") :command (ds/c-mod "alt+v")
:subsections [:edit] :subsections [:edit]
:section [:workspace]
:fn #(st/emit! (dw/paste-selected-props))} :fn #(st/emit! (dw/paste-selected-props))}
:delete {:tooltip (ds/supr) :delete {:tooltip (ds/supr)
:command ["del" "backspace"] :command ["del" "backspace"]
:subsections [:edit] :subsections [:edit]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/delete-selected))} :fn #(emit-when-no-readonly (dw/delete-selected))}
:duplicate {:tooltip (ds/meta "D") :duplicate {:tooltip (ds/meta "D")
:command (ds/c-mod "d") :command (ds/c-mod "d")
:subsections [:edit] :subsections [:edit]
:section [:workspace]
:fn #(emit-when-no-readonly (dwv/duplicate-or-add-variant))} :fn #(emit-when-no-readonly (dwv/duplicate-or-add-variant))}
:start-editing {:tooltip (ds/enter) :start-editing {:tooltip (ds/enter)
:command "enter" :command "enter"
:subsections [:edit] :subsections [:edit]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/start-editing-selected))} :fn #(emit-when-no-readonly (dw/start-editing-selected))}
:start-measure {:tooltip (ds/alt "") :start-measure {:tooltip (ds/alt "")
:command ["alt" "."] :command ["alt" "."]
:type "keydown" :type "keydown"
:subsections [:edit] :subsections [:edit]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/toggle-distances-display true))} :fn #(emit-when-no-readonly (dw/toggle-distances-display true))}
:stop-measure {:tooltip (ds/alt "") :stop-measure {:tooltip (ds/alt "")
:command ["alt" "."] :command ["alt" "."]
:type "keyup" :type "keyup"
:subsections [:edit] :subsections [:edit]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/toggle-distances-display false))} :fn #(emit-when-no-readonly (dw/toggle-distances-display false))}
:escape {:tooltip (ds/esc) :escape {:tooltip (ds/esc)
@ -153,8 +168,10 @@
:fn #(st/emit! :interrupt (dwdc/clear-drawing) (dw/deselect-all true))} :fn #(st/emit! :interrupt (dwdc/clear-drawing) (dw/deselect-all true))}
:find {:tooltip (ds/meta "F") :command (ds/c-mod "f") :subsections [:edit] :find {:tooltip (ds/meta "F") :command (ds/c-mod "f") :subsections [:edit]
:section [:workspace]
:fn #(st/emit! (dw/open-layers-search :find))} :fn #(st/emit! (dw/open-layers-search :find))}
:find-and-replace {:tooltip (ds/meta "H") :command (ds/c-mod "h") :subsections [:edit] :find-and-replace {:tooltip (ds/meta "H") :command (ds/c-mod "h") :subsections [:edit]
:section [:workspace]
:fn #(st/emit! (dw/open-layers-search :find-and-replace))} :fn #(st/emit! (dw/open-layers-search :find-and-replace))}
;; MODIFY LAYERS ;; MODIFY LAYERS
@ -162,11 +179,13 @@
:rename {:tooltip (ds/alt "N") :rename {:tooltip (ds/alt "N")
:command "alt+n" :command "alt+n"
:subsections [:edit] :subsections [:edit]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/start-rename-selected))} :fn #(emit-when-no-readonly (dw/start-rename-selected))}
:group {:tooltip (ds/meta "G") :group {:tooltip (ds/meta "G")
:command (ds/c-mod "g") :command (ds/c-mod "g")
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/group-selected))} :fn #(emit-when-no-readonly (dw/group-selected))}
:ungroup {:tooltip (ds/shift "G") :ungroup {:tooltip (ds/shift "G")
@ -177,99 +196,119 @@
:mask {:tooltip (ds/meta "M") :mask {:tooltip (ds/meta "M")
:command (ds/c-mod "m") :command (ds/c-mod "m")
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/mask-group))} :fn #(emit-when-no-readonly (dw/mask-group))}
:unmask {:tooltip (ds/meta-shift "M") :unmask {:tooltip (ds/meta-shift "M")
:command (ds/c-mod "shift+m") :command (ds/c-mod "shift+m")
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/unmask-group))} :fn #(emit-when-no-readonly (dw/unmask-group))}
:create-component-variant {:tooltip (ds/meta "K") :create-component-variant {:tooltip (ds/meta "K")
:command (ds/c-mod "k") :command (ds/c-mod "k")
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dwv/add-component-or-variant))} :fn #(emit-when-no-readonly (dwv/add-component-or-variant))}
:detach-component {:tooltip (ds/meta-shift "K") :detach-component {:tooltip (ds/meta-shift "K")
:command (ds/c-mod "shift+k") :command (ds/c-mod "shift+k")
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly dwl/detach-selected-components)} :fn #(emit-when-no-readonly dwl/detach-selected-components)}
:flip-vertical {:tooltip (ds/shift "V") :flip-vertical {:tooltip (ds/shift "V")
:command "shift+v" :command "shift+v"
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/flip-vertical-selected))} :fn #(emit-when-no-readonly (dw/flip-vertical-selected))}
:flip-horizontal {:tooltip (ds/shift "H") :flip-horizontal {:tooltip (ds/shift "H")
:command "shift+h" :command "shift+h"
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/flip-horizontal-selected))} :fn #(emit-when-no-readonly (dw/flip-horizontal-selected))}
:bring-forward {:tooltip (ds/meta ds/up-arrow) :bring-forward {:tooltip (ds/meta ds/up-arrow)
:command (ds/c-mod "up") :command (ds/c-mod "up")
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/vertical-order-selected :up))} :fn #(emit-when-no-readonly (dw/vertical-order-selected :up))}
:bring-backward {:tooltip (ds/meta ds/down-arrow) :bring-backward {:tooltip (ds/meta ds/down-arrow)
:command (ds/c-mod "down") :command (ds/c-mod "down")
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/vertical-order-selected :down))} :fn #(emit-when-no-readonly (dw/vertical-order-selected :down))}
:bring-front {:tooltip (ds/meta-shift ds/up-arrow) :bring-front {:tooltip (ds/meta-shift ds/up-arrow)
:command (ds/c-mod "shift+up") :command (ds/c-mod "shift+up")
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/vertical-order-selected :top))} :fn #(emit-when-no-readonly (dw/vertical-order-selected :top))}
:bring-back {:tooltip (ds/meta-shift ds/down-arrow) :bring-back {:tooltip (ds/meta-shift ds/down-arrow)
:command (ds/c-mod "shift+down") :command (ds/c-mod "shift+down")
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/vertical-order-selected :bottom))} :fn #(emit-when-no-readonly (dw/vertical-order-selected :bottom))}
:move-fast-up {:tooltip (ds/shift ds/up-arrow) :move-fast-up {:tooltip (ds/shift ds/up-arrow)
:command ["shift+up" "shift+alt+up"] :command ["shift+up" "shift+alt+up"]
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dwt/move-selected :up true))} :fn #(emit-when-no-readonly (dwt/move-selected :up true))}
:move-fast-down {:tooltip (ds/shift ds/down-arrow) :move-fast-down {:tooltip (ds/shift ds/down-arrow)
:command ["shift+down" "shift+alt+down"] :command ["shift+down" "shift+alt+down"]
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dwt/move-selected :down true))} :fn #(emit-when-no-readonly (dwt/move-selected :down true))}
:move-fast-right {:tooltip (ds/shift ds/right-arrow) :move-fast-right {:tooltip (ds/shift ds/right-arrow)
:command ["shift+right" "shift+alt+right"] :command ["shift+right" "shift+alt+right"]
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dwt/move-selected :right true))} :fn #(emit-when-no-readonly (dwt/move-selected :right true))}
:move-fast-left {:tooltip (ds/shift ds/left-arrow) :move-fast-left {:tooltip (ds/shift ds/left-arrow)
:command ["shift+left" "shift+alt+left"] :command ["shift+left" "shift+alt+left"]
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dwt/move-selected :left true))} :fn #(emit-when-no-readonly (dwt/move-selected :left true))}
:move-unit-up {:tooltip ds/up-arrow :move-unit-up {:tooltip ds/up-arrow
:command ["up" "alt+up"] :command ["up" "alt+up"]
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dwt/move-selected :up false))} :fn #(emit-when-no-readonly (dwt/move-selected :up false))}
:move-unit-down {:tooltip ds/down-arrow :move-unit-down {:tooltip ds/down-arrow
:command ["down" "alt+down"] :command ["down" "alt+down"]
:section [:workspace]
:subsections [:modify-layers] :subsections [:modify-layers]
:fn #(emit-when-no-readonly (dwt/move-selected :down false))} :fn #(emit-when-no-readonly (dwt/move-selected :down false))}
:move-unit-left {:tooltip ds/right-arrow :move-unit-left {:tooltip ds/right-arrow
:command ["right" "alt+right"] :command ["right" "alt+right"]
:subsections [:modify-layers] :subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dwt/move-selected :right false))} :fn #(emit-when-no-readonly (dwt/move-selected :right false))}
:move-unit-right {:tooltip ds/left-arrow :move-unit-right {:tooltip ds/left-arrow
:command ["left" "alt+left"] :command ["left" "alt+left"]
:section [:workspace]
:subsections [:modify-layers] :subsections [:modify-layers]
:fn #(emit-when-no-readonly (dwt/move-selected :left false))} :fn #(emit-when-no-readonly (dwt/move-selected :left false))}
:artboard-selection {:tooltip (ds/meta (ds/alt "G")) :artboard-selection {:tooltip (ds/meta (ds/alt "G"))
:command (ds/c-mod "alt+g") :command (ds/c-mod "alt+g")
:section [:workspace]
:subsections [:modify-layers] :subsections [:modify-layers]
:fn #(emit-when-no-readonly (dws/create-artboard-from-selection))} :fn #(emit-when-no-readonly (dws/create-artboard-from-selection))}
:toggle-layout-flex {:tooltip (ds/shift "A") :toggle-layout-flex {:tooltip (ds/shift "A")
:command "shift+a" :command "shift+a"
:section [:workspace]
:subsections [:modify-layers] :subsections [:modify-layers]
:fn #(emit-when-no-readonly :fn #(emit-when-no-readonly
(with-meta (dwsl/toggle-layout :flex) (with-meta (dwsl/toggle-layout :flex)
@ -277,6 +316,7 @@
:toggle-layout-grid {:tooltip (ds/meta-shift "A") :toggle-layout-grid {:tooltip (ds/meta-shift "A")
:command (ds/c-mod "shift+a") :command (ds/c-mod "shift+a")
:section [:workspace]
:subsections [:modify-layers] :subsections [:modify-layers]
:fn #(emit-when-no-readonly :fn #(emit-when-no-readonly
(with-meta (dwsl/toggle-layout :grid) (with-meta (dwsl/toggle-layout :grid)
@ -285,32 +325,38 @@
:draw-frame {:tooltip "B" :draw-frame {:tooltip "B"
:command ["b" "a"] :command ["b" "a"]
:section [:workspace :basics]
:subsections [:tools :basics] :subsections [:tools :basics]
:fn #(emit-when-no-readonly (dwd/select-for-drawing :frame))} :fn #(emit-when-no-readonly (dwd/select-for-drawing :frame))}
:move {:tooltip "V" :move {:tooltip "V"
:command "v" :command "v"
:section [:workspace]
:subsections [:tools] :subsections [:tools]
:fn #(emit-when-no-readonly :interrupt)} :fn #(emit-when-no-readonly :interrupt)}
:draw-rect {:tooltip "R" :draw-rect {:tooltip "R"
:command "r" :command "r"
:section [:workspace]
:subsections [:tools] :subsections [:tools]
:fn #(emit-when-no-readonly (dwd/select-for-drawing :rect))} :fn #(emit-when-no-readonly (dwd/select-for-drawing :rect))}
:draw-ellipse {:tooltip "E" :draw-ellipse {:tooltip "E"
:command "e" :command "e"
:section [:workspace]
:subsections [:tools] :subsections [:tools]
:fn #(emit-when-no-readonly (dwd/select-for-drawing :circle))} :fn #(emit-when-no-readonly (dwd/select-for-drawing :circle))}
:draw-text {:tooltip "T" :draw-text {:tooltip "T"
:command "t" :command "t"
:section [:workspace]
:subsections [:tools] :subsections [:tools]
:fn #(emit-when-no-readonly dwtxt/start-edit-if-selected :fn #(emit-when-no-readonly dwtxt/start-edit-if-selected
(dwd/select-for-drawing :text))} (dwd/select-for-drawing :text))}
:draw-path {:tooltip "P" :draw-path {:tooltip "P"
:command "p" :command "p"
:section [:workspace]
:subsections [:tools] :subsections [:tools]
:fn #(emit-when-no-readonly (dwd/select-for-drawing :path))} :fn #(emit-when-no-readonly (dwd/select-for-drawing :path))}
@ -326,52 +372,62 @@
:draw-curve {:tooltip (ds/shift "C") :draw-curve {:tooltip (ds/shift "C")
:command "shift+c" :command "shift+c"
:section [:workspace]
:subsections [:tools] :subsections [:tools]
:fn #(emit-when-no-readonly (dwd/select-for-drawing :curve))} :fn #(emit-when-no-readonly (dwd/select-for-drawing :curve))}
:add-comment {:tooltip "C" :add-comment {:tooltip "C"
:command "c" :command "c"
:section [:workspace]
:subsections [:tools] :subsections [:tools]
:fn #(st/emit! (dwd/select-for-drawing :comments))} :fn #(st/emit! (dwd/select-for-drawing :comments))}
:toggle-comments-visibility :toggle-comments-visibility
{:tooltip (ds/meta-shift "C") {:tooltip (ds/meta-shift "C")
:command (ds/c-mod "shift+c") :command (ds/c-mod "shift+c")
:section [:workspace]
:subsections [:main-menu] :subsections [:main-menu]
:fn #(st/emit! (dwcm/toggle-comments-visibility {:origin "workspace-shortcuts"}))} :fn #(st/emit! (dwcm/toggle-comments-visibility {:origin "workspace-shortcuts"}))}
:insert-image {:tooltip (ds/shift "K") :insert-image {:tooltip (ds/shift "K")
:command "shift+k" :command "shift+k"
:section [:workspace]
:subsections [:tools] :subsections [:tools]
:fn #(-> "image-upload" dom/get-element dom/click)} :fn #(-> "image-upload" dom/get-element dom/click)}
:toggle-visibility {:tooltip (ds/meta-shift "H") :toggle-visibility {:tooltip (ds/meta-shift "H")
:command (ds/c-mod "shift+h") :command (ds/c-mod "shift+h")
:section [:workspace]
:subsections [:tools] :subsections [:tools]
:fn #(emit-when-no-readonly (dw/toggle-visibility-selected))} :fn #(emit-when-no-readonly (dw/toggle-visibility-selected))}
:toggle-lock {:tooltip (ds/meta-shift "L") :toggle-lock {:tooltip (ds/meta-shift "L")
:command (ds/c-mod "shift+l") :command (ds/c-mod "shift+l")
:section [:workspace]
:subsections [:tools] :subsections [:tools]
:fn #(emit-when-no-readonly (dw/toggle-lock-selected))} :fn #(emit-when-no-readonly (dw/toggle-lock-selected))}
:toggle-lock-size {:tooltip (ds/shift "L") :toggle-lock-size {:tooltip (ds/shift "L")
:command "shift+l" :command "shift+l"
:section [:workspace]
:subsections [:tools] :subsections [:tools]
:fn #(emit-when-no-readonly (dw/toggle-proportion-lock))} :fn #(emit-when-no-readonly (dw/toggle-proportion-lock))}
:scale {:tooltip "K" :scale {:tooltip "K"
:command "k" :command "k"
:section [:workspace]
:subsections [:tools] :subsections [:tools]
:fn #(emit-when-no-readonly (toggle-layout-flag :scale-text))} :fn #(emit-when-no-readonly (toggle-layout-flag :scale-text))}
:open-color-picker {:tooltip "I" :open-color-picker {:tooltip "I"
:command "i" :command "i"
:section [:workspace]
:subsections [:tools] :subsections [:tools]
:fn #(emit-when-no-readonly (mdc/picker-for-selected-shape))} :fn #(emit-when-no-readonly (mdc/picker-for-selected-shape))}
:toggle-focus-mode {:command "f" :toggle-focus-mode {:command "f"
:tooltip "F" :tooltip "F"
:section [:workspace :basics]
:subsections [:basics :tools] :subsections [:basics :tools]
:fn #(st/emit! (dw/toggle-focus-mode))} :fn #(st/emit! (dw/toggle-focus-mode))}
@ -379,41 +435,49 @@
:align-left {:tooltip (ds/alt "A") :align-left {:tooltip (ds/alt "A")
:command "alt+a" :command "alt+a"
:section [:workspace]
:subsections [:alignment] :subsections [:alignment]
:fn #(emit-when-no-readonly (dw/align-objects :hleft))} :fn #(emit-when-no-readonly (dw/align-objects :hleft))}
:align-right {:tooltip (ds/alt "D") :align-right {:tooltip (ds/alt "D")
:command "alt+d" :command "alt+d"
:section [:workspace]
:subsections [:alignment] :subsections [:alignment]
:fn #(emit-when-no-readonly (dw/align-objects :hright))} :fn #(emit-when-no-readonly (dw/align-objects :hright))}
:align-top {:tooltip (ds/alt "W") :align-top {:tooltip (ds/alt "W")
:command "alt+w" :command "alt+w"
:section [:workspace]
:subsections [:alignment] :subsections [:alignment]
:fn #(emit-when-no-readonly (dw/align-objects :vtop))} :fn #(emit-when-no-readonly (dw/align-objects :vtop))}
:align-hcenter {:tooltip (ds/alt "H") :align-hcenter {:tooltip (ds/alt "H")
:command "alt+h" :command "alt+h"
:section [:workspace]
:subsections [:alignment] :subsections [:alignment]
:fn #(emit-when-no-readonly (dw/align-objects :hcenter))} :fn #(emit-when-no-readonly (dw/align-objects :hcenter))}
:align-vcenter {:tooltip (ds/alt "V") :align-vcenter {:tooltip (ds/alt "V")
:command "alt+v" :command "alt+v"
:section [:workspace]
:subsections [:alignment] :subsections [:alignment]
:fn #(emit-when-no-readonly (dw/align-objects :vcenter))} :fn #(emit-when-no-readonly (dw/align-objects :vcenter))}
:align-bottom {:tooltip (ds/alt "S") :align-bottom {:tooltip (ds/alt "S")
:command "alt+s" :command "alt+s"
:section [:workspace]
:subsections [:alignment] :subsections [:alignment]
:fn #(emit-when-no-readonly (dw/align-objects :vbottom))} :fn #(emit-when-no-readonly (dw/align-objects :vbottom))}
:h-distribute {:tooltip (ds/meta-shift (ds/alt "H")) :h-distribute {:tooltip (ds/meta-shift (ds/alt "H"))
:command (ds/c-mod "shift+alt+h") :command (ds/c-mod "shift+alt+h")
:section [:workspace]
:subsections [:alignment] :subsections [:alignment]
:fn #(emit-when-no-readonly (dw/distribute-objects :horizontal))} :fn #(emit-when-no-readonly (dw/distribute-objects :horizontal))}
:v-distribute {:tooltip (ds/meta-shift (ds/alt "V")) :v-distribute {:tooltip (ds/meta-shift (ds/alt "V"))
:command (ds/c-mod "shift+alt+v") :command (ds/c-mod "shift+alt+v")
:section [:workspace]
:subsections [:alignment] :subsections [:alignment]
:fn #(emit-when-no-readonly (dw/distribute-objects :vertical))} :fn #(emit-when-no-readonly (dw/distribute-objects :vertical))}
@ -421,11 +485,13 @@
:toggle-rulers {:tooltip (ds/meta-shift "R") :toggle-rulers {:tooltip (ds/meta-shift "R")
:command (ds/c-mod "shift+r") :command (ds/c-mod "shift+r")
:section [:workspace]
:subsections [:main-menu] :subsections [:main-menu]
:fn #(st/emit! (toggle-layout-flag :rulers))} :fn #(st/emit! (toggle-layout-flag :rulers))}
:select-all {:tooltip (ds/meta "A") :select-all {:tooltip (ds/meta "A")
:command (ds/c-mod "a") :command (ds/c-mod "a")
:section [:workspace]
:subsections [:main-menu] :subsections [:main-menu]
:fn #(st/emit! (dw/select-all))} :fn #(st/emit! (dw/select-all))}
@ -434,37 +500,44 @@
:command [(ds/c-mod "'") (ds/c-mod "219")] :command [(ds/c-mod "'") (ds/c-mod "219")]
:show-command (ds/c-mod "'") :show-command (ds/c-mod "'")
:subsections [:main-menu] :subsections [:main-menu]
:section [:workspace]
:fn #(st/emit! (toggle-layout-flag :display-guides))} :fn #(st/emit! (toggle-layout-flag :display-guides))}
:toggle-alignment {:tooltip (ds/meta "\\") :toggle-alignment {:tooltip (ds/meta "\\")
:command (ds/c-mod "\\") :command (ds/c-mod "\\")
:subsections [:main-menu] :subsections [:main-menu]
:section [:workspace]
:fn #(st/emit! (toggle-layout-flag :dynamic-alignment))} :fn #(st/emit! (toggle-layout-flag :dynamic-alignment))}
:thumbnail-set {:tooltip (ds/shift "T") :thumbnail-set {:tooltip (ds/shift "T")
:command "shift+t" :command "shift+t"
:subsections [:main-menu] :subsections [:main-menu]
:section [:workspace]
:fn #(st/emit! (dw/toggle-file-thumbnail-selected))} :fn #(st/emit! (dw/toggle-file-thumbnail-selected))}
:show-pixel-grid {:tooltip (ds/shift ",") :show-pixel-grid {:tooltip (ds/shift ",")
:command "shift+," :command "shift+,"
:subsections [:main-menu] :subsections [:main-menu]
:section [:workspace]
:fn #(st/emit! (toggle-layout-flag :show-pixel-grid))} :fn #(st/emit! (toggle-layout-flag :show-pixel-grid))}
:snap-pixel-grid {:command "," :snap-pixel-grid {:command ","
:tooltip "," :tooltip ","
:subsections [:main-menu] :subsections [:main-menu]
:section [:workspace]
:fn #(st/emit! (toggle-layout-flag :snap-pixel-grid))} :fn #(st/emit! (toggle-layout-flag :snap-pixel-grid))}
:export-shapes {:tooltip (ds/meta-shift "E") :export-shapes {:tooltip (ds/meta-shift "E")
:command (ds/c-mod "shift+e") :command (ds/c-mod "shift+e")
:subsections [:basics :main-menu] :subsections [:basics :main-menu]
:section [:workspace :basics]
:fn #(st/emit! :fn #(st/emit!
(de/show-workspace-export-dialog {:origin "workspace:shortcuts"}))} (de/show-workspace-export-dialog {:origin "workspace:shortcuts"}))}
:toggle-snap-ruler-guide {:tooltip (ds/meta-shift "G") :toggle-snap-ruler-guide {:tooltip (ds/meta-shift "G")
:command (ds/c-mod "shift+g") :command (ds/c-mod "shift+g")
:subsections [:main-menu] :subsections [:main-menu]
:section [:workspace]
:fn #(st/emit! (toggle-layout-flag :snap-ruler-guides))} :fn #(st/emit! (toggle-layout-flag :snap-ruler-guides))}
:toggle-snap-guides {:tooltip (ds/meta-shift "'") :toggle-snap-guides {:tooltip (ds/meta-shift "'")
@ -472,11 +545,13 @@
:command [(ds/c-mod "shift+'") (ds/c-mod "shift+219")] :command [(ds/c-mod "shift+'") (ds/c-mod "shift+219")]
:show-command (ds/c-mod "shift+'") :show-command (ds/c-mod "shift+'")
:subsections [:main-menu] :subsections [:main-menu]
:section [:workspace]
:fn #(st/emit! (toggle-layout-flag :snap-guides))} :fn #(st/emit! (toggle-layout-flag :snap-guides))}
:show-shortcuts {:tooltip "?" :show-shortcuts {:tooltip "?"
:command "?" :command "?"
:subsections [:main-menu] :subsections [:main-menu]
:section [:workspace]
:fn #(st/emit! (toggle-layout-flag :shortcuts))} :fn #(st/emit! (toggle-layout-flag :shortcuts))}
;; PANELS ;; PANELS
@ -484,22 +559,26 @@
:toggle-layers {:tooltip (ds/alt "L") :toggle-layers {:tooltip (ds/alt "L")
:command (ds/a-mod "l") :command (ds/a-mod "l")
:subsections [:panels] :subsections [:panels]
:section [:workspace]
:fn #(st/emit! (dcm/go-to-workspace :layout :layers))} :fn #(st/emit! (dcm/go-to-workspace :layout :layers))}
:toggle-assets {:tooltip (ds/alt "I") :toggle-assets {:tooltip (ds/alt "I")
:command (ds/a-mod "i") :command (ds/a-mod "i")
:subsections [:panels] :subsections [:panels]
:section [:workspace]
:fn #(st/emit! (dcm/go-to-workspace :layout :assets))} :fn #(st/emit! (dcm/go-to-workspace :layout :assets))}
:toggle-history {:tooltip (ds/meta-alt "H") :toggle-history {:tooltip (ds/meta-alt "H")
:command (ds/ca-mod "h") :command (ds/ca-mod "h")
:subsections [:panels] :subsections [:panels]
:section [:workspace]
:fn #(emit-when-no-readonly :fn #(emit-when-no-readonly
(dw/toggle-layout-flag :document-history))} (dw/toggle-layout-flag :document-history))}
:toggle-colorpalette {:tooltip (ds/alt "P") :toggle-colorpalette {:tooltip (ds/alt "P")
:command (ds/a-mod "p") :command (ds/a-mod "p")
:subsections [:panels] :subsections [:panels]
:section [:workspace]
:fn #(do (r/set-resize-type! :bottom) :fn #(do (r/set-resize-type! :bottom)
(emit-when-no-readonly (dw/remove-layout-flag :hide-palettes) (emit-when-no-readonly (dw/remove-layout-flag :hide-palettes)
(dw/remove-layout-flag :textpalette) (dw/remove-layout-flag :textpalette)
@ -508,6 +587,7 @@
:toggle-textpalette {:tooltip (ds/alt "T") :toggle-textpalette {:tooltip (ds/alt "T")
:command (ds/a-mod "t") :command (ds/a-mod "t")
:subsections [:panels] :subsections [:panels]
:section [:workspace]
:fn #(do (r/set-resize-type! :bottom) :fn #(do (r/set-resize-type! :bottom)
(emit-when-no-readonly (dw/remove-layout-flag :hide-palettes) (emit-when-no-readonly (dw/remove-layout-flag :hide-palettes)
(dw/remove-layout-flag :colorpalette) (dw/remove-layout-flag :colorpalette)
@ -516,6 +596,7 @@
:hide-ui {:tooltip "\\" :hide-ui {:tooltip "\\"
:command "\\" :command "\\"
:subsections [:panels :basics] :subsections [:panels :basics]
:section [:workspace :basics]
:fn #(st/emit! (toggle-layout-flag :hide-ui))} :fn #(st/emit! (toggle-layout-flag :hide-ui))}
;; ZOOM-WORKSPACE ;; ZOOM-WORKSPACE
@ -523,36 +604,43 @@
:increase-zoom {:tooltip "+" :increase-zoom {:tooltip "+"
:command ["+" "="] :command ["+" "="]
:subsections [:zoom-workspace] :subsections [:zoom-workspace]
:section [:workspace]
:fn #(st/emit! (dw/increase-zoom))} :fn #(st/emit! (dw/increase-zoom))}
:decrease-zoom {:tooltip "-" :decrease-zoom {:tooltip "-"
:command ["-" "_"] :command ["-" "_"]
:subsections [:zoom-workspace] :subsections [:zoom-workspace]
:section [:workspace]
:fn #(st/emit! (dw/decrease-zoom))} :fn #(st/emit! (dw/decrease-zoom))}
:reset-zoom {:tooltip (ds/shift "0") :reset-zoom {:tooltip (ds/shift "0")
:command ["shift+0" "shift+num0"] :command ["shift+0" "shift+num0"]
:subsections [:zoom-workspace] :subsections [:zoom-workspace]
:section [:workspace]
:fn #(st/emit! dw/reset-zoom)} :fn #(st/emit! dw/reset-zoom)}
:fit-all {:tooltip (ds/shift "1") :fit-all {:tooltip (ds/shift "1")
:command ["shift+1" "shift+num1"] :command ["shift+1" "shift+num1"]
:subsections [:zoom-workspace] :subsections [:zoom-workspace]
:section [:workspace]
:fn #(st/emit! dw/zoom-to-fit-all)} :fn #(st/emit! dw/zoom-to-fit-all)}
:zoom-selected {:tooltip (ds/shift "2") :zoom-selected {:tooltip (ds/shift "2")
:command ["shift+2" "shift+num2" "@" "\""] :command ["shift+2" "shift+num2" "@" "\""]
:subsections [:zoom-workspace] :subsections [:zoom-workspace]
:section [:workspace]
:fn #(st/emit! dw/zoom-to-selected-shape)} :fn #(st/emit! dw/zoom-to-selected-shape)}
:zoom-lense-increase {:tooltip "Z" :zoom-lense-increase {:tooltip "Z"
:command "z" :command "z"
:subsections [:zoom-workspace] :subsections [:zoom-workspace]
:section [:workspace]
:fn identity} :fn identity}
:zoom-lense-decrease {:tooltip (ds/alt "Z") :zoom-lense-decrease {:tooltip (ds/alt "Z")
:command "alt+z" :command "alt+z"
:subsections [:zoom-workspace] :subsections [:zoom-workspace]
:section [:workspace]
:fn identity} :fn identity}
;; NAVIGATION ;; NAVIGATION
@ -561,36 +649,43 @@
:open-viewer {:tooltip "G V" :open-viewer {:tooltip "G V"
:command "g v" :command "g v"
:subsections [:navigation-workspace] :subsections [:navigation-workspace]
:section [:workspace]
:fn #(st/emit! (dcm/go-to-viewer))} :fn #(st/emit! (dcm/go-to-viewer))}
:open-inspect {:tooltip "G I" :open-inspect {:tooltip "G I"
:command "g i" :command "g i"
:subsections [:navigation-workspace] :subsections [:navigation-workspace]
:section [:workspace]
:fn #(st/emit! (dcm/go-to-viewer :section :inspect))} :fn #(st/emit! (dcm/go-to-viewer :section :inspect))}
:open-comments {:tooltip "G C" :open-comments {:tooltip "G C"
:command "g c" :command "g c"
:subsections [:navigation-workspace] :subsections [:navigation-workspace]
:section [:workspace]
:fn #(st/emit! (dcm/go-to-viewer :section :comments))} :fn #(st/emit! (dcm/go-to-viewer :section :comments))}
:open-dashboard {:tooltip "G D" :open-dashboard {:tooltip "G D"
:command "g d" :command "g d"
:subsections [:navigation-workspace] :subsections [:navigation-workspace]
:section [:workspace]
:fn #(st/emit! (dcm/go-to-dashboard-recent))} :fn #(st/emit! (dcm/go-to-dashboard-recent))}
:select-prev {:tooltip (ds/shift "tab") :select-prev {:tooltip (ds/shift "tab")
:command "shift+tab" :command "shift+tab"
:subsections [:navigation-workspace] :subsections [:navigation-workspace]
:section [:workspace]
:fn #(st/emit! (dw/select-prev-shape))} :fn #(st/emit! (dw/select-prev-shape))}
:select-next {:tooltip ds/tab :select-next {:tooltip ds/tab
:command "tab" :command "tab"
:subsections [:navigation-workspace] :subsections [:navigation-workspace]
:section [:workspace]
:fn #(st/emit! (dw/select-next-shape))} :fn #(st/emit! (dw/select-next-shape))}
:select-parent-layer {:tooltip (ds/shift ds/enter) :select-parent-layer {:tooltip (ds/shift ds/enter)
:command "shift+enter" :command "shift+enter"
:subsections [:navigation-workspace] :subsections [:navigation-workspace]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/select-parent-layer))} :fn #(emit-when-no-readonly (dw/select-parent-layer))}
;; SHAPE ;; SHAPE
@ -598,27 +693,32 @@
:bool-union {:tooltip (ds/meta (ds/alt "U")) :bool-union {:tooltip (ds/meta (ds/alt "U"))
:command (ds/c-mod "alt+u") :command (ds/c-mod "alt+u")
:subsections [:shape] :subsections [:shape]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/create-bool :union))} :fn #(emit-when-no-readonly (dw/create-bool :union))}
:bool-difference {:tooltip (ds/meta (ds/alt "D")) :bool-difference {:tooltip (ds/meta (ds/alt "D"))
:command (ds/c-mod "alt+d") :command (ds/c-mod "alt+d")
:subsections [:shape] :subsections [:shape]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/create-bool :difference))} :fn #(emit-when-no-readonly (dw/create-bool :difference))}
:bool-intersection {:tooltip (ds/meta (ds/alt "I")) :bool-intersection {:tooltip (ds/meta (ds/alt "I"))
:command (ds/c-mod "alt+i") :command (ds/c-mod "alt+i")
:subsections [:shape] :subsections [:shape]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/create-bool :intersection))} :fn #(emit-when-no-readonly (dw/create-bool :intersection))}
:bool-exclude {:tooltip (ds/meta (ds/alt "E")) :bool-exclude {:tooltip (ds/meta (ds/alt "E"))
:command (ds/c-mod "alt+e") :command (ds/c-mod "alt+e")
:subsections [:shape] :subsections [:shape]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/create-bool :exclude))} :fn #(emit-when-no-readonly (dw/create-bool :exclude))}
;; THEME ;; THEME
:toggle-theme {:tooltip (ds/alt "M") :toggle-theme {:tooltip (ds/alt "M")
:command (ds/a-mod "m") :command (ds/a-mod "m")
:subsections [:basics] :subsections [:generic :basics]
:section [:workspace :basics]
:fn #(st/emit! (with-meta (du/toggle-theme) :fn #(st/emit! (with-meta (du/toggle-theme)
{::ev/origin "workspace:shortcut"}))} {::ev/origin "workspace:shortcut"}))}
@ -626,7 +726,8 @@
;; PLUGINS ;; PLUGINS
:plugins {:tooltip (ds/meta (ds/alt "P")) :plugins {:tooltip (ds/meta (ds/alt "P"))
:command (ds/c-mod "alt+p") :command (ds/c-mod "alt+p")
:subsections [:basics] :subsections [:generic :basics]
:section [:workspace :basics]
:fn #(when (features/active-feature? @st/state "plugins/runtime") :fn #(when (features/active-feature? @st/state "plugins/runtime")
(st/emit! (st/emit!
(ev/event {::ev/name "open-plugins-manager" ::ev/origin "workspace:shortcuts"}) (ev/event {::ev/name "open-plugins-manager" ::ev/origin "workspace:shortcuts"})
@ -644,6 +745,7 @@
(map (fn [n] [(keyword (str "opacity-" n)) (map (fn [n] [(keyword (str "opacity-" n))
{:tooltip (str n) {:tooltip (str n)
:command [(str n) (str "num" n)] :command [(str n) (str "num" n)]
:section [:workspace]
:subsections [:modify-layers] :subsections [:modify-layers]
:fn #(emit-when-no-readonly (dwly/pressed-opacity n))}]))))) :fn #(emit-when-no-readonly (dwly/pressed-opacity n))}])))))
@ -652,6 +754,24 @@
*assert* *assert*
(merge debug-shortcuts))) (merge debug-shortcuts)))
(defn get-tooltip [shortcut] (defn get-tooltip
"Returns the tooltip string for a shortcut, using any custom binding
from the user's profile props if one exists, falling back to the
default :tooltip field."
[shortcut]
(assert (contains? shortcuts shortcut) (str shortcut)) (assert (contains? shortcuts shortcut) (str shortcut))
(get-in shortcuts [shortcut :tooltip])) (let [custom-shortcuts (deref refs/custom-shortcuts)
custom-command (get-in custom-shortcuts [:workspace shortcut])]
(if (and custom-command (not= custom-command ""))
(ds/command->tooltip custom-command)
(get-in shortcuts [shortcut :tooltip]))))
(defn get-effective-tooltip
"Returns the tooltip string for a shortcut given an already-resolved
custom-shortcuts map. Use this when you already have the custom
shortcuts derefed for the current render cycle."
[shortcut custom-shortcuts]
(let [custom-command (get-in custom-shortcuts [:workspace shortcut])]
(if (and custom-command (not= custom-command ""))
(ds/command->tooltip custom-command)
(get-in shortcuts [shortcut :tooltip]))))

View File

@ -237,31 +237,37 @@
{:underline {:tooltip (ds/meta "U") {:underline {:tooltip (ds/meta "U")
:command (ds/c-mod "u") :command (ds/c-mod "u")
:subsections [:text-editor] :subsections [:text-editor]
:section [:workspace]
:fn #(update-attrs-when-no-readonly {:text-decoration "toggle-underline"})} :fn #(update-attrs-when-no-readonly {:text-decoration "toggle-underline"})}
:line-through {:tooltip (ds/alt (ds/meta-shift "5")) :line-through {:tooltip (ds/alt (ds/meta-shift "5"))
:command "alt+shift+5" :command "alt+shift+5"
:subsections [:text-editor] :subsections [:text-editor]
:section [:workspace]
:fn #(update-attrs-when-no-readonly {:text-decoration "toggle-line-through"})} :fn #(update-attrs-when-no-readonly {:text-decoration "toggle-line-through"})}
:font-size-inc {:tooltip (ds/meta-shift ">") :font-size-inc {:tooltip (ds/meta-shift ">")
:command (ds/c-mod "shift+.") :command (ds/c-mod "shift+.")
:subsections [:text-editor] :subsections [:text-editor]
:section [:workspace]
:fn #(update-attrs-when-no-readonly {:font-size-inc true})} :fn #(update-attrs-when-no-readonly {:font-size-inc true})}
:font-size-dec {:tooltip (ds/meta-shift "<") :font-size-dec {:tooltip (ds/meta-shift "<")
:command (ds/c-mod "shift+,") :command (ds/c-mod "shift+,")
:subsections [:text-editor] :subsections [:text-editor]
:section [:workspace]
:fn #(update-attrs-when-no-readonly {:font-size-dec true})} :fn #(update-attrs-when-no-readonly {:font-size-dec true})}
:bold {:tooltip (ds/meta "b") :bold {:tooltip (ds/meta "b")
:command (ds/c-mod "b") :command (ds/c-mod "b")
:subsections [:text-editor] :subsections [:text-editor]
:section [:workspace]
:fn #(update-attrs-when-no-readonly {:font-variant-id "toggle-bold"})} :fn #(update-attrs-when-no-readonly {:font-variant-id "toggle-bold"})}
:italic {:tooltip (ds/meta "i") :italic {:tooltip (ds/meta "i")
:command (ds/c-mod "i") :command (ds/c-mod "i")
:subsections [:text-editor] :subsections [:text-editor]
:section [:workspace]
:fn #(update-attrs-when-no-readonly {:font-variant-id "toggle-italic"})}}) :fn #(update-attrs-when-no-readonly {:font-variant-id "toggle-italic"})}})

View File

@ -32,6 +32,9 @@
(def profile (def profile
(l/derived (l/key :profile) st/state)) (l/derived (l/key :profile) st/state))
(def custom-shortcuts
(l/derived (fn [state] (get-in state [:profile :props :custom-shortcuts])) st/state))
(def current-page-id (def current-page-id
(l/derived (l/key :current-page-id) st/state)) (l/derived (l/key :current-page-id) st/state))

View File

@ -203,7 +203,8 @@
:settings-feedback :settings-feedback
:settings-subscription :settings-subscription
:settings-integrations :settings-integrations
:settings-notifications) :settings-notifications
:settings-shortcuts)
(let [params (get params :query) (let [params (get params :query)
error-report-id (some-> params :error-report-id uuid/parse*)] error-report-id (some-> params :error-report-id uuid/parse*)]
[:? [:> settings-page* [:? [:> settings-page*

View File

@ -10,7 +10,7 @@
display: flex; display: flex;
gap: deprecated.$s-2; gap: deprecated.$s-2;
height: deprecated.$s-32; height: deprecated.$s-32;
width: 100%; flex-grow: 1;
border-radius: deprecated.$br-8; border-radius: deprecated.$br-8;
background-color: var(--search-bar-background-color); background-color: var(--search-bar-background-color);
} }

View File

@ -33,6 +33,8 @@
(def is-render? (mf/create-context false)) (def is-render? (mf/create-context false))
(def is-component? (mf/create-context false)) (def is-component? (mf/create-context false))
(def shortcuts-ctx (mf/create-context nil))
(def sidebar (def sidebar
"A context that intends to store the current sidebar position, "A context that intends to store the current sidebar position,
useful for components that behaves distinctly if they are showed in useful for components that behaves distinctly if they are showed in

View File

@ -305,7 +305,7 @@
(filter :is-default) (filter :is-default)
(first)))] (first)))]
(hooks/use-shortcuts ::dashboard sc/shortcuts-dashboard) (hooks/use-shortcuts ::dashboard sc/shortcuts-dashboard :dashboard)
(mf/with-effect [team-id] (mf/with-effect [team-id]
(st/emit! (dd/initialize team-id)) (st/emit! (dd/initialize team-id))

View File

@ -195,7 +195,7 @@
(st/emit! (dpj/fetch-files project-id) (st/emit! (dpj/fetch-files project-id)
(dd/clear-selected-files))) (dd/clear-selected-files)))
(hooks/use-shortcuts ::dashboard sc/shortcuts-drafts-libraries) (hooks/use-shortcuts ::dashboard sc/shortcuts-drafts-libraries :dashboard)
[:* [:*
[:> header* {:team team [:> header* {:team team

View File

@ -60,7 +60,7 @@
(st/emit! (dtm/fetch-shared-files team-id) (st/emit! (dtm/fetch-shared-files team-id)
(dd/clear-selected-files))) (dd/clear-selected-files)))
(hooks/use-shortcuts ::dashboard sc/shortcuts-drafts-libraries) (hooks/use-shortcuts ::dashboard sc/shortcuts-drafts-libraries :dashboard)
[:* [:*
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"} [:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}

View File

@ -369,7 +369,7 @@
(st/emit! (dd/fetch-recent-files team-id) (st/emit! (dd/fetch-recent-files team-id)
(dd/clear-selected-files))) (dd/clear-selected-files)))
(hooks/use-shortcuts ::dashboard sc/shortcuts-projects) (hooks/use-shortcuts ::dashboard sc/shortcuts-projects :dashboard)
(when (seq projects) (when (seq projects)
[:* [:*

View File

@ -42,4 +42,4 @@
(on-ref node)))})] (on-ref node)))})]
[:> element props [:> element props
(when icon [:> icon* {:icon-id icon :size "m"}]) (when icon [:> icon* {:icon-id icon :size "m"}])
[:span {:class (stl/css :label-wrapper)} children]])) children]))

View File

@ -14,7 +14,7 @@
(def ^:private schema:empty-state (def ^:private schema:empty-state
[:map [:map
[:class {:optional true} :string] [:class {:optional true} :string]
[:icon [:and :string [:fn #(contains? icon-list %)]]] [:icon {:optional true} [:and :string [:fn #(contains? icon-list %)]]]
[:text :string]]) [:text :string]])
(mf/defc empty-state* (mf/defc empty-state*
@ -22,8 +22,9 @@
[{:keys [class icon text] :rest props}] [{:keys [class icon text] :rest props}]
(let [props (mf/spread-props props {:class [class (stl/css :group)]})] (let [props (mf/spread-props props {:class [class (stl/css :group)]})]
[:> :div props [:> :div props
[:div {:class (stl/css :icon-wrapper)} (when icon
[:> icon* {:icon-id icon [:div {:class (stl/css :icon-wrapper)}
:size "l" [:> icon* {:icon-id icon
:class (stl/css :icon)}]] :size "l"
:class (stl/css :icon)}]])
[:div {:class (stl/css :text)} text]])) [:div {:class (stl/css :text)} text]]))

View File

@ -42,13 +42,14 @@
state)) state))
(defn use-shortcuts (defn use-shortcuts
[key shortcuts] [key shortcuts group-key]
(mf/use-effect (let [custom-shortcuts (mf/deref refs/custom-shortcuts)]
#js [(str key) shortcuts] (mf/use-effect
(fn [] #js [(str key) shortcuts custom-shortcuts]
(st/emit! (dsc/push-shortcuts key shortcuts))
(fn [] (fn []
(st/emit! (dsc/pop-shortcuts key)))))) (st/emit! (dsc/push-shortcuts key shortcuts group-key))
(fn []
(st/emit! (dsc/pop-shortcuts key)))))))
(defn- set-timer (defn- set-timer
[state ms func] [state ms func]

View File

@ -41,7 +41,8 @@
["/options" :settings-options] ["/options" :settings-options]
["/subscriptions" :settings-subscription] ["/subscriptions" :settings-subscription]
["/integrations" :settings-integrations] ["/integrations" :settings-integrations]
["/notifications" :settings-notifications]] ["/notifications" :settings-notifications]
["/shortcuts" :settings-shortcuts]]
["/frame-preview" :frame-preview] ["/frame-preview" :frame-preview]

View File

@ -21,6 +21,7 @@
[app.main.ui.settings.options :refer [options-page*]] [app.main.ui.settings.options :refer [options-page*]]
[app.main.ui.settings.password :refer [password-page*]] [app.main.ui.settings.password :refer [password-page*]]
[app.main.ui.settings.profile :refer [profile-page*]] [app.main.ui.settings.profile :refer [profile-page*]]
[app.main.ui.settings.shortcuts :refer [shortcuts-page*]]
[app.main.ui.settings.sidebar :refer [sidebar*]] [app.main.ui.settings.sidebar :refer [sidebar*]]
[app.main.ui.settings.subscription :refer [subscription-page*]] [app.main.ui.settings.subscription :refer [subscription-page*]]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
@ -38,7 +39,7 @@
(let [section (get-in route [:data :name]) (let [section (get-in route [:data :name])
profile (mf/deref refs/profile)] profile (mf/deref refs/profile)]
(hooks/use-shortcuts ::dashboard sc/shortcuts) (hooks/use-shortcuts ::dashboard sc/shortcuts :dashboard)
(mf/with-effect [profile] (mf/with-effect [profile]
(when (nil? profile) (when (nil? profile)
@ -54,7 +55,7 @@
[:div {:class (stl/css :dashboard-content)} [:div {:class (stl/css :dashboard-content)}
[:> header*] [:> header*]
[:section {:class (stl/css :dashboard-container)} [:div {:class (stl/css :dashboard-container)}
(case section (case section
:settings-profile :settings-profile
[:> profile-page*] [:> profile-page*]
@ -77,7 +78,10 @@
[:> integrations-page*] [:> integrations-page*]
:settings-notifications :settings-notifications
[:> notifications-page* {:profile profile}])]]]])) [:> notifications-page* {:profile profile}]
:settings-shortcuts
[:> shortcuts-page* {:profile profile}])]]]]))
(mf/defc settings-page* (mf/defc settings-page*
{::mf/lazy-load true} {::mf/lazy-load true}

View File

@ -98,7 +98,8 @@
:form form} :form form}
;; --- Feedback section ;; --- Feedback section
[:h2 {:class (stl/css :field-title :feedback-title)} (tr "feedback.title-contact-us")] [:h2 {:class (stl/css :field-title :feedback-title)
:id "feedback-section-title"} (tr "feedback.title-contact-us")]
[:p {:class (stl/css :field-text :feedback-title)} (tr "feedback.subtitle")] [:p {:class (stl/css :field-text :feedback-title)} (tr "feedback.subtitle")]
[:div {:class (stl/css :fields-row)} [:div {:class (stl/css :fields-row)}
@ -164,6 +165,7 @@
(:content errors/last-report)) (:content errors/last-report))
props (mf/spread-props props {:error-report report})] props (mf/spread-props props {:error-report report})]
[:div {:class (stl/css :dashboard-settings)} [:section {:class (stl/css :dashboard-settings)
:aria-labelledby "feedback-section-title"}
[:div {:class (stl/css :form-container)} [:div {:class (stl/css :form-container)}
[:> feedback-form* props]]])) [:> feedback-form* props]]]))

View File

@ -640,10 +640,12 @@
(dom/set-html-title (tr "title.settings.integrations")) (dom/set-html-title (tr "title.settings.integrations"))
(st/emit! (du/fetch-access-tokens))) (st/emit! (du/fetch-access-tokens)))
[:div {:class (stl/css :integrations)} [:section {:class (stl/css :integrations)
[:> heading* {:level 1 :aria-labelledby "integrations-section-title"}
[:> heading* {:level 2
:typography t/title-large :typography t/title-large
:class (stl/css :color-primary)} :class (stl/css :color-primary)
:id "integrations-section-title"}
(tr "integrations.title")] (tr "integrations.title")]
(when ^boolean mcp-enabled? (when ^boolean mcp-enabled?

View File

@ -122,10 +122,15 @@
(mf/use-effect (mf/use-effect
#(dom/set-html-title (tr "title.settings.options"))) #(dom/set-html-title (tr "title.settings.options")))
[:div {:class (stl/css :dashboard-settings)} [:section {:class (stl/css :dashboard-settings)
:aria-labelledby "options-section-title"}
[:* [:*
[:div {:class (stl/css :form-container) :data-testid "settings-form"} [:div {:class (stl/css :form-container) :data-testid "settings-form"}
[:h2 (tr "labels.settings")] [:> heading* {:level 2
:typography t/title-large
:class (stl/css :color-primary)
:id "options-section-title"}
(tr "labels.settings")]
[:> options-form*]] [:> options-form*]]
(when (contains? cf/flags :render-switch) (when (contains? cf/flags :render-switch)
[:> webgl-settings* {:renderer renderer}])]])) [:> webgl-settings* {:renderer renderer}])]]))

View File

@ -106,7 +106,8 @@
(mf/with-effect [] (mf/with-effect []
(dom/set-html-title (tr "title.settings.password"))) (dom/set-html-title (tr "title.settings.password")))
[:section {:class (stl/css :dashboard-settings)} [:section {:class (stl/css :dashboard-settings)
:aria-labelledby "password-section-title"}
[:div {:class (stl/css :form-container)} [:div {:class (stl/css :form-container)}
[:h2 (tr "dashboard.password-change")] [:h2 {:id "password-section-title"} (tr "dashboard.password-change")]
[:> password-form*]]]) [:> password-form*]]])

View File

@ -143,8 +143,9 @@
(mf/with-effect [] (mf/with-effect []
(dom/set-html-title (tr "title.settings.profile"))) (dom/set-html-title (tr "title.settings.profile")))
[:div {:class (stl/css :dashboard-settings)} [:section {:class (stl/css :dashboard-settings)
:aria-labelledby "profile-section-title"}
[:div {:class (stl/css :form-container)} [:div {:class (stl/css :form-container)}
[:h2 (tr "labels.profile")] [:h2 {:id "profile-section-title"} (tr "labels.profile")]
[:> profile-photo-form*] [:> profile-photo-form*]
[:> profile-form*]]]) [:> profile-form*]]])

View File

@ -0,0 +1,166 @@
(ns app.main.ui.settings.restore-shortcuts-modal
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.i18n :refer [tr]]
[app.main.data.dashboard.shortcuts :as dsc]
[app.main.data.dashboard.shortcuts.customize :as customize]
[app.main.data.modal :as modal]
[app.main.data.shortcuts :as ds]
[app.main.data.viewer.shortcuts :as vsc]
[app.main.data.workspace.path.shortcuts :as psc]
[app.main.data.workspace.shortcuts :as wsc]
[app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.shortcuts :as ss]
[app.util.dom :as dom]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(def ^:private workspace-shortcuts-raw
(d/deep-merge psc/shortcuts
wsc/shortcuts))
(def ^:private context->defaults
{:workspace workspace-shortcuts-raw
:dashboard dsc/shortcuts
:viewer vsc/shortcuts})
(defn extract-shortcut-keys [shortcut-key custom-shortcuts context]
(let [defaults (get context->defaults context)
default-command (:command (get defaults shortcut-key))
default-managed-list (if (coll? default-command)
default-command
(conj () default-command))
default-chars-list (map ds/split-sc default-managed-list)
default-last-element (last default-chars-list)
default-short-char-list (if (= 1 (count default-chars-list))
default-chars-list
(drop-last default-chars-list))
ctx-customs (get custom-shortcuts context {})
current-command (or (get ctx-customs shortcut-key) default-command)
current-managed-list (if (coll? current-command)
current-command
(conj () current-command))
current-chars-list (map ds/split-sc current-managed-list)
current-last-element (last current-chars-list)
current-short-char-list (if (= 1 (count current-chars-list))
current-chars-list
(drop-last current-chars-list))]
[default-last-element default-short-char-list current-last-element current-command current-short-char-list]))
(mf/defc restore-all-modal
{::mf/register modal/components
::mf/register-as :restore-all-modal}
[{:keys [custom-shortcuts]}]
(let [handle-close-dialog (mf/use-fn
(fn [event]
(dom/stop-propagation event)
(st/emit! (modal/hide))))
handle-accept-dialog (mf/use-fn
(fn [event]
(dom/stop-propagation event)
(st/emit! (customize/reset-all-custom-shortcuts))
(st/emit! (modal/hide))))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog)}
[:> icon-button* {:class (stl/css :close-btn)
:variant "ghost"
:aria-label (tr "labels.close")
:on-click handle-close-dialog
:tooltip-class (stl/css :close-btn-tooltip)
:icon i/close}]
[:div {:class (stl/css :modal-title)}
(tr "restore-shortcuts.modal-title")]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-content-text)}
(tr "restore-shortcuts.modal-text")]
[:table {:class (stl/css :shortcuts-table)}
[:thead
[:tr {:class (stl/css :shortcuts-list-header)}
[:th {:class (stl/css :shortcut-header-name)}
(tr "restore-shortcuts.acction")]
[:th {:class (stl/css :shortcut-header-command)}
(tr "labels.current")]
[:th {:class (stl/css :shortcut-header-command)}
(tr "labels.default")]]]
[:tbody {:class (stl/css :shortcuts-list-body)}
(for [context [:workspace :dashboard :viewer]
shortcut-key (keys (get custom-shortcuts context {}))]
(let [[default-last-element
default-short-char-list
current-last-element
current-command
current-short-char-list] (extract-shortcut-keys shortcut-key custom-shortcuts context)
current-penultimate (last current-short-char-list)
default-penultimate (last default-short-char-list)]
[:tr {:key (dm/str (name context) "-" (name shortcut-key))
:class (stl/css :shortcuts-list-item)}
[:td {:class (stl/css :shortcut-name)}
(ss/translation-keyname :sc shortcut-key)]
[:td {:class (stl/css :shortcut-command)}
(if (str/blank? current-command)
[:span {:class (stl/css :shortcut-empty)} "-"]
(for [chars current-short-char-list]
[:* {:key (str/join chars)}
(for [char chars]
[:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key))
:char char
:class (stl/css :default-command)
:command shortcut-key}])
(when (not= chars current-penultimate) [:span {:class (stl/css :space)} ","])]))
(when (not= current-last-element current-penultimate)
[:*
[:span {:class (stl/css :space)} (tr "shortcuts.or")]
(for [char current-last-element]
[:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key))
:char char
:class (stl/css :default-command)
:command shortcut-key}])])]
[:td {:class (stl/css :shortcut-command)}
(for [chars default-short-char-list]
[:* {:key (str/join chars)}
(for [char chars]
[:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key))
:class (stl/css :default-command)
:char char
:command shortcut-key}])
(when (not= chars default-penultimate) [:span {:class (stl/css :space)} ","])])
(when (not= default-last-element default-penultimate)
[:*
[:span {:class (stl/css :space)} (tr "shortcuts.or")]
(for [char default-last-element]
[:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key))
:class (stl/css :default-command)
:char char
:command shortcut-key}])])]]))]]]
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
[:> button* {:class (stl/css :cancel-button)
:variant "secondary"
:type "button"
:on-click modal/hide!}
(tr "labels.cancel")]
[:> button* {:class (stl/css :cancel-button)
:type "button"
:variant "primary"
:on-click handle-accept-dialog}
(tr "restore-shortcuts.restore")]]]]]))

View File

@ -0,0 +1,147 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC Sucursal en España SL
@use "ds/typography.scss" as t;
@use "ds/_borders.scss" as *;
@use "ds/spacing.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
.modal-overlay {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
inset-inline-start: 0;
inset-block-start: 0;
block-size: 100%;
inline-size: 100%;
z-index: var(--z-index-set);
background-color: var(--overlay-color);
}
.modal-dialog {
position: relative;
padding: var(--sp-xxxl);
border-radius: $br-8;
background-color: var(--color-background-primary);
border: $b-2 solid var(--color-background-quaternary);
display: grid;
grid-template-rows: auto 1fr auto;
gap: var(--sp-xxxl);
min-inline-size: $sz-364;
min-block-size: $sz-192;
max-block-size: $sz-520;
inline-size: $sz-500;
max-inline-size: $sz-712;
}
.modal-title {
@include t.use-typography("headline-medium");
color: var(--color-foreground-primary);
block-size: px2rem(26);
}
.close-btn {
position: absolute;
inset-block-start: px2rem(8);
inset-inline-end: px2rem(6);
block-size: $sz-32;
inline-size: $sz-28;
}
.close-btn-tooltip {
position: absolute;
inset-block-start: px2rem(8);
inset-inline-end: px2rem(6);
}
.modal-content {
display: flex;
flex-direction: column;
gap: var(--sp-l);
block-size: fit-content;
}
.modal-content-text {
@include t.use-typography("body-medium");
color: var(--color-foreground-primary);
}
.modal-footer {
justify-self: end;
display: flex;
flex-direction: row;
gap: var(--sp-s);
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: var(--sp-s);
}
.shortcuts-table {
inline-size: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.shortcuts-list-item,
.shortcuts-list-header {
@include t.use-typography("body-medium");
color: var(--color-foreground-secondary);
display: grid;
grid-template-columns: 40% 30% 30%;
}
.shortcuts-list-header {
border-block-end: $b-1 solid var(--color-foreground-secondary);
block-size: $sz-32;
text-align: start;
padding-block-end: var(--sp-s);
}
.shortcuts-list-body {
display: flex;
flex-direction: column;
gap: var(--sp-s);
}
.shortcuts-list-item {
color: var(--color-foreground-primary);
}
.shortcuts-list-item:first-child {
padding-block-start: var(--sp-s);
}
.shortcut-header-name,
.shortcut-header-command {
text-align: start;
}
.shortcut-command {
display: flex;
gap: var(--sp-s);
align-items: center;
justify-content: start;
text-align: start;
flex-wrap: wrap;
}
.default-command {
background-color: var(--color-background-tertiary);
color: var(--color-foreground-secondary);
border-radius: $br-6;
}
.shortcut-empty {
color: var(--color-foreground-secondary);
}

View File

@ -0,0 +1,479 @@
(ns app.main.ui.settings.shortcuts
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.i18n :refer [tr]]
[app.common.json :as json]
[app.common.schema :as sm]
[app.main.data.dashboard.shortcuts :as dsc]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.shortcuts :as ds]
[app.main.data.viewer.shortcuts :as vsc]
[app.main.data.workspace.path.shortcuts :as psc]
[app.main.data.workspace.shortcuts :as wsc]
[app.main.store :as st]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu*
dropdown-menu-item*]]
[app.main.ui.components.search-bar :refer [search-bar*]]
[app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.foundations.typography :as t]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.settings.restore-shortcuts-modal]
[app.main.ui.shortcuts :as ss]
[app.util.dom :as dom]
[app.util.strings :refer [matches-search]]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[malli.error :as me]
[rumext.v2 :as mf]))
(mf/defc search-section*
[{:keys [filter-term on-search-term-change on-search-clear-click on-restore-all show-restore-all? has-custom-shortcuts]}]
[:div {:class (stl/css :shortcuts-search-section)}
[:> search-bar* {:on-change on-search-term-change
:on-clear on-search-clear-click
:value filter-term
:placeholder (tr "shortcuts.title")
:icon-id i/search
:auto-focus true}]
(when (and show-restore-all? has-custom-shortcuts)
[:> button*
{:variant "secondary"
:on-click on-restore-all
:class (stl/css :restore-all-button)
:icon i/reload}
(tr "dashboard.restore-all-deleted-button")])])
(defn- filter-shortcuts-tree
[tree shortcut-filter search-term]
(let [sorted-comp (when (sorted? tree)
#(compare (name %1) (name %2)))
result (keep (fn [[k node]]
(let [children (:children node)]
(cond
(map? children)
(let [filtered-children
(filter-shortcuts-tree children shortcut-filter search-term)]
(when (seq filtered-children)
[k (assoc node :children filtered-children)]))
(shortcut-filter k node search-term)
[k node])))
tree)]
(if sorted-comp
(into (sorted-map-by sorted-comp) result)
(into {} result))))
(defn- collect-open-section-ids
[tree]
(letfn [(walk [nodes]
(mapcat
(fn [node]
(let [children (:children node)]
(concat
(when (:children node)
[(:id node)])
(when (map? children)
(walk (vals children))))))
nodes))]
(->> (walk (vals tree))
(remove nil?)
set)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Imported shortcuts JSON validation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private known-shortcut-keys
"Known shortcut keys per context, derived from the default shortcuts maps."
{:workspace (set (keys wsc/shortcuts))
:dashboard (set (keys dsc/shortcuts))
:viewer (set (keys vsc/shortcuts))})
(def ^:private schema:imported-shortcuts
"Malli schema for an imported custom-shortcuts payload.
The expected shape is a map of shortcut contexts to maps of shortcut-key -> command-string.
Only keys that exist in the default shortcuts for each context are accepted."
(letfn [(context-schema [ctx]
(into [:map {:closed true}]
(map (fn [k] [k {:optional true} :string]))
(get known-shortcut-keys ctx)))]
[:map {:closed true}
[:workspace {:optional true} (context-schema :workspace)]
[:dashboard {:optional true} (context-schema :dashboard)]
[:viewer {:optional true} (context-schema :viewer)]]))
(defn- flatten-humanize
"Turns Malli's humanized explain output into a flat list of {:path :message} maps."
([human] (flatten-humanize [] human))
([prefix human]
(cond
(or (string? human) (symbol? human) (keyword? human))
[{:path (str/join "/" (map #(if (keyword? %) (name %) (str %)) prefix))
:message (str human)}]
(map? human)
(mapcat (fn [[k v]] (flatten-humanize (conj prefix k) v)) human)
(sequential? human)
(mapcat #(flatten-humanize prefix %) human)
:else [])))
(defn validate-imported-shortcuts
"Validates an imported custom-shortcuts JSON payload.
Returns {:valid? true} when the payload conforms to the expected shape, or
{:valid? false :errors [{:path \"...\" :message \"...\"} ...]} otherwise.
Never throws for validation errors."
[data]
(try
(if-not (map? data)
{:valid? false
:errors [{:path ""
:message "Expected a map of shortcut contexts"}]}
(let [explain (sm/explain schema:imported-shortcuts data)]
(if (nil? explain)
{:valid? true}
{:valid? false
:errors (-> explain me/humanize flatten-humanize vec)})))
(catch :default e
{:valid? false
:errors [{:path ""
:message (str "Validation failed: " (.-message e))}]})))
(mf/defc shortcuts-list*
[{:keys [shortcuts open-sections filter-term custom-shortcuts editable? manage-sections conflicts]}]
(let [sections (mf/with-memo [shortcuts]
(keep (fn [section]
(when (seq (get-in section [1 :children]))
section))
shortcuts))]
[:div {:class (stl/css :shortcuts-list)
:aria-label (tr "shortcuts.title")}
(for [section sections]
(let [[section-key _] section]
[:> ss/shortcut-section* {:key (name section-key)
:section section
:manage-sections manage-sections
:open-sections open-sections
:filter-term filter-term
:editable? editable?
:custom-shortcuts custom-shortcuts
:conflicts conflicts}]))]))
(mf/defc shortcuts-tab-section*
[{:keys [shortcut-filter show-restore-all? on-restore-all empty-str custom-shortcuts conflicts]}]
(let [{:keys [all-shortcuts]} (mf/use-ctx ctx/shortcuts-ctx)
open-sections* (mf/use-state [[:workspace]])
open-sections (deref open-sections*)
filter-term* (mf/use-state "")
filter-term (deref filter-term*)
section-has-content?
(fn [section]
(let [children (:children section)]
(if (and (= (count children) 1) (contains? children :none))
(seq (:children (:none children)))
(seq children))))
all-shortcuts (into {} (filter (fn [[_ v]] (section-has-content? v)) all-shortcuts))
filtered-shortcuts (filter-shortcuts-tree all-shortcuts shortcut-filter filter-term)
on-search-term-change
(mf/use-fn
(mf/deps all-shortcuts shortcut-filter)
(fn [term]
(reset! filter-term* term)
(if (str/blank? term)
(reset! open-sections* [[:workspace]])
(let [filtered-tree (filter-shortcuts-tree all-shortcuts shortcut-filter term)
open-ids (collect-open-section-ids filtered-tree)]
(reset! open-sections* open-ids)))))
on-search-clear-click
(mf/use-fn
(fn [_]
(reset! open-sections* [[:workspace]])
(reset! filter-term* "")))
manage-sections
(fn [item]
(fn [event]
(dom/stop-propagation event)
(let [is-present? (some #(= % item) open-sections)
new-value (if is-present?
(filterv (fn [element] (not= element item)) open-sections)
(conj open-sections item))]
(reset! open-sections* new-value))))]
[:div {:class (stl/css :shortcuts-section)}
[:> search-section* {:filter-term filter-term
:on-search-term-change on-search-term-change
:on-search-clear-click on-search-clear-click
:on-restore-all on-restore-all
:show-restore-all? show-restore-all?
:has-custom-shortcuts (some #(seq (val %)) custom-shortcuts)}]
(if (seq filtered-shortcuts)
[:> shortcuts-list* {:shortcuts filtered-shortcuts
:open-sections open-sections
:filter-term filter-term*
:custom-shortcuts custom-shortcuts
:editable? true
:manage-sections manage-sections
:conflicts conflicts}]
[:> empty-state* {:text empty-str
:class (stl/css :shortcuts-empty-state)}])]))
(mf/defc shortcuts-page*
[{:keys [profile]}]
(let [section* (mf/use-state :all)
section (deref section*)
show-menu* (mf/use-state false)
show-menu? (deref show-menu*)
open-menu
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(reset! show-menu* true)))
close-menu
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(reset! show-menu* false)))
input-ref (mf/use-ref nil)
tabs
(mf/with-memo []
[{:label (tr "labels.all")
:id "all"}
{:label (tr "shortcuts.personalized")
:data-testid "personalized"
:id "personalized"}
{:label (tr "shortcuts.disabled")
:data-testid "disabled"
:id "disabled"}])
handle-change-tab
(mf/use-fn
(fn [new-section]
(reset! section* (keyword new-section))))
custom-shortcuts (get-in profile [:props :custom-shortcuts])
has-custom-shortcuts (some #(seq (val %)) custom-shortcuts)
path-shortcuts-custom (ds/apply-custom-overrides
psc/shortcuts
custom-shortcuts
:workspace)
path-shortcuts-with-translation (->> path-shortcuts-custom
(ss/add-translation :sc)
(into {}))
workspace-shortcuts-custom (ds/apply-custom-overrides
wsc/shortcuts
custom-shortcuts
:workspace)
workspace-shortcuts-with-translation (->> workspace-shortcuts-custom
(ss/add-translation :sc)
(into {}))
dashboard-shortcuts-custom (ds/apply-custom-overrides
dsc/shortcuts
custom-shortcuts :dashboard)
dashboard-shortcuts-with-translation (->> dashboard-shortcuts-custom
(ss/add-translation :sc)
(into {}))
viewer-shortcuts-custom (ds/apply-custom-overrides
vsc/shortcuts
custom-shortcuts
:viewer)
viewer-shortcuts-with-translation (->> viewer-shortcuts-custom
(ss/add-translation :sc)
(into {}))
all-shortcuts-raw (d/deep-merge
workspace-shortcuts-custom
path-shortcuts-custom
dashboard-shortcuts-custom
viewer-shortcuts-custom)
{:keys [all-shortcuts]}
(ss/build-all-shortcuts-without-basics
workspace-shortcuts-with-translation
path-shortcuts-with-translation
dashboard-shortcuts-with-translation
viewer-shortcuts-with-translation
:workspace-orig wsc/shortcuts
:path-orig psc/shortcuts
:dashboard-orig dsc/shortcuts
:viewer-orig vsc/shortcuts)
shortcuts-ctx-value
{:workspace-sc-trans (d/deep-merge workspace-shortcuts-with-translation path-shortcuts-with-translation)
:dashboard-sc-trans dashboard-shortcuts-with-translation
:viewer-sc-trans viewer-shortcuts-with-translation
:all-shortcuts all-shortcuts
:all-sc-raw all-shortcuts-raw}
on-restore-all
(mf/use-fn
(mf/deps custom-shortcuts)
(fn [event]
(dom/stop-propagation event)
(st/emit! (modal/show {:type :restore-all-modal
:custom-shortcuts custom-shortcuts}))))
on-import-file
(mf/use-fn
(fn [_]
(dom/click (mf/ref-val input-ref))))
shortcuts-json
(mf/with-memo
[custom-shortcuts]
(some-> custom-shortcuts
(json/encode :key-fn d/name :indent 2)))
on-export
(mf/use-fn
(mf/deps shortcuts-json has-custom-shortcuts)
(fn []
(when has-custom-shortcuts
(->> (wapi/create-blob shortcuts-json "application/json")
(dom/trigger-download "penpot-shortcuts.json")))))
on-file-selected
(mf/use-fn
(mf/deps all-shortcuts-raw)
(fn [event]
(let [file (-> (dom/get-target event)
(dom/get-files)
(first))]
(->> (wapi/read-file-as-text file)
(rx/subs!
(fn [content]
(try
(let [shortcuts (js->clj (.parse js/JSON content)
:keywordize-keys true)
validation (validate-imported-shortcuts shortcuts)]
(if (:valid? validation)
(st/emit! (ss/import-custom-shortcuts shortcuts all-shortcuts-raw))
(st/emit! (ntf/error (tr "errors.invalid-data")))))
(catch :default _
(st/emit! (ntf/error (tr "errors.invalid-data"))))))))
(-> (mf/ref-val input-ref)
(dom/set-value! "")))))]
[:section {:class (stl/css :shortcuts-page)
:aria-label (tr "shortcuts.page")}
[:div {:class (stl/css :shortcuts-content)}
[:> heading* {:level 2
:typography t/title-large
:class (stl/css :page-title)}
(tr "label.shortcuts")]
[:> text* {:class (stl/css :shortcuts-description)
:as "span"
:typography t/body-small}
[:> icon* {:icon-id i/info
:class (stl/css :shortcuts-description-icon)}]
(tr "shortcuts.reload-hint")]
[:> (mf/provider ctx/shortcuts-ctx) {:value shortcuts-ctx-value}
[:div {:class (stl/css :shortcuts-content)}
[:> tab-switcher* {:tabs tabs
:selected (name section)
:on-change handle-change-tab
:class (stl/css :shortcuts-switcher)}
(case section
:all
[:> shortcuts-tab-section* {:shortcut-filter (fn [_ shortcut search-term]
(or (str/blank? search-term)
(matches-search (:translation shortcut) search-term)))
:show-restore-all? true
:empty-str (tr "shortcuts.no-shortcuts")
:on-restore-all on-restore-all
:custom-shortcuts custom-shortcuts}]
:personalized
[:> shortcuts-tab-section* {:shortcut-filter (fn [shortcut-key shortcut search-term]
(let [shortcut-group (first (:section shortcut))
group-map (get custom-shortcuts shortcut-group)
group-map (if (map? group-map) group-map {})
customized? (and (contains? group-map shortcut-key)
(not (str/blank? (get group-map shortcut-key))))]
(and customized?
(or (str/blank? search-term)
(matches-search (:translation shortcut) search-term)))))
:show-restore-all? true
:empty-str (tr "shortcuts.no-personalized")
:on-restore-all on-restore-all
:custom-shortcuts custom-shortcuts}]
:disabled
[:> shortcuts-tab-section* {:shortcut-filter (fn [shortcut-key shortcut search-term]
(let [shortcut-group (first (:section shortcut))
group-map (get custom-shortcuts shortcut-group)
group-map (if (map? group-map) group-map {})
in-group? (contains? group-map shortcut-key)
blank? (str/blank? (get group-map shortcut-key))]
(and in-group? blank?
(or (str/blank? search-term)
(matches-search (:translation shortcut) search-term)))))
:show-restore-all? true
:empty-str (tr "shortcuts.no-disabled")
:on-restore-all on-restore-all
:custom-shortcuts custom-shortcuts}])]]
[:div {:class (stl/css :shortcuts-page-footer)}
[:div {:class (stl/css :shortcuts-info)}
[:div {:class (stl/css :shortcuts-info-wrapper)}
[:div {:class (stl/css :dot-wrapper)}
[:div {:class (stl/css :shortcuts-customized-dot)}]]
[:p {:class (stl/css :shortcuts-text)}
(tr "shortcuts.personalized")]]
[:div {:class (stl/css :shortcuts-info-wrapper)}
[:> icon* {:class (stl/css :shortcuts-not-assigned-icon)
:icon-id i/broken-link}]
[:p {:class (stl/css :shortcuts-text)}
(tr "shortcuts.disabled")]]]
[:div {:class (stl/css :export-wrapper)}
[:> button* {:variant "secondary"
:on-click open-menu
:icon i/import-export}
(tr "shortcuts.import-export")]
[:> dropdown-menu* {:show show-menu?
:on-close close-menu
:id "tokens-menu"
:class (stl/css :import-export-menu)}
[:> dropdown-menu-item* {:class (stl/css :import-export-menu-item)
:on-click on-import-file}
[:div {:class (stl/css :import-menu-item)}
[:div (tr "labels.import")]]]
(when has-custom-shortcuts
[:> dropdown-menu-item* {:class (stl/css :import-export-menu-item)
:on-click on-export}
(tr "labels.export")])]]
[:input
{:type "file"
:accept ".json,application/json"
:ref input-ref
:style {:display "none"}
:on-change on-file-selected}]]]]]))

View File

@ -0,0 +1,324 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC Sucursal en España SL
@use "ds/typography.scss" as t;
@use "ds/_borders.scss" as *;
@use "ds/spacing.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
@use "ds/z-index.scss" as *;
.shortcuts-page {
display: flex;
justify-content: center;
padding-block-end: px2rem(32);
}
.shortcuts-list {
@include t.use-typography("body-small");
display: flex;
flex-direction: column;
block-size: 100%;
padding: var(--sp-m);
overflow-y: auto;
color: var(--color-foreground-secondary);
min-block-size: px2rem(250);
}
.shortcuts-content {
display: flex;
flex-direction: column;
max-inline-size: px2rem(590);
inline-size: 100%;
}
.page-title {
color: var(--color-foreground-primary);
inline-size: 100%;
margin-block-start: px2rem(80);
}
.shortcuts-section {
margin-block-end: var(--sp-lg);
}
.shortcuts-search-section {
display: flex;
justify-content: center;
gap: var(--sp-xs);
padding-block-start: var(--sp-s);
}
.restore-all-button {
min-inline-size: px2rem(130);
max-inline-size: px2rem(300);
inline-size: fit-content;
}
// RESTORE ALL MODAL
.modal-overlay {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
inset-inline-start: 0;
inset-block-start: 0;
block-size: 100%;
inline-size: 100%;
z-index: var(--z-index-set);
background-color: var(--overlay-color);
}
.modal-dialog {
position: relative;
padding: var(--sp-xxxl);
border-radius: $br-8;
background-color: var(--color-background-primary);
border: $b-2 solid var(--color-background-quaternary);
display: grid;
grid-template-rows: 0 auto 1fr;
gap: var(--sp-xxxl);
min-inline-size: $sz-364;
min-block-size: $sz-192;
max-block-size: $sz-520;
inline-size: $sz-500;
max-inline-size: $sz-712;
}
.modal-title {
@include t.use-typography("headline-medium");
color: var(--color-foreground-primary);
block-size: px2rem(26);
}
.close-btn {
position: absolute;
inset-block-start: px2rem(8);
inset-inline-end: px2rem(6);
block-size: $sz-32;
inline-size: $sz-28;
}
.modal-content {
display: flex;
flex-direction: column;
gap: var(--sp-l);
block-size: fit-content;
}
.modal-content-text {
@include t.use-typography("body-medium");
color: var(--color-foreground-primary);
}
.modal-footer {
justify-self: end;
display: flex;
flex-direction: row;
gap: var(--sp-s);
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: var(--sp-s);
}
.shortcuts-table {
inline-size: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.shortcuts-list-item,
.shortcuts-list-header {
@include t.use-typography("body-medium");
color: var(--color-foreground-secondary);
display: grid;
grid-template-columns: 40% 30% 30%;
}
.shortcuts-list-header {
border-block-end: $b-1 solid var(--color-foreground-secondary);
block-size: $sz-32;
text-align: start;
padding-block-end: var(--sp-s);
}
.shortcuts-list-body {
display: flex;
flex-direction: column;
gap: var(--sp-s);
}
.shortcuts-list-item {
color: var(--color-foreground-primary);
}
.shortcuts-list-item:first-child {
padding-block-start: var(--sp-s);
}
.shortcut-header-name,
.shortcut-header-command {
text-align: start;
}
.shortcut-command {
display: flex;
gap: var(--sp-s);
align-items: center;
justify-content: start;
text-align: start;
flex-wrap: wrap;
}
.default-command {
@include t.use-typography("body-small");
color: var(--color-foreground-secondary);
background-color: var(--color-background-tertiary);
padding: px2rem(6) var(--sp-xs);
}
.shortcuts-page-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-s);
padding-block-start: var(--sp-m);
}
.shortcuts-info {
display: flex;
gap: var(--sp-s);
align-items: center;
}
.shortcuts-page-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-s);
padding-block-start: var(--sp-m);
}
.shortcuts-info {
@include t.use-typography("body-small");
color: var(--color-foreground-secondary);
}
.shortcuts-customized-dot,
.shortcuts-conflict-dot {
inline-size: px2rem(4);
block-size: px2rem(4);
border-radius: 50%;
background-color: var(--color-accent-info);
}
.shortcuts-conflict-dot {
background-color: var(--color-accent-error);
}
.shortcuts-info {
display: flex;
gap: var(--sp-m);
}
.shortcuts-info-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: var(--sp-xxs);
}
.dot-wrapper {
display: flex;
align-items: center;
justify-content: center;
block-size: px2rem(16);
inline-size: px2rem(16);
}
.shortcuts-text {
@include t.use-typography("body-small");
color: var(--color-foreground-secondary);
margin: 0;
}
.shortcuts-not-assigned-icon {
margin-inline-end: var(--sp-xs);
}
.export-wrapper {
position: relative;
}
.import-export-menu {
box-shadow: 0 0 px2rem(12) 0 var(--color-shadow-dark);
display: flex;
flex-direction: column;
gap: var(--sp-xs);
position: absolute;
padding: var(--sp-xs);
border-radius: $br-8;
z-index: var(--z-index-dropdown);
color: var(--color-foreground-primary);
background-color: var(--color-background-tertiary);
border: $b-2 solid var(--color-background-quaternary);
inset-block-start: calc(-1 * px2rem(6));
inset-inline-end: 0;
translate: 0 -100%;
inline-size: px2rem(192);
margin: 0;
}
.import-export-menu-item {
@include t.use-typography("body-medium");
display: flex;
align-items: center;
justify-content: space-between;
block-size: $sz-28;
inline-size: 100%;
padding: px2rem(6);
border-radius: $br-8;
cursor: pointer;
&:hover {
background-color: var(--color-background-quaternary);
color: var(--color-foreground-primary);
}
}
.import-export-menu-item-icon {
display: flex;
align-items: center;
justify-content: center;
}
.shortcuts-empty-state {
display: flex;
justify-content: center;
align-items: center;
block-size: px2rem(250);
}
.shortcuts-description {
color: var(--color-foreground-secondary);
display: flex;
align-items: center;
gap: var(--sp-xs);
padding: var(--sp-s) 0;
margin-block-end: var(--sp-l);
}

View File

@ -48,6 +48,9 @@
(def ^:private go-settings-notifications (def ^:private go-settings-notifications
#(st/emit! (rt/nav :settings-notifications))) #(st/emit! (rt/nav :settings-notifications)))
(def ^:private go-settings-shortcuts
#(st/emit! (rt/nav :settings-shortcuts)))
(defn- show-release-notes (defn- show-release-notes
[event] [event]
(let [version (:main cf/version)] (let [version (:main cf/version)]
@ -66,6 +69,7 @@
subscription? (= section :settings-subscription) subscription? (= section :settings-subscription)
integrations? (= section :settings-integrations) integrations? (= section :settings-integrations)
notifications? (= section :settings-notifications) notifications? (= section :settings-notifications)
shortcuts? (= section :settings-shortcuts)
team-id (or (dtm/get-last-team-id) team-id (or (dtm/get-last-team-id)
(:default-team-id profile)) (:default-team-id profile))
@ -83,7 +87,8 @@
[:hr {:class (stl/css :sidebar-separator)}] [:hr {:class (stl/css :sidebar-separator)}]
[:div {:class (stl/css :sidebar-content-section)} [:nav {:class (stl/css :sidebar-content-section)
:aria-label (tr "labels.settings")}
[:ul {:class (stl/css :sidebar-nav-settings)} [:ul {:class (stl/css :sidebar-nav-settings)}
[:li {:class (stl/css-case :current profile? [:li {:class (stl/css-case :current profile?
:settings-item true) :settings-item true)
@ -100,6 +105,12 @@
:on-click go-settings-notifications} :on-click go-settings-notifications}
[:span {:class (stl/css :element-title)} (tr "labels.notifications")]] [:span {:class (stl/css :element-title)} (tr "labels.notifications")]]
(when (contains? cf/flags :custom-shortcuts)
[:li {:class (stl/css-case :current shortcuts?
:settings-item true)
:on-click go-settings-shortcuts}
[:span {:class (stl/css :element-title)} (tr "label.shortcuts")]])
[:li {:class (stl/css-case :current options? [:li {:class (stl/css-case :current options?
:settings-item true) :settings-item true)
:on-click go-settings-options :on-click go-settings-options
@ -137,7 +148,7 @@
(mf/defc sidebar* (mf/defc sidebar*
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [profile section]}] [{:keys [profile section]}]
[:div {:class (stl/css :dashboard-sidebar :settings)} [:aside {:class (stl/css :dashboard-sidebar :settings)}
[:> sidebar-content* {:profile profile [:> sidebar-content* {:profile profile
:section section}] :section section}]
[:> profile-section* {:profile profile}]]) [:> profile-section* {:profile profile}]])

View File

@ -565,10 +565,10 @@
(tr "subscription.settings.enterprise")))})) (tr "subscription.settings.enterprise")))}))
(rt/nav :settings-subscription {} {::rt/replace true}))))) (rt/nav :settings-subscription {} {::rt/replace true})))))
[:section {:class (stl/css :dashboard-section)} [:section {:class (stl/css :dashboard-section)
:aria-labelledby "subscription-section-title"}
[:div {:class (stl/css :dashboard-content)} [:div {:class (stl/css :dashboard-content)}
[:h2 {:class (stl/css :title-section)} (tr "subscription.labels")] [:h2 {:id "subscription-section-title" :class (stl/css :title-section)} (tr "subscription.labels")]
[:div {:class (stl/css :your-subscription)} [:div {:class (stl/css :your-subscription)}
[:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.section-plan")] [:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.section-plan")]

View File

@ -0,0 +1,700 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.main.ui.shortcuts
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.config :as cf]
[app.main.data.dashboard.shortcuts.customize :as customize]
[app.main.data.profile :as du]
[app.main.data.shortcuts :as ds]
[app.main.store :as st]
[app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.ds.tooltip.tooltip :refer [tooltip*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.strings :refer [matches-search]]
[cuerdas.core :as str]
[goog.events :as events]
[rumext.v2 :as mf]))
(def ^:private modifier-keys
#{"Control" "Shift" "Alt" "Meta"})
(def ^:private key-name-map
{"ArrowUp" "up"
"ArrowDown" "down"
"ArrowLeft" "left"
"ArrowRight" "right"
"Escape" "escape"
"Enter" "enter"
"Backspace" "backspace"
"Delete" "del"
"Tab" "tab"
" " "space"})
(defn- keyboard-event->mousetrap
[^js event]
(let [parts (cond-> []
(and (.-ctrlKey event)
(not (cf/check-platform? :macos)))
(conj "ctrl")
(and (.-metaKey event)
(cf/check-platform? :macos))
(conj "command")
(.-altKey event)
(conj "alt")
(.-shiftKey event)
(conj "shift"))
key (.-key event)
key (if (contains? modifier-keys key)
nil
(or (get key-name-map key)
(.toLowerCase key)))]
(when key
(str/join "+" (conj parts key)))))
(defn- keyboard-event->display-parts
[^js event]
(let [parts (cond-> []
(and (.-ctrlKey event)
(not (cf/check-platform? :macos)))
(conj "ctrl")
(and (.-metaKey event)
(cf/check-platform? :macos))
(conj "command")
(.-altKey event)
(conj "alt")
(.-shiftKey event)
(conj "shift"))
key (.-key event)]
(if (contains? modifier-keys key)
{:modifiers parts :finalized? false}
{:modifiers parts
:final-key (or (get key-name-map key) (.toLowerCase key))
:finalized? true})))
(defn translation-keyname
[type keyname]
(let [translat-pre (case type
:sc "shortcuts."
:sec "shortcut-section."
:sub-sec "shortcut-subsection.")]
(tr (str translat-pre (d/name keyname)))))
(defn- find-conflict
[new-command all-shortcuts current-key]
(let [command-index (ds/build-command-index all-shortcuts)]
(when-let [conflicting-key (get command-index new-command)]
(when (not= conflicting-key current-key)
{:key conflicting-key
:name (translation-keyname :sc conflicting-key)}))))
(def ^:private import-contexts
[:workspace :dashboard :viewer])
(defn- import-context-group
"Imports a single context group from the payload, disabling any default
shortcut whose command collides with a newly imported one, and any
previously-imported entry in the same batch with a duplicate command."
[group all-shortcuts]
(reduce
(fn [acc [command recorded-command]]
(let [default-conflict (find-conflict recorded-command all-shortcuts command)
acc-conflict (some (fn [[k v]]
(when (and (not= k command) (= v recorded-command))
k))
acc)]
(cond-> (assoc acc command recorded-command)
(:key default-conflict)
(assoc (:key default-conflict) "")
acc-conflict
(assoc acc-conflict ""))))
{}
group))
(defn import-custom-shortcuts
[shortcuts all-shortcuts]
(let [current-customs (or (get-in shortcuts [:profile :props :custom-shortcuts])
(get-in @st/state [:profile :props :custom-shortcuts])
{})
new-customs (reduce
(fn [acc ctx]
(if (contains? shortcuts ctx)
(assoc acc ctx (import-context-group (get shortcuts ctx) all-shortcuts))
acc))
current-customs
import-contexts)]
(du/update-profile-props {:custom-shortcuts new-customs})))
(defn add-translation
[type item]
(map (fn [[k v]] [k (assoc v :translation (translation-keyname type k))]) item))
(defn shortcuts->subsections
[shortcuts]
(let [subsections (into #{} (mapcat :subsections) (vals shortcuts))
subsections (vec (sort-by name subsections))
get-sc-by-subsection
(fn [subsection [k v]]
(when (some #(= subsection %) (:subsections v)) {k v}))
reduce-sc
(fn [acc subsection]
(let [shortcuts-by-subsection (into {} (keep (partial get-sc-by-subsection subsection) shortcuts))]
(assoc acc subsection {:children shortcuts-by-subsection})))]
(reduce reduce-sc (sorted-map-by (fn [a b] (compare (name a) (name b)))) subsections)))
(defn build-all-shortcuts
[workspace-sc dashboard-sc viewer-sc]
(let [walk (fn walk [element parent-id]
(if (nil? element)
element
(let [sorted-comp (when (sorted? element)
#(compare (name %1) (name %2)))
rec-fn (fn [[k item]]
(let [item-id (if (nil? parent-id)
[k]
(conj parent-id k))]
[k (assoc item :id item-id :children (walk (:children item) item-id))]))]
(if sorted-comp
(into (sorted-map-by sorted-comp) (map rec-fn element))
(into {} (map rec-fn element))))))
sorted-map-fn #(sorted-map-by (fn [a b] (compare (name a) (name b))))
ws-subs (->> (shortcuts->subsections workspace-sc)
(add-translation :sub-sec)
(into (sorted-map-fn)))
db-subs (->> (shortcuts->subsections dashboard-sc)
(add-translation :sub-sec)
(into (sorted-map-fn)))
vw-subs (->> (shortcuts->subsections viewer-sc)
(add-translation :sub-sec)
(into (sorted-map-fn)))
basics (into {} (concat (:children (:basics ws-subs))
(:children (:basics db-subs))
(:children (:basics vw-subs))))
ws-subs (dissoc ws-subs :basics)
db-subs (dissoc db-subs :basics)
vw-subs (dissoc vw-subs :basics)
all {:basics {:children {:none {:children basics}}
:translation (tr "shortcut-section.basics")}
:workspace {:children ws-subs
:translation (tr "shortcut-section.workspace")}
:dashboard {:children db-subs
:translation (tr "shortcut-section.dashboard")}
:viewer {:children vw-subs
:translation (tr "shortcut-section.viewer")}}
all (walk all nil)
all-sc-names (map #(translation-keyname :sc %)
(concat (keys workspace-sc)
(keys dashboard-sc)
(keys viewer-sc)))
all-sub-names (map #(translation-keyname :sub-sec %)
(concat (keys ws-subs)
(keys db-subs)
(keys vw-subs)))
all-section-names (map #(translation-keyname :sec %) (keys all))]
{:all-shortcuts all
:all-sc-names all-sc-names
:all-sub-names all-sub-names
:all-section-names all-section-names}))
(defn build-all-shortcuts-without-basics
[workspace-sc path-sc dashboard-sc viewer-sc
& {:keys [workspace-orig path-orig dashboard-orig viewer-orig]}]
(let [original-commands (into {}
(concat
(map (fn [[k v]] [k (:command v)]) workspace-orig)
(map (fn [[k v]] [k (:command v)]) path-orig)
(map (fn [[k v]] [k (:command v)]) dashboard-orig)
(map (fn [[k v]] [k (:command v)]) viewer-orig)))
walk (fn walk [element parent-id]
(if (nil? element)
element
(let [sorted-comp (when (sorted? element)
#(compare (name %1) (name %2)))
rec-fn (fn [[k item]]
(let [item-id (if (nil? parent-id)
[k]
(conj parent-id k))
item (if (:command item)
(assoc item :original-command (get original-commands k (:command item)))
item)]
[k (assoc item :id item-id :children (walk (:children item) item-id))]))]
(if sorted-comp
(into (sorted-map-by sorted-comp) (map rec-fn element))
(into {} (map rec-fn element))))))
sorted-map-fn #(sorted-map-by (fn [a b] (compare (name a) (name b))))
ws-subs (->> (shortcuts->subsections workspace-sc)
(add-translation :sub-sec)
(into (sorted-map-fn)))
pt-subs (->> (shortcuts->subsections path-sc)
(add-translation :sub-sec)
(into (sorted-map-fn)))
db-subs (->> (shortcuts->subsections dashboard-sc)
(add-translation :sub-sec)
(into (sorted-map-fn)))
vw-subs (->> (shortcuts->subsections viewer-sc)
(add-translation :sub-sec)
(into (sorted-map-fn)))
ws-subs (dissoc ws-subs :basics)
pt-subs (dissoc pt-subs :basics)
db-subs (dissoc db-subs :basics)
vw-subs (dissoc vw-subs :basics)
ws-subs (merge ws-subs pt-subs)
all {:workspace {:children ws-subs
:translation (tr "shortcut-section.workspace")}
:dashboard {:children db-subs
:translation (tr "shortcut-section.dashboard")}
:viewer {:children vw-subs
:translation (tr "shortcut-section.viewer")}}
all (walk all nil)
all-sc-names (map #(translation-keyname :sc %)
(concat (keys workspace-sc)
(keys path-sc)
(keys dashboard-sc)
(keys viewer-sc)))
all-sub-names (map #(translation-keyname :sub-sec %)
(concat (keys ws-subs)
(keys db-subs)
(keys vw-subs)))
all-section-names (map #(translation-keyname :sec %) (keys all))]
{:all-shortcuts all
:all-sc-names all-sc-names
:all-sub-names all-sub-names
:all-section-names all-section-names}))
(mf/defc converted-chars*
[{:keys [char command class]}]
(let [modified-keys {:up ds/up-arrow
:down ds/down-arrow
:left ds/left-arrow
:right ds/right-arrow
:plus "+"}
macos-keys {:command "\u2318"
:option "\u2325"
:alt "\u2325"
:delete "\u232B"
:del "\u232B"
:shift "\u21E7"
:control "\u2303"
:esc "\u238B"
:enter "\u23CE"}
is-macos? (cf/check-platform? :macos)
char (if (contains? modified-keys (keyword char)) ((keyword char) modified-keys) char)
char (if (and is-macos? (contains? macos-keys (keyword char))) ((keyword char) macos-keys) char)
unique-key (str (d/name command) "-" char)]
[:span {:class [class (stl/css :key)]
:key unique-key} char]))
(mf/defc shortcuts-keys*
[{:keys [content command is-customized has-conflict?]}]
(let [managed-list (if (coll? content)
content
(conj () content))
chars-list (map ds/split-sc managed-list)
last-element (last chars-list)
short-char-list (if (= 1 (count chars-list))
chars-list
(drop-last chars-list))
penultimate (last short-char-list)]
[:span {:class (stl/css :keys)}
(for [chars short-char-list]
[:* {:key (str/join chars)}
(for [char chars]
[:> converted-chars* {:key (dm/str char "-" (name command))
:char char
:command command
:class (cond
has-conflict? (stl/css :conflict-key)
is-customized (stl/css :customized-key))}])
(when (not= chars penultimate) [:span {:class (stl/css :space)} ","])])
(when (not= last-element penultimate)
[:*
[:span {:class (stl/css :space)} (tr "shortcuts.or")]
(for [char last-element]
[:> converted-chars* {:key (dm/str char "-" (name command))
:char char
:command command
:class (cond
has-conflict? (stl/css :conflict-key)
is-customized (stl/css :customized-key))}])])]))
(mf/defc shortcut-row-editable*
[{:keys [elements custom-shortcuts command-translate conflicts section-key]}]
(let [{:keys [workspace-sc-trans dashboard-sc-trans viewer-sc-trans]} (mf/use-ctx ctx/shortcuts-ctx)
section-shortcuts (case section-key
(:workspace :path) workspace-sc-trans
:dashboard dashboard-sc-trans
:viewer viewer-sc-trans
workspace-sc-trans)
sc-by-translate (first (filter #(= (:translation (second %)) command-translate) elements))
[command command-info] sc-by-translate
content (or (:show-command command-info) (:command command-info))
group-map (if (= section-key :basics)
(merge (get custom-shortcuts :workspace)
(get custom-shortcuts :dashboard)
(get custom-shortcuts :viewer))
(get custom-shortcuts section-key))
group-map (if (map? group-map) group-map {})
customized? (contains? group-map command)
has-conflict? (contains? conflicts command)
is-editing* (mf/use-state false)
is-editing (deref is-editing*)
recording-ref (mf/use-ref nil)
display-parts* (mf/use-state nil)
display-parts (deref display-parts*)
recorded-command* (mf/use-state nil)
recorded-command (deref recorded-command*)
conflict* (mf/use-state nil)
conflict (deref conflict*)
clean-editing-state
(fn []
(reset! is-editing* false)
(reset! display-parts* nil)
(reset! recorded-command* nil)
(reset! conflict* nil))
effective-section-key (if (= section-key :basics) :workspace section-key)
on-reset-shortcut
(mf/use-fn
(mf/deps effective-section-key command-info)
(fn [shortcut-key]
(st/emit! (customize/reset-custom-shortcut shortcut-key (:original-command command-info) effective-section-key))
(clean-editing-state)))
start-editing
(mf/use-fn
(fn [_]
(reset! is-editing* true)
(reset! display-parts* nil)
(reset! recorded-command* nil)
(reset! conflict* nil)))
stop-editing
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(clean-editing-state)))
disabled-shortcut
(mf/use-fn
(mf/deps command effective-section-key)
(fn [event]
(dom/prevent-default event)
(st/emit! (customize/set-custom-shortcut
command
""
nil
effective-section-key))
(clean-editing-state)))
save-fn
(mf/use-fn
(mf/deps recorded-command conflict command effective-section-key)
(fn [event]
(dom/prevent-default event)
(when recorded-command
(st/emit! (customize/set-custom-shortcut
command
recorded-command
(:key conflict)
effective-section-key)))
(clean-editing-state)))
on-editable-container-blur
(mf/use-fn
(fn [e]
(when-not (.contains (.-currentTarget e)
(.-relatedTarget e))
(clean-editing-state))))]
(mf/with-effect [is-editing]
(when is-editing
(some-> (mf/ref-val recording-ref) (dom/focus!))))
(mf/with-effect [is-editing]
(when is-editing
(letfn [(on-keydown [^js event]
(.preventDefault event)
(.stopPropagation event)
(let [recorded-command (keyboard-event->mousetrap event)
parts (keyboard-event->display-parts event)]
(reset! display-parts* parts)
(when recorded-command
(reset! recorded-command* recorded-command)
(reset! conflict* (find-conflict recorded-command section-shortcuts command)))))]
(->> (events/listen (mf/ref-val recording-ref) "keydown" on-keydown)
(partial events/unlistenByKey)))))
[:li {:class (stl/css-case :shortcuts-name-editable true
:shortcuts-editing is-editing
:customized customized?)
:data-customized (str customized?)
:data-conflict (str has-conflict?)
:aria-label command-translate
:key command-translate}
[:button {:on-click start-editing
:disabled is-editing
:aria-label (dm/str "Edit " command-translate)
:class (stl/css-case :shortcut-button-editable true
:shortcut-button-editing is-editing)}
[:span {:class (stl/css :command-name)
:id (dm/str command-translate "-label")}
command-translate]
[:div {:class (stl/css :shortcut-actions)
:aria-labelledby (dm/str command-translate "-label")}
(if (and customized? (str/blank? content))
[:> icon* {:icon-id i/broken-link
:size "s"
:class (stl/css :shortcut-detach-icon)}]
(if has-conflict?
(let [conflicting-key (get conflicts command)
conflict-name (translation-keyname :sc conflicting-key)]
[:> tooltip* {:content (tr "shortcuts.overwritten-by" conflict-name)
:id (dm/str (name command) "-conflict-tooltip")
:class (stl/css :option-text)}
[:> shortcuts-keys* {:content content
:command command
:is-customized customized?
:has-conflict? has-conflict?}]])
[:> shortcuts-keys* {:content content
:command command
:is-customized customized?
:has-conflict? has-conflict?}]))]]
(when is-editing
[:div {:class (stl/css :shortcut-editing)
:ref recording-ref
:tab-index 0
:on-blur on-editable-container-blur}
[:div {:class (stl/css :shortcut-recording-area)}
(if (nil? display-parts)
[:span {:class (stl/css :shortcut-placeholder)}
(tr "shortcuts.key-combo")]
[:span {:class (stl/css :shortcut-recorded-keys)}
(for [mod (:modifiers display-parts)]
[:span {:class (stl/css-case :key true
:customized-key customized?) :key mod} mod])
(when (:final-key display-parts)
[:span {:class (stl/css-case :key true
:customized-key customized?)} (:final-key display-parts)])
(when-not (:finalized? display-parts)
[:span {:class (stl/css :recording-ellipsis)} "..."])])]
(when recorded-command
(if conflict
[:> context-notification* {:level :warning :class (stl/css :modal-error-msg)}
(tr "shortcuts.edit-modal.conflict" (:name conflict))]
[:> context-notification* {:level :success :class (stl/css :modal-success-msg)}
(tr "shortcuts.edit-modal.success")]))
[:div {:class (stl/css :edit-buttons)}
[:div {:class (stl/css :confirmation-buttons)}
[:> icon-button* {:variant "secondary"
:aria-label (tr "labels.cancel")
:on-click stop-editing
:icon i/close
:icon-size "m"}]
[:> icon-button* {:variant "primary"
:aria-label (tr "labels.save")
:on-click save-fn
:icon i/tick
:icon-size "m"}]]
[:div {:class (stl/css :confirmation-buttons)}
[:> icon-button* {:variant "secondary"
:aria-label (tr "shortcuts.disable")
:on-click (fn [e]
(dom/stop-propagation e)
(disabled-shortcut command))
:icon i/broken-link
:icon-size "m"}]
(when customized?
[:> icon-button* {:variant "secondary"
:aria-label (tr "shortcuts.reset")
:on-click (fn [e]
(dom/stop-propagation e)
(on-reset-shortcut command))
:icon i/reload
:icon-size "m"}])]]])]))
(mf/defc shortcut-row*
[{:keys [elements filter-term is-match-section is-match-subsection
editable? custom-shortcuts section-key conflicts hidden subsection-name]}]
(let [shortcut-translations (->> elements vals (map :translation) sort)
match-shortcut? (some #(matches-search % filter-term) shortcut-translations)
filtered (if (and (or is-match-section is-match-subsection) (not match-shortcut?))
shortcut-translations
(filter #(matches-search % filter-term) shortcut-translations))
sorted-filtered (sort filtered)
trigger-ref (mf/use-ref nil)]
[:ul {:class (stl/css :sub-menu)
:id (dm/str (d/name subsection-name) "-subsection")
:hidden hidden}
(for [command-translate sorted-filtered]
(let [sc-by-translate (first (filter #(= (:translation (second %)) command-translate) elements))
[command command-info] sc-by-translate
content (or (:show-command command-info) (:command command-info))
group-map (if (= section-key :basics)
(merge (get custom-shortcuts :workspace)
(get custom-shortcuts :dashboard)
(get custom-shortcuts :viewer))
(get custom-shortcuts section-key))
group-map (if (map? group-map) group-map {})
customized? (contains? group-map command)
has-conflict? (contains? conflicts command)]
(if editable?
[:> shortcut-row-editable* {:elements elements
:custom-shortcuts custom-shortcuts
:section-key section-key
:key command-translate
:command-translate command-translate
:conflicts conflicts}]
[:li {:class (stl/css-case :shortcuts-name true
:customized customized?)
:data-customized (str customized?)
:data-conflict (str has-conflict?)
:aria-label command-translate
:key command-translate}
[:span {:class (stl/css :command-name)
:id (dm/str command-translate "-label")}
command-translate]
[:div {:class (stl/css :shortcut-actions)
:aria-labelledby (dm/str command-translate "-label")}
(if (and customized? (str/blank? content))
[:> tooltip* {:content (tr "shortcuts.disabled-shortcut")
:trigger-ref trigger-ref
:id (dm/str command-translate "-tooltip")
:class (stl/css :option-text)}
[:> icon* {:icon-id i/broken-link
:aria-labelledby (dm/str command-translate "-tooltip")
:size "s"
:ref trigger-ref
:class (stl/css :shortcut-detach-icon)}]]
(if has-conflict?
(let [conflicting-key (get conflicts command)
conflict-name (translation-keyname :sc conflicting-key)]
[:> tooltip* {:content (tr "shortcuts.overwritten-by" conflict-name)
:id (dm/str (name command) "-conflict-tooltip")
:class (stl/css :option-text)}
[:> shortcuts-keys* {:content content
:command command
:is-customized customized?
:has-conflict? has-conflict?}]])
[:> shortcuts-keys* {:content content
:command command
:is-customized customized?
:has-conflict? has-conflict?}]))]])))]))
(mf/defc section-title*
[{:keys [name is-visible is-sub on-click]}]
(let [handle-key-down
(fn [event]
(when (or (= "Enter" (.-key event))
(= " " (.-key event)))
(.preventDefault event)
(on-click event)))]
[:button {:class (if is-sub
(stl/css :subsection-title)
(stl/css :section-title))
:on-click on-click
:on-key-down handle-key-down
:aria-expanded is-visible
:aria-controls (dm/str (str/lower name) (if is-sub "-subsection" "-section"))}
[:> icon* {:icon-id (if is-visible i/arrow-down i/arrow-right)
:size "s"
:aria-hidden true}]
[:span {:class (if is-sub
(stl/css :subsection-name)
(stl/css :section-name))} name]]))
(mf/defc shortcut-subsection*
[{:keys [subsections manage-sections filter-term is-match-section open-sections
editable? custom-shortcuts section-key conflicts hidden]}]
(if (= :none (first (keys subsections)))
[:> shortcut-row* {:elements (:children (:none subsections))
:filter-term filter-term
:is-match-section is-match-section
:is-match-subsection true
:editable? editable?
:hidden hidden
:custom-shortcuts custom-shortcuts
:section-key section-key
:conflicts conflicts}]
[:ul {:class (stl/css :subsection-menu)
:hidden hidden
:id (dm/str (name section-key) "-section")}
(let [sorted-subs (sort-by (fn [[_ v]] (:translation v)) subsections)]
(for [[sub-name sub-info] sorted-subs]
(let [visible? (some #(= % (:id sub-info)) open-sections)]
[:li {:key (name sub-name)}
[:h4
[:> section-title* {:name (:translation sub-info)
:is-visible visible?
:is-sub true
:on-click (manage-sections (:id sub-info))}]]
[:> shortcut-row* {:elements (:children sub-info)
:subsection-name sub-name
:filter-term filter-term
:is-match-section is-match-section
:is-match-subsection true
:editable? editable?
:hidden (not visible?)
:custom-shortcuts custom-shortcuts
:section-key section-key
:conflicts conflicts}]])))]))
(mf/defc shortcut-section*
[{:keys [section manage-sections open-sections filter-term
editable? custom-shortcuts conflicts]}]
(let [[section-key section-info] section
section-id (:id section-info)
section-translation (:translation section-info)
subsections (:children section-info)
visible? (some #(= % section-id) open-sections)]
[:div {:class (stl/css :section)}
[:h3 {:class (stl/css :section-header)}
[:> section-title* {:name section-translation
:is-visible visible?
:is-sub false
:on-click (manage-sections section-id)}]]
[:> shortcut-subsection* {:subsections subsections
:open-sections open-sections
:manage-sections manage-sections
:is-match-section false
:filter-term filter-term
:editable? editable?
:hidden (not visible?)
:custom-shortcuts custom-shortcuts
:section-key section-key
:conflicts conflicts}]]))

View File

@ -0,0 +1,214 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC Sucursal en España SL
@use "refactor/common-refactor.scss" as deprecated;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
@use "ds/mixins.scss" as *;
@use "ds/typography.scss" as t;
.section {
margin: 0;
}
.section-header {
padding: 0;
}
.section-title,
.subsection-title {
@include t.use-typography("title-small");
display: flex;
align-items: center;
margin: 0;
padding: var(--sp-s) 0;
color: var(--color-foreground-secondary);
cursor: pointer;
border: none;
background: none;
.subsection-name,
.section-name {
padding-inline-start: var(--sp-xs);
}
&:hover {
color: var(--color-foreground-primary);
}
}
.subsection-title {
text-transform: none;
padding-inline-start: var(--sp-m);
}
.subsection-menu {
margin-block-end: var(--sp-s);
}
.sub-menu {
margin-block-end: var(--sp-s);
}
.shortcuts-name {
display: flex;
align-items: center;
justify-content: space-between;
inline-size: 100%;
min-block-size: $sz-32;
padding: px2rem(6);
margin-block-end: var(--sp-s);
border-radius: $br-8;
background-color: var(--color-background-tertiary);
}
.command-name {
@include t.use-typography("body-small");
margin-inline-start: var(--sp-xxs);
color: var(--color-foreground-primary);
flex: 1;
min-inline-size: 0;
text-align: start;
}
// Editable rows
.shortcuts-name-editable {
inline-size: 100%;
min-block-size: $sz-32;
padding: px2rem(6);
margin-block-end: var(--sp-s);
border-radius: $br-8;
background-color: var(--color-background-tertiary);
&:hover {
background-color: var(--color-background-quaternary);
}
}
.shortcuts-editing {
background-color: var(--color-background-quaternary);
display: flex;
flex-direction: column;
gap: var(--sp-s);
&:focus-within {
border: $b-1 solid var(--color-accent-primary);
}
}
.shortcut-button-editable {
border: none;
background: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
inline-size: 100%;
}
.shortcut-button-editing {
gap: var(--sp-s);
}
.shortcut-editing {
display: flex;
flex-direction: column;
gap: var(--sp-s);
outline: none;
}
.shortcut-recording-area {
inline-size: 100%;
padding: px2rem(24);
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-background-tertiary);
border-radius: $br-8;
border: $b-1 dashed var(--color-foreground-secondary);
}
.shortcut-actions {
display: flex;
align-items: center;
gap: var(--sp-xxs);
}
.edit-buttons {
display: flex;
align-items: center;
transition: opacity 0.15s ease;
justify-content: space-between;
flex-direction: row-reverse;
}
.confirmation-buttons {
display: flex;
align-items: center;
gap: var(--sp-s);
justify-content: flex-end;
}
.keys {
display: flex;
justify-content: center;
align-items: center;
gap: var(--sp-xxs);
}
.key {
@include t.use-typography("body-small");
display: flex;
justify-content: center;
align-items: center;
text-transform: capitalize;
block-size: px2rem(20);
padding: px2rem(2) px2rem(6);
border-radius: $br-6;
color: var(--color-foreground-primary);
background-color: var(--color-background-primary);
}
.customized-key {
background-color: var(--color-background-info);
color: var(--color-foreground-primary);
}
.conflict-key {
background-color: var(--color-background-error);
color: var(--color-foreground-error);
}
.space {
margin: 0 var(--sp-xxs);
color: var(--color-foreground-secondary);
}
.shortcut-placeholder {
@include t.use-typography("body-small");
color: var(--color-foreground-secondary);
}
.shortcut-recorded-keys {
display: flex;
justify-content: center;
align-items: center;
gap: var(--sp-xxs);
}
.shortcut-detach-icon {
color: var(--color-foreground-secondary);
}
.recording-ellipsis {
color: var(--color-foreground-secondary);
}

View File

@ -406,7 +406,7 @@
(when (not (dom/fullscreen?)) (when (not (dom/fullscreen?))
(st/emit! (dv/exit-fullscreen)))))] (st/emit! (dv/exit-fullscreen)))))]
(hooks/use-shortcuts ::viewer sc/shortcuts) (hooks/use-shortcuts ::viewer sc/shortcuts :viewer)
(when (nil? page) (when (nil? page)
(ex/raise :type :not-found)) (ex/raise :type :not-found))

View File

@ -788,7 +788,7 @@
(combine-groups-with-resolved color-tokens)))] (combine-groups-with-resolved color-tokens)))]
(mf/with-effect [] (mf/with-effect []
(st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts))) (st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts :workspace)))
(fn [] (fn []
(st/emit! (dsc/pop-shortcuts ::colorpicker)) (st/emit! (dsc/pop-shortcuts ::colorpicker))
(when (and @dirty? @last-change on-close) (when (and @dirty? @last-change on-close)

View File

@ -56,9 +56,14 @@
(mf/defc menu-entry* (mf/defc menu-entry*
{::mf/private true} {::mf/private true}
[{:keys [title shortcut on-click on-pointer-enter on-pointer-leave [{:keys [title shortcut shortcut-key on-click on-pointer-enter on-pointer-leave
on-unmount children is-selected icon disabled value]}] on-unmount children is-selected icon disabled value]}]
(let [submenu-ref (mf/use-ref nil) (let [cs (mf/deref refs/custom-shortcuts)
customized? (and shortcut-key
(let [c (get-in cs [:workspace shortcut-key])]
(and c (not= c ""))))
submenu-ref (mf/use-ref nil)
hovering? (mf/use-ref false) hovering? (mf/use-ref false)
on-click' on-click'
@ -128,7 +133,8 @@
[:span {:class (stl/css :shortcut)} [:span {:class (stl/css :shortcut)}
(for [[idx sc] (d/enumerate (scd/split-sc shortcut))] (for [[idx sc] (d/enumerate (scd/split-sc shortcut))]
[:span {:key (dm/str shortcut "-" idx) [:span {:key (dm/str shortcut "-" idx)
:class (stl/css :shortcut-key)} sc])]) :class (stl/css-case :shortcut-key true
:customized-key customized?)} sc])])
(when (> (count children) 1) (when (> (count children) 1)
[:span {:class (stl/css :submenu-icon)} [:span {:class (stl/css :submenu-icon)}
@ -149,7 +155,10 @@
(mf/defc context-menu-edit* (mf/defc context-menu-edit*
{::mf/private true} {::mf/private true}
[{:keys [shapes]}] [{:keys [shapes]}]
(let [multiple? (> (count shapes) 1) (let [custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
multiple? (> (count shapes) 1)
do-copy #(st/emit! (dw/copy-selected)) do-copy #(st/emit! (dw/copy-selected))
do-copy-link #(st/emit! (dw/copy-link-to-clipboard)) do-copy-link #(st/emit! (dw/copy-link-to-clipboard))
@ -207,19 +216,24 @@
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-id") [:> menu-entry* {:title (tr "workspace.shape.menu.copy-id")
:on-click do-copy-id}]) :on-click do-copy-id}])
[:> menu-entry* {:title (tr "workspace.shape.menu.copy") [:> menu-entry* {:title (tr "workspace.shape.menu.copy")
:shortcut (sc/get-tooltip :copy) :shortcut (get-tt :copy)
:shortcut-key :copy
:on-click do-copy}] :on-click do-copy}]
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-link") [:> menu-entry* {:title (tr "workspace.shape.menu.copy-link")
:shortcut (sc/get-tooltip :copy-link) :shortcut (get-tt :copy-link)
:shortcut-key :copy-link
:on-click do-copy-link}] :on-click do-copy-link}]
[:> menu-entry* {:title (tr "workspace.shape.menu.cut") [:> menu-entry* {:title (tr "workspace.shape.menu.cut")
:shortcut (sc/get-tooltip :cut) :shortcut (get-tt :cut)
:shortcut-key :cut
:on-click do-cut}] :on-click do-cut}]
[:> menu-entry* {:title (tr "workspace.shape.menu.paste") [:> menu-entry* {:title (tr "workspace.shape.menu.paste")
:shortcut (sc/get-tooltip :paste) :shortcut (get-tt :paste)
:shortcut-key :paste
:on-click do-paste}] :on-click do-paste}]
[:> menu-entry* {:title (tr "workspace.shape.menu.duplicate") [:> menu-entry* {:title (tr "workspace.shape.menu.duplicate")
:shortcut (sc/get-tooltip :duplicate) :shortcut (get-tt :duplicate)
:shortcut-key :duplicate
:on-click do-duplicate}] :on-click do-duplicate}]
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-paste-as") [:> menu-entry* {:title (tr "workspace.shape.menu.copy-paste-as")
@ -242,11 +256,13 @@
:on-click handle-copy-text}] :on-click handle-copy-text}]
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-props") [:> menu-entry* {:title (tr "workspace.shape.menu.copy-props")
:shortcut (sc/get-tooltip :copy-props) :shortcut (get-tt :copy-props)
:shortcut-key :copy-props
:disabled multiple? :disabled multiple?
:on-click handle-copy-props}] :on-click handle-copy-props}]
[:> menu-entry* {:title (tr "workspace.shape.menu.paste-props") [:> menu-entry* {:title (tr "workspace.shape.menu.paste-props")
:shortcut (sc/get-tooltip :paste-props) :shortcut (get-tt :paste-props)
:shortcut-key :paste-props
:disabled (and (cf/check-browser? :chrome) (not @enabled-paste-props*)) :disabled (and (cf/check-browser? :chrome) (not @enabled-paste-props*))
:on-click handle-paste-props}]] :on-click handle-paste-props}]]
@ -255,7 +271,10 @@
(mf/defc context-menu-layer-position* (mf/defc context-menu-layer-position*
{::mf/private true} {::mf/private true}
[{:keys [shapes]}] [{:keys [shapes]}]
(let [do-bring-forward (mf/use-fn #(st/emit! (dw/vertical-order-selected :up))) (let [custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
do-bring-forward (mf/use-fn #(st/emit! (dw/vertical-order-selected :up)))
do-bring-to-front (mf/use-fn #(st/emit! (dw/vertical-order-selected :top))) do-bring-to-front (mf/use-fn #(st/emit! (dw/vertical-order-selected :top)))
do-send-backward (mf/use-fn #(st/emit! (dw/vertical-order-selected :down))) do-send-backward (mf/use-fn #(st/emit! (dw/vertical-order-selected :down)))
do-send-to-back (mf/use-fn #(st/emit! (dw/vertical-order-selected :bottom))) do-send-to-back (mf/use-fn #(st/emit! (dw/vertical-order-selected :bottom)))
@ -284,16 +303,20 @@
:on-unmount (on-unmount (:id object)) :on-unmount (on-unmount (:id object))
:icon (usi/get-shape-icon object)}])]) :icon (usi/get-shape-icon object)}])])
[:> menu-entry* {:title (tr "workspace.shape.menu.forward") [:> menu-entry* {:title (tr "workspace.shape.menu.forward")
:shortcut (sc/get-tooltip :bring-forward) :shortcut (get-tt :bring-forward)
:shortcut-key :bring-forward
:on-click do-bring-forward}] :on-click do-bring-forward}]
[:> menu-entry* {:title (tr "workspace.shape.menu.front") [:> menu-entry* {:title (tr "workspace.shape.menu.front")
:shortcut (sc/get-tooltip :bring-front) :shortcut (get-tt :bring-front)
:shortcut-key :bring-front
:on-click do-bring-to-front}] :on-click do-bring-to-front}]
[:> menu-entry* {:title (tr "workspace.shape.menu.backward") [:> menu-entry* {:title (tr "workspace.shape.menu.backward")
:shortcut (sc/get-tooltip :bring-backward) :shortcut (get-tt :bring-backward)
:shortcut-key :bring-backward
:on-click do-send-backward}] :on-click do-send-backward}]
[:> menu-entry* {:title (tr "workspace.shape.menu.back") [:> menu-entry* {:title (tr "workspace.shape.menu.back")
:shortcut (sc/get-tooltip :bring-back) :shortcut (get-tt :bring-back)
:shortcut-key :bring-back
:on-click do-send-to-back}] :on-click do-send-to-back}]
[:> menu-separator* {}]])) [:> menu-separator* {}]]))
@ -301,22 +324,30 @@
(mf/defc context-menu-flip* (mf/defc context-menu-flip*
{::mf/private true} {::mf/private true}
[] []
(let [do-flip-vertical #(st/emit! (dw/flip-vertical-selected)) (let [custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
do-flip-vertical #(st/emit! (dw/flip-vertical-selected))
do-flip-horizontal #(st/emit! (dw/flip-horizontal-selected))] do-flip-horizontal #(st/emit! (dw/flip-horizontal-selected))]
[:* [:*
[:> menu-entry* {:title (tr "workspace.shape.menu.flip-vertical") [:> menu-entry* {:title (tr "workspace.shape.menu.flip-vertical")
:shortcut (sc/get-tooltip :flip-vertical) :shortcut (get-tt :flip-vertical)
:shortcut-key :flip-vertical
:on-click do-flip-vertical}] :on-click do-flip-vertical}]
[:> menu-entry* {:title (tr "workspace.shape.menu.flip-horizontal") [:> menu-entry* {:title (tr "workspace.shape.menu.flip-horizontal")
:shortcut (sc/get-tooltip :flip-horizontal) :shortcut (get-tt :flip-horizontal)
:shortcut-key :flip-horizontal
:on-click do-flip-horizontal}] :on-click do-flip-horizontal}]
[:> menu-separator* {}]])) [:> menu-separator* {}]]))
(mf/defc context-menu-thumbnail* (mf/defc context-menu-thumbnail*
{::mf/private true} {::mf/private true}
[{:keys [shapes]}] [{:keys [shapes]}]
(let [single? (= (count shapes) 1) (let [custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
single? (= (count shapes) 1)
has-frame? (some cfh/frame-shape? shapes) has-frame? (some cfh/frame-shape? shapes)
do-toggle-thumbnail #(st/emit! (dw/toggle-file-thumbnail-selected))] do-toggle-thumbnail #(st/emit! (dw/toggle-file-thumbnail-selected))]
(when (and single? has-frame?) (when (and single? has-frame?)
@ -325,25 +356,33 @@
[:> menu-entry* {:title (tr "workspace.shape.menu.thumbnail-remove") [:> menu-entry* {:title (tr "workspace.shape.menu.thumbnail-remove")
:on-click do-toggle-thumbnail}] :on-click do-toggle-thumbnail}]
[:> menu-entry* {:title (tr "workspace.shape.menu.thumbnail-set") [:> menu-entry* {:title (tr "workspace.shape.menu.thumbnail-set")
:shortcut (sc/get-tooltip :thumbnail-set) :shortcut (get-tt :thumbnail-set)
:shortcut-key :thumbnail-set
:on-click do-toggle-thumbnail}]) :on-click do-toggle-thumbnail}])
[:> menu-separator* {}]]))) [:> menu-separator* {}]])))
(mf/defc context-menu-rename* (mf/defc context-menu-rename*
{::mf/private true} {::mf/private true}
[{:keys [shapes]}] [{:keys [shapes]}]
(let [do-rename #(st/emit! (dw/start-rename-selected))] (let [custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
do-rename #(st/emit! (dw/start-rename-selected))]
(when (= (count shapes) 1) (when (= (count shapes) 1)
[:* [:*
[:> menu-separator* {}] [:> menu-separator* {}]
[:> menu-entry* {:title (tr "workspace.shape.menu.rename") [:> menu-entry* {:title (tr "workspace.shape.menu.rename")
:shortcut (sc/get-tooltip :rename) :shortcut (get-tt :rename)
:shortcut-key :rename
:on-click do-rename}]]))) :on-click do-rename}]])))
(mf/defc context-menu-group* (mf/defc context-menu-group*
{::mf/private true} {::mf/private true}
[{:keys [shapes]}] [{:keys [shapes]}]
(let [multiple? (> (count shapes) 1) (let [custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
multiple? (> (count shapes) 1)
single? (= (count shapes) 1) single? (= (count shapes) 1)
objects (deref refs/workspace-page-objects) objects (deref refs/workspace-page-objects)
@ -371,44 +410,56 @@
[:* [:*
(when (or has-bool? has-group? has-mask? has-frame?) (when (or has-bool? has-group? has-mask? has-frame?)
[:> menu-entry* {:title (tr "workspace.shape.menu.ungroup") [:> menu-entry* {:title (tr "workspace.shape.menu.ungroup")
:shortcut (sc/get-tooltip :ungroup) :shortcut (get-tt :ungroup)
:shortcut-key :ungroup
:on-click do-remove-group}]) :on-click do-remove-group}])
[:> menu-entry* {:title (tr "workspace.shape.menu.group") [:> menu-entry* {:title (tr "workspace.shape.menu.group")
:shortcut (sc/get-tooltip :group) :shortcut (get-tt :group)
:shortcut-key :group
:on-click do-create-group}] :on-click do-create-group}]
(when (or multiple? (and is-group? (not has-mask?)) is-bool?) (when (or multiple? (and is-group? (not has-mask?)) is-bool?)
[:> menu-entry* {:title (tr "workspace.shape.menu.mask") [:> menu-entry* {:title (tr "workspace.shape.menu.mask")
:shortcut (sc/get-tooltip :mask) :shortcut (get-tt :mask)
:shortcut-key :mask
:on-click do-mask-group}]) :on-click do-mask-group}])
(when has-mask? (when has-mask?
[:> menu-entry* {:title (tr "workspace.shape.menu.unmask") [:> menu-entry* {:title (tr "workspace.shape.menu.unmask")
:shortcut (sc/get-tooltip :unmask) :shortcut (get-tt :unmask)
:shortcut-key :unmask
:on-click do-unmask-group}]) :on-click do-unmask-group}])
[:> menu-entry* {:title (tr "workspace.shape.menu.create-artboard-from-selection") [:> menu-entry* {:title (tr "workspace.shape.menu.create-artboard-from-selection")
:shortcut (sc/get-tooltip :artboard-selection) :shortcut (get-tt :artboard-selection)
:shortcut-key :artboard-selection
:on-click do-create-artboard-from-selection}] :on-click do-create-artboard-from-selection}]
[:> menu-separator* {}]])])) [:> menu-separator* {}]])]))
(mf/defc context-focus-mode-menu* (mf/defc context-focus-mode-menu*
{::mf/private true} {::mf/private true}
[] []
(let [focus (mf/deref refs/workspace-focus-selected) (let [custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
focus (mf/deref refs/workspace-focus-selected)
do-toggle-focus-mode #(st/emit! (dw/toggle-focus-mode))] do-toggle-focus-mode #(st/emit! (dw/toggle-focus-mode))]
[:> menu-entry* {:title (if (empty? focus) [:> menu-entry* {:title (if (empty? focus)
(tr "workspace.focus.focus-on") (tr "workspace.focus.focus-on")
(tr "workspace.focus.focus-off")) (tr "workspace.focus.focus-off"))
:shortcut (sc/get-tooltip :toggle-focus-mode) :shortcut (get-tt :toggle-focus-mode)
:shortcut-key :toggle-focus-mode
:on-click do-toggle-focus-mode}])) :on-click do-toggle-focus-mode}]))
(mf/defc context-menu-path* (mf/defc context-menu-path*
{::mf/private true} {::mf/private true}
[{:keys [shapes objects disable-flatten disable-booleans]}] [{:keys [shapes objects disable-flatten disable-booleans]}]
(let [multiple? (> (count shapes) 1) (let [custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
multiple? (> (count shapes) 1)
single? (= (count shapes) 1) single? (= (count shapes) 1)
has-group? (->> shapes (d/seek cfh/group-shape?)) has-group? (->> shapes (d/seek cfh/group-shape?))
@ -446,7 +497,8 @@
[:* [:*
(when (and single? (not is-frame?)) (when (and single? (not is-frame?))
[:> menu-entry* {:title (tr "workspace.shape.menu.edit") [:> menu-entry* {:title (tr "workspace.shape.menu.edit")
:shortcut (sc/get-tooltip :start-editing) :shortcut (get-tt :start-editing)
:shortcut-key :start-editing
:on-click do-start-editing}]) :on-click do-start-editing}])
(when-not (or disable-flatten has-frame? has-path?) (when-not (or disable-flatten has-frame? has-path?)
@ -463,16 +515,20 @@
(or multiple? (and single? (or is-group? is-bool?)))) (or multiple? (and single? (or is-group? is-bool?))))
[:> menu-entry* {:title (tr "workspace.shape.menu.path")} [:> menu-entry* {:title (tr "workspace.shape.menu.path")}
[:> menu-entry* {:title (tr "workspace.shape.menu.union") [:> menu-entry* {:title (tr "workspace.shape.menu.union")
:shortcut (sc/get-tooltip :bool-union) :shortcut (get-tt :bool-union)
:shortcut-key :bool-union
:on-click (make-do-bool :union)}] :on-click (make-do-bool :union)}]
[:> menu-entry* {:title (tr "workspace.shape.menu.difference") [:> menu-entry* {:title (tr "workspace.shape.menu.difference")
:shortcut (sc/get-tooltip :bool-difference) :shortcut (get-tt :bool-difference)
:shortcut-key :bool-difference
:on-click (make-do-bool :difference)}] :on-click (make-do-bool :difference)}]
[:> menu-entry* {:title (tr "workspace.shape.menu.intersection") [:> menu-entry* {:title (tr "workspace.shape.menu.intersection")
:shortcut (sc/get-tooltip :bool-intersection) :shortcut (get-tt :bool-intersection)
:shortcut-key :bool-intersection
:on-click (make-do-bool :intersection)}] :on-click (make-do-bool :intersection)}]
[:> menu-entry* {:title (tr "workspace.shape.menu.exclude") [:> menu-entry* {:title (tr "workspace.shape.menu.exclude")
:shortcut (sc/get-tooltip :bool-exclude) :shortcut (get-tt :bool-exclude)
:shortcut-key :bool-exclude
:on-click (make-do-bool :exclude)}] :on-click (make-do-bool :exclude)}]
(when (and single? is-bool? (not disable-flatten)) (when (and single? is-bool? (not disable-flatten))
@ -484,7 +540,10 @@
(mf/defc context-menu-layer-options* (mf/defc context-menu-layer-options*
{::mf/private true} {::mf/private true}
[{:keys [shapes]}] [{:keys [shapes]}]
(let [ids (mapv :id shapes) (let [custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
ids (mapv :id shapes)
do-show-shape #(st/emit! (dw/update-shape-flags ids {:hidden false})) do-show-shape #(st/emit! (dw/update-shape-flags ids {:hidden false}))
do-hide-shape #(st/emit! (dw/update-shape-flags ids {:hidden true})) do-hide-shape #(st/emit! (dw/update-shape-flags ids {:hidden true}))
do-lock-shape #(st/emit! (dw/update-shape-flags ids {:blocked true})) do-lock-shape #(st/emit! (dw/update-shape-flags ids {:blocked true}))
@ -492,18 +551,22 @@
[:* [:*
(if (every? :hidden shapes) (if (every? :hidden shapes)
[:> menu-entry* {:title (tr "workspace.shape.menu.show") [:> menu-entry* {:title (tr "workspace.shape.menu.show")
:shortcut (sc/get-tooltip :toggle-visibility) :shortcut (get-tt :toggle-visibility)
:shortcut-key :toggle-visibility
:on-click do-show-shape}] :on-click do-show-shape}]
[:> menu-entry* {:title (tr "workspace.shape.menu.hide") [:> menu-entry* {:title (tr "workspace.shape.menu.hide")
:shortcut (sc/get-tooltip :toggle-visibility) :shortcut (get-tt :toggle-visibility)
:shortcut-key :toggle-visibility
:on-click do-hide-shape}]) :on-click do-hide-shape}])
(if (every? :blocked shapes) (if (every? :blocked shapes)
[:> menu-entry* {:title (tr "workspace.shape.menu.unlock") [:> menu-entry* {:title (tr "workspace.shape.menu.unlock")
:shortcut (sc/get-tooltip :toggle-lock) :shortcut (get-tt :toggle-lock)
:shortcut-key :toggle-lock
:on-click do-unlock-shape}] :on-click do-unlock-shape}]
[:> menu-entry* {:title (tr "workspace.shape.menu.lock") [:> menu-entry* {:title (tr "workspace.shape.menu.lock")
:shortcut (sc/get-tooltip :toggle-lock) :shortcut (get-tt :toggle-lock)
:shortcut-key :toggle-lock
:on-click do-lock-shape}])])) :on-click do-lock-shape}])]))
(mf/defc context-menu-prototype* (mf/defc context-menu-prototype*
@ -530,7 +593,10 @@
(mf/defc context-menu-layout* (mf/defc context-menu-layout*
{::mf/private true} {::mf/private true}
[{:keys [shapes]}] [{:keys [shapes]}]
(let [single? (= (count shapes) 1) (let [custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
single? (= (count shapes) 1)
objects (deref refs/workspace-page-objects) objects (deref refs/workspace-page-objects)
any-in-copy? (some true? (map #(ctn/has-any-copy-parent? objects %) shapes)) any-in-copy? (some true? (map #(ctn/has-any-copy-parent? objects %) shapes))
@ -565,28 +631,35 @@
[:> menu-separator* {}] [:> menu-separator* {}]
(if has-flex? (if has-flex?
[:> menu-entry* {:title (tr "workspace.shape.menu.remove-flex") [:> menu-entry* {:title (tr "workspace.shape.menu.remove-flex")
:shortcut (sc/get-tooltip :toggle-layout-flex) :shortcut (get-tt :toggle-layout-flex)
:shortcut-key :toggle-layout-flex
:on-click on-remove-layout}] :on-click on-remove-layout}]
[:> menu-entry* {:title (tr "workspace.shape.menu.remove-grid") [:> menu-entry* {:title (tr "workspace.shape.menu.remove-grid")
:shortcut (sc/get-tooltip :toggle-layout-grid) :shortcut (get-tt :toggle-layout-grid)
:shortcut-key :toggle-layout-grid
:on-click on-remove-layout}])] :on-click on-remove-layout}])]
(when (or single? (not any-is-variant?)) (when (or single? (not any-is-variant?))
[:div [:div
[:> menu-separator* {}] [:> menu-separator* {}]
[:> menu-entry* {:title (tr "workspace.shape.menu.add-flex") [:> menu-entry* {:title (tr "workspace.shape.menu.add-flex")
:shortcut (sc/get-tooltip :toggle-layout-flex) :shortcut (get-tt :toggle-layout-flex)
:shortcut-key :toggle-layout-flex
:value "flex" :value "flex"
:on-click on-add-layout}] :on-click on-add-layout}]
[:> menu-entry* {:title (tr "workspace.shape.menu.add-grid") [:> menu-entry* {:title (tr "workspace.shape.menu.add-grid")
:shortcut (sc/get-tooltip :toggle-layout-grid) :shortcut (get-tt :toggle-layout-grid)
:shortcut-key :toggle-layout-grid
:value "grid" :value "grid"
:on-click on-add-layout}]])))])) :on-click on-add-layout}]])))]))
(mf/defc context-menu-component* (mf/defc context-menu-component*
{:mf/private true} {:mf/private true}
[{:keys [shapes]}] [{:keys [shapes]}]
(let [single? (= (count shapes) 1) (let [custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
single? (= (count shapes) 1)
objects (deref refs/workspace-page-objects) objects (deref refs/workspace-page-objects)
can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) shapes)) can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) shapes))
components-menu-entries (cmm/generate-components-menu-entries shapes) components-menu-entries (cmm/generate-components-menu-entries shapes)
@ -608,7 +681,8 @@
[:> menu-separator* {}] [:> menu-separator* {}]
[:> menu-entry* {:title (tr "workspace.shape.menu.create-component") [:> menu-entry* {:title (tr "workspace.shape.menu.create-component")
:shortcut (sc/get-tooltip :create-component-variant) :shortcut (get-tt :create-component-variant)
:shortcut-key :create-component-variant
:on-click do-add-component}] :on-click do-add-component}]
(when (not single?) (when (not single?)
[:> menu-entry* {:title (tr "workspace.shape.menu.create-multiple-components") [:> menu-entry* {:title (tr "workspace.shape.menu.create-multiple-components")
@ -618,17 +692,19 @@
[:* [:*
[:> menu-separator*] [:> menu-separator*]
(for [entry (filter some? components-menu-entries)] (for [entry (filter some? components-menu-entries)]
[:> menu-entry* {:key (:title entry) (let [sc-key (:shortcut entry)]
:title (:title entry) [:> menu-entry* {:key (:title entry)
:shortcut (when (contains? entry :shortcut) :title (:title entry)
(sc/get-tooltip (:shortcut entry))) :shortcut (when sc-key (get-tt sc-key))
:on-click (:action entry)}])]) :shortcut-key sc-key
:on-click (:action entry)}]))])
(when variant-container? (when variant-container?
[:* [:*
[:> menu-separator*] [:> menu-separator*]
[:> menu-entry* {:title (tr "workspace.shape.menu.add-variant") [:> menu-entry* {:title (tr "workspace.shape.menu.add-variant")
:shortcut (sc/get-tooltip :create-component-variant) :shortcut (get-tt :create-component-variant)
:shortcut-key :create-component-variant
:on-click do-add-variant}]]) :on-click do-add-variant}]])
(when (and (not single?) all-main? (not any-variant?)) (when (and (not single?) all-main? (not any-variant?))
@ -659,11 +735,15 @@
(mf/defc context-menu-delete* (mf/defc context-menu-delete*
{::mf/private true} {::mf/private true}
[] []
(let [do-delete #(st/emit! (dw/delete-selected))] (let [custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
do-delete #(st/emit! (dw/delete-selected))]
[:* [:*
[:> menu-separator* {}] [:> menu-separator* {}]
[:> menu-entry* {:title (tr "workspace.shape.menu.delete") [:> menu-entry* {:title (tr "workspace.shape.menu.delete")
:shortcut (sc/get-tooltip :delete) :shortcut (get-tt :delete)
:shortcut-key :delete
:on-click do-delete}]])) :on-click do-delete}]]))
(mf/defc shape-context-menu* (mf/defc shape-context-menu*
@ -741,7 +821,10 @@
(mf/defc viewport-context-menu* (mf/defc viewport-context-menu*
[] []
(let [focus (mf/deref refs/workspace-focus-selected) (let [custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
focus (mf/deref refs/workspace-focus-selected)
read-only? (mf/use-ctx ctx/workspace-read-only?) read-only? (mf/use-ctx ctx/workspace-read-only?)
do-paste #(st/emit! (dw/paste-from-clipboard)) do-paste #(st/emit! (dw/paste-from-clipboard))
do-hide-ui #(st/emit! (-> (dw/toggle-layout-flag :hide-ui) do-hide-ui #(st/emit! (-> (dw/toggle-layout-flag :hide-ui)
@ -750,15 +833,18 @@
[:* [:*
(when-not ^boolean read-only? (when-not ^boolean read-only?
[:> menu-entry* {:title (tr "workspace.shape.menu.paste") [:> menu-entry* {:title (tr "workspace.shape.menu.paste")
:shortcut (sc/get-tooltip :paste) :shortcut (get-tt :paste)
:shortcut-key :paste
:on-click do-paste}]) :on-click do-paste}])
[:> menu-entry* {:title (tr "workspace.shape.menu.hide-ui") [:> menu-entry* {:title (tr "workspace.shape.menu.hide-ui")
:shortcut (sc/get-tooltip :hide-ui) :shortcut (get-tt :hide-ui)
:shortcut-key :hide-ui
:on-click do-hide-ui}] :on-click do-hide-ui}]
(when (d/not-empty? focus) (when (d/not-empty? focus)
[:> menu-entry* {:title (tr "workspace.focus.focus-off") [:> menu-entry* {:title (tr "workspace.focus.focus-off")
:shortcut (sc/get-tooltip :toggle-focus-mode) :shortcut (get-tt :toggle-focus-mode)
:shortcut-key :toggle-focus-mode
:on-click do-toggle-focus-mode}])])) :on-click do-toggle-focus-mode}])]))
(mf/defc grid-track-context-menu* (mf/defc grid-track-context-menu*

View File

@ -21,8 +21,8 @@
width: deprecated.$s-240; width: deprecated.$s-240;
padding: deprecated.$s-4; padding: deprecated.$s-4;
border-radius: deprecated.$br-8; border-radius: deprecated.$br-8;
border: deprecated.$s-2 solid var(--panel-border-color); border: deprecated.$s-2 solid var(--color-background-quaternary);
background-color: var(--menu-background-color); background-color: var(--color-background-tertiary);
max-height: 100vh; max-height: 100vh;
overflow-y: auto; overflow-y: auto;
} }
@ -36,6 +36,12 @@
} }
.context-menu-item { .context-menu-item {
--context-menu-item-bg-color: var(--color-background-tertiary);
--context-menu-item-title-color: var(--color-foreground-primary);
--context-menu-item-shortcut-color: var(--color-foreground-secondary);
--context-menu-item-shortcut-bg-color: var(--color-background-primary);
--context-menu-item-shortcut-border-color: transparent;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@ -44,54 +50,50 @@
padding: deprecated.$s-6; padding: deprecated.$s-6;
border-radius: deprecated.$br-8; border-radius: deprecated.$br-8;
cursor: pointer; cursor: pointer;
background-color: var(--context-menu-item-bg-color);
.title { border: 1px solid var(--context-menu-item-shortcut-border-color);
@include deprecated.body-small-typography;
color: var(--menu-foreground-color);
}
.shortcut {
@include deprecated.flex-center;
gap: deprecated.$s-2;
color: var(--menu-shortcut-foreground-color);
.shortcut-key {
@include deprecated.body-small-typography;
@include deprecated.flex-center;
height: deprecated.$s-20;
padding: deprecated.$s-2 deprecated.$s-6;
border-radius: deprecated.$br-6;
background-color: var(--menu-shortcut-background-color);
}
}
.submenu-icon svg {
@extend %button-icon-small;
stroke: var(--menu-foreground-color);
}
&:hover { &:hover {
background-color: var(--menu-background-color-hover); --context-menu-item-title-color: var(--menu-foreground-color-hover);
--context-menu-item-bg-color: var(--menu-background-color-hover);
.title { --context-menu-item-shortcut-color: var(--menu-shortcut-foreground-color-hover);
color: var(--menu-foreground-color-hover);
}
.shortcut {
color: var(--menu-shortcut-foreground-color-hover);
}
} }
&:focus { &:focus {
border: 1px solid var(--menu-border-color-focus); --context-menu-item-bg-color: var(--menu-background-color-focus);
background-color: var(--menu-background-color-focus); --context-menu-item-shortcut-border-color: var(--menu-border-color-focus);
} }
} }
.title {
@include deprecated.body-small-typography;
color: var(--context-menu-item-title-color);
}
.shortcut {
@include deprecated.flex-center;
gap: deprecated.$s-2;
color: var(--context-menu-item-shortcut-color);
}
.shortcut-key {
@include deprecated.body-small-typography;
@include deprecated.flex-center;
height: deprecated.$s-20;
padding: deprecated.$s-2 deprecated.$s-6;
border-radius: deprecated.$br-6;
background-color: var(--context-menu-item-shortcut-bg-color);
}
.submenu-icon svg {
@extend %button-icon-small;
stroke: var(--menu-foreground-color);
}
.icon-menu-item { .icon-menu-item {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
@ -165,3 +167,8 @@
border: deprecated.$s-2 solid var(--menu-foreground-color); border: deprecated.$s-2 solid var(--menu-foreground-color);
} }
} }
.customized-key {
background-color: var(--color-background-info);
color: var(--color-foreground-primary);
}

View File

@ -51,11 +51,15 @@
(mf/defc shortcuts* (mf/defc shortcuts*
{::mf/private true} {::mf/private true}
[{:keys [id]}] [{:keys [id]}]
[:span {:class (stl/css :shortcut)} (let [custom-shortcuts (mf/deref refs/custom-shortcuts)
(for [sc (scd/split-sc (sc/get-tooltip id))] customized? (let [c (get-in custom-shortcuts [:workspace id])]
[:span {:class (stl/css :shortcut-key) (and c (not= c "")))]
:key sc} [:span {:class (stl/css :shortcut)}
sc])]) (for [sc (scd/split-sc (sc/get-effective-tooltip id custom-shortcuts))]
[:span {:class (stl/css-case :shortcut-key true
:customized-key customized?)
:key sc}
sc])]))
(mf/defc help-info-menu* (mf/defc help-info-menu*
{::mf/private true {::mf/private true
@ -982,6 +986,19 @@
::ev/origin "workspace:menu"}) ::ev/origin "workspace:menu"})
(modal/show :plugin-management {})))) (modal/show :plugin-management {}))))
show-shortcuts
(mf/use-fn
(mf/deps layout)
(fn [event]
(dom/stop-propagation event)
(reset! show-menu* false)
(reset! selected-sub-menu* nil)
(when (contains? layout :collapse-left-sidebar)
(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
(st/emit!
(-> (dw/toggle-layout-flag :shortcuts)
(vary-meta assoc ::ev/origin "workspace-menu")))))
subscription (:subscription (:props profile)) subscription (:subscription (:props profile))
subscription-type (get-subscription-type subscription)] subscription-type (get-subscription-type subscription)]
@ -1148,6 +1165,7 @@
:toggle-flag toggle-flag :toggle-flag toggle-flag
:toggle-theme toggle-theme :toggle-theme toggle-theme
:toggle-render toggle-render :toggle-render toggle-render
:show-shortcuts show-shortcuts
:on-close close-sub-menu}] :on-close close-sub-menu}]
:plugins :plugins

View File

@ -182,3 +182,8 @@
border-radius: $br-6; border-radius: $br-6;
background-color: var(--menu-shortcut-background-color); background-color: var(--menu-shortcut-background-color);
} }
.customized-key {
background-color: var(--color-background-info);
color: var(--color-foreground-primary);
}

View File

@ -51,9 +51,12 @@
(mf/defc palette* (mf/defc palette*
[{:keys [layout on-change-size]}] [{:keys [layout on-change-size]}]
(let [color-palette? (:colorpalette layout) (let [color-palette? (:colorpalette layout)
text-palette? (:textpalette layout) text-palette? (:textpalette layout)
hide-palettes? (:hide-palettes layout) hide-palettes? (:hide-palettes layout)
custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
read-only? (mf/use-ctx ctx/workspace-read-only?) read-only? (mf/use-ctx ctx/workspace-read-only?)
container (mf/use-ref nil) container (mf/use-ref nil)
@ -179,16 +182,16 @@
[:ul {:class (dm/str size-classname " " (stl/css-case :palette-btn-list true [:ul {:class (dm/str size-classname " " (stl/css-case :palette-btn-list true
:hidden-bts hide-palettes?))} :hidden-bts hide-palettes?))}
[:li {:class (stl/css :palette-item)} [:li {:class (stl/css :palette-item)}
[:button {:title (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette)) [:button {:title (tr "workspace.toolbar.color-palette" (get-tt :toggle-colorpalette))
:aria-label (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette)) :aria-label (tr "workspace.toolbar.color-palette" (get-tt :toggle-colorpalette))
:class (stl/css-case :palette-btn true :class (stl/css-case :palette-btn true
:selected color-palette?) :selected color-palette?)
:on-click on-select-color-palette} :on-click on-select-color-palette}
deprecated-icon/drop-icon]] deprecated-icon/drop-icon]]
[:li {:class (stl/css :palette-item)} [:li {:class (stl/css :palette-item)}
[:button {:title (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette)) [:button {:title (tr "workspace.toolbar.text-palette" (get-tt :toggle-textpalette))
:aria-label (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette)) :aria-label (tr "workspace.toolbar.text-palette" (get-tt :toggle-textpalette))
:class (stl/css-case :palette-btn true :class (stl/css-case :palette-btn true
:selected text-palette?) :selected text-palette?)
:on-click on-select-text-palette} :on-click on-select-text-palette}

View File

@ -40,8 +40,10 @@
{::mf/wrap [mf/memo] {::mf/wrap [mf/memo]
::mf/wrap-props false} ::mf/wrap-props false}
[{:keys [zoom on-increase on-decrease on-zoom-reset on-zoom-fit on-zoom-selected]}] [{:keys [zoom on-increase on-decrease on-zoom-reset on-zoom-fit on-zoom-selected]}]
(let [open* (mf/use-state false) (let [open* (mf/use-state false)
open? (deref open*) open? (deref open*)
custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
open-dropdown open-dropdown
(mf/use-fn (mf/use-fn
@ -97,14 +99,14 @@
:on-click on-zoom-fit} :on-click on-zoom-fit}
(tr "workspace.header.zoom-fit-all") (tr "workspace.header.zoom-fit-all")
[:span {:class (stl/css :shortcuts)} [:span {:class (stl/css :shortcuts)}
(for [sc (scd/split-sc (sc/get-tooltip :fit-all))] (for [sc (scd/split-sc (get-tt :fit-all))]
[:span {:class (stl/css :shortcut-key) [:span {:class (stl/css :shortcut-key)
:key (str "zoom-fit-" sc)} sc])]] :key (str "zoom-fit-" sc)} sc])]]
[:li {:class (stl/css :zoom-option) [:li {:class (stl/css :zoom-option)
:on-click on-zoom-selected} :on-click on-zoom-selected}
(tr "workspace.header.zoom-selected") (tr "workspace.header.zoom-selected")
[:span {:class (stl/css :shortcuts)} [:span {:class (stl/css :shortcuts)}
(for [sc (scd/split-sc (sc/get-tooltip :zoom-selected))] (for [sc (scd/split-sc (get-tt :zoom-selected))]
[:span {:class (stl/css :shortcut-key) [:span {:class (stl/css :shortcut-key)
:key (str "zoom-selected-" sc)} sc])]]]]])) :key (str "zoom-selected-" sc)} sc])]]]]]))
@ -118,6 +120,9 @@
read-only? (mf/use-ctx ctx/workspace-read-only?) read-only? (mf/use-ctx ctx/workspace-read-only?)
selected-drawtool (mf/deref refs/selected-drawing-tool) selected-drawtool (mf/deref refs/selected-drawing-tool)
custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
on-increase (mf/use-fn #(st/emit! (dw/increase-zoom nil))) on-increase (mf/use-fn #(st/emit! (dw/increase-zoom nil)))
on-decrease (mf/use-fn #(st/emit! (dw/decrease-zoom nil))) on-decrease (mf/use-fn #(st/emit! (dw/decrease-zoom nil)))
on-zoom-reset (mf/use-fn #(st/emit! dw/reset-zoom)) on-zoom-reset (mf/use-fn #(st/emit! dw/reset-zoom))
@ -211,8 +216,8 @@
:on-zoom-selected on-zoom-selected}]] :on-zoom-selected on-zoom-selected}]]
[:div {:class (stl/css :comments-section)} [:div {:class (stl/css :comments-section)}
[:button {:title (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment)) [:button {:title (tr "workspace.toolbar.comments" (get-tt :add-comment))
:aria-label (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment)) :aria-label (tr "workspace.toolbar.comments" (get-tt :add-comment))
:class (stl/css-case :comments-btn true :class (stl/css-case :comments-btn true
:selected (= selected-drawtool :comments)) :selected (= selected-drawtool :comments))
:on-click toggle-comments :on-click toggle-comments
@ -239,7 +244,7 @@
deprecated-icon/share]) deprecated-icon/share])
[:a {:class (stl/css :viewer-btn) [:a {:class (stl/css :viewer-btn)
:title (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer)) :title (tr "workspace.header.viewer" (get-tt :open-viewer))
:on-click nav-to-viewer} :on-click nav-to-viewer}
deprecated-icon/play]])) deprecated-icon/play]]))

View File

@ -253,7 +253,7 @@
(on-select @selected)) (on-select @selected))
(mf/with-effect [] (mf/with-effect []
(st/emit! (dsc/push-shortcuts :typography {})) (st/emit! (dsc/push-shortcuts :typography {} :workspace))
(fn [] (fn []
(st/emit! (dsc/pop-shortcuts :typography)))) (st/emit! (dsc/pop-shortcuts :typography))))

View File

@ -10,450 +10,80 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.config :as cf] [app.config :as cf]
[app.main.data.dashboard.shortcuts] [app.main.data.dashboard.shortcuts :as dsc]
[app.main.data.shortcuts :as ds] [app.main.data.shortcuts :as ds]
[app.main.data.viewer.shortcuts] [app.main.data.viewer.shortcuts :as vsc]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.data.workspace.path.shortcuts] [app.main.data.workspace.path.shortcuts :as psc]
[app.main.data.workspace.shortcuts] [app.main.data.workspace.shortcuts :as wsc]
[app.main.refs :as refs]
[app.main.router :as rt]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.button-link :as bl]
[app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.components.search-bar :refer [search-bar*]]
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[app.main.ui.ds.product.panel-title :refer [panel-title*]] [app.main.ui.ds.product.panel-title :refer [panel-title*]]
[app.main.ui.shortcuts :as ss]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.strings :refer [matches-search]] [app.util.strings :refer [matches-search]]
[clojure.set :as set] [clojure.set :as set]
[clojure.string]
[cuerdas.core :as str]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc converted-chars*
[{:keys [char command]}]
(let [modified-keys {:up ds/up-arrow
:down ds/down-arrow
:left ds/left-arrow
:right ds/right-arrow
:plus "+"}
macos-keys {:command "\u2318"
:option "\u2325"
:alt "\u2325"
:delete "\u232B"
:del "\u232B"
:shift "\u21E7"
:control "\u2303"
:esc "\u238B"
:enter "\u23CE"}
is-macos? (cf/check-platform? :macos)
char (if (contains? modified-keys (keyword char)) ((keyword char) modified-keys) char)
char (if (and is-macos? (contains? macos-keys (keyword char))) ((keyword char) macos-keys) char)
unique-key (str (d/name command) "-" char)]
[:span {:class (stl/css :key)
:key unique-key} char]))
(defn translation-keyname
[type keyname]
;; Execution time translation strings:
(comment
(tr "shortcut-subsection.alignment")
(tr "shortcut-subsection.edit")
(tr "shortcut-subsection.general-dashboard")
(tr "shortcut-subsection.general-viewer")
(tr "shortcut-subsection.main-menu")
(tr "shortcut-subsection.modify-layers")
(tr "shortcut-subsection.navigation-dashboard")
(tr "shortcut-subsection.navigation-viewer")
(tr "shortcut-subsection.navigation-workspace")
(tr "shortcut-subsection.panels")
(tr "shortcut-subsection.path-editor")
(tr "shortcut-subsection.shape")
(tr "shortcut-subsection.text-editor")
(tr "shortcut-subsection.tools")
(tr "shortcut-subsection.zoom-viewer")
(tr "shortcut-subsection.zoom-workspace")
(tr "shortcuts.add-comment")
(tr "shortcuts.add-node")
(tr "shortcuts.align-bottom")
(tr "shortcuts.align-center")
(tr "shortcuts.align-hcenter")
(tr "shortcuts.align-justify")
(tr "shortcuts.align-left")
(tr "shortcuts.align-right")
(tr "shortcuts.align-top")
(tr "shortcuts.align-vcenter")
(tr "shortcuts.artboard-selection")
(tr "shortcuts.bold")
(tr "shortcuts.bool-difference")
(tr "shortcuts.bool-exclude")
(tr "shortcuts.bool-intersection")
(tr "shortcuts.bool-union")
(tr "shortcuts.bring-back")
(tr "shortcuts.bring-backward")
(tr "shortcuts.bring-forward")
(tr "shortcuts.bring-front")
(tr "shortcuts.clear-undo")
(tr "shortcuts.copy")
(tr "shortcuts.copy-link")
(tr "shortcuts.create-component-variant")
(tr "shortcuts.create-new-project")
(tr "shortcuts.cut")
(tr "shortcuts.decrease-zoom")
(tr "shortcuts.delete")
(tr "shortcuts.delete-node")
(tr "shortcuts.detach-component")
(tr "shortcuts.draw-curve")
(tr "shortcuts.draw-ellipse")
(tr "shortcuts.draw-frame")
(tr "shortcuts.draw-nodes")
(tr "shortcuts.draw-path")
(tr "shortcuts.draw-rect")
(tr "shortcuts.draw-text")
(tr "shortcuts.duplicate")
(tr "shortcuts.escape")
(tr "shortcuts.export-shapes")
(tr "shortcuts.find")
(tr "shortcuts.find-and-replace")
(tr "shortcuts.fit-all")
(tr "shortcuts.flip-horizontal")
(tr "shortcuts.flip-vertical")
(tr "shortcuts.font-size-dec")
(tr "shortcuts.font-size-inc")
(tr "shortcuts.go-to-drafts")
(tr "shortcuts.go-to-libs")
(tr "shortcuts.go-to-search")
(tr "shortcuts.group")
(tr "shortcuts.h-distribute")
(tr "shortcuts.hide-ui")
(tr "shortcuts.increase-zoom")
(tr "shortcuts.insert-image")
(tr "shortcuts.italic")
(tr "shortcuts.join-nodes")
(tr "shortcuts.line-through")
(tr "shortcuts.make-corner")
(tr "shortcuts.make-curve")
(tr "shortcuts.mask")
(tr "shortcuts.merge-nodes")
(tr "shortcuts.move")
(tr "shortcuts.move-fast-down")
(tr "shortcuts.move-fast-left")
(tr "shortcuts.move-fast-right")
(tr "shortcuts.move-fast-up")
(tr "shortcuts.move-nodes")
(tr "shortcuts.move-unit-down")
(tr "shortcuts.move-unit-left")
(tr "shortcuts.move-unit-right")
(tr "shortcuts.move-unit-up")
(tr "shortcuts.next-frame")
(tr "shortcuts.opacity-0")
(tr "shortcuts.opacity-1")
(tr "shortcuts.opacity-2")
(tr "shortcuts.opacity-3")
(tr "shortcuts.opacity-4")
(tr "shortcuts.opacity-5")
(tr "shortcuts.opacity-6")
(tr "shortcuts.opacity-7")
(tr "shortcuts.opacity-8")
(tr "shortcuts.opacity-9")
(tr "shortcuts.open-color-picker")
(tr "shortcuts.open-comments")
(tr "shortcuts.open-dashboard")
(tr "shortcuts.open-inspect")
(tr "shortcuts.open-interactions")
(tr "shortcuts.open-viewer")
(tr "shortcuts.open-workspace")
(tr "shortcuts.paste")
(tr "shortcuts.paste-replace")
(tr "shortcuts.prev-frame")
(tr "shortcuts.redo")
(tr "shortcuts.rename")
(tr "shortcuts.reset-zoom")
(tr "shortcuts.scale")
(tr "shortcuts.search-placeholder")
(tr "shortcuts.select-all")
(tr "shortcuts.select-next")
(tr "shortcuts.select-parent-layer")
(tr "shortcuts.select-prev")
(tr "shortcuts.separate-nodes")
(tr "shortcuts.show-pixel-grid")
(tr "shortcuts.show-shortcuts")
(tr "shortcuts.snap-nodes")
(tr "shortcuts.snap-pixel-grid")
(tr "shortcuts.start-editing")
(tr "shortcuts.start-measure")
(tr "shortcuts.stop-measure")
(tr "shortcuts.thumbnail-set")
(tr "shortcuts.toggle-alignment")
(tr "shortcuts.toggle-assets")
(tr "shortcuts.toggle-colorpalette")
(tr "shortcuts.toggle-focus-mode")
(tr "shortcuts.toggle-fullscreen")
(tr "shortcuts.toggle-guides")
(tr "shortcuts.toggle-history")
(tr "shortcuts.toggle-layers")
(tr "shortcuts.toggle-layout-flex")
(tr "shortcuts.toggle-layout-grid")
(tr "shortcuts.toggle-lock")
(tr "shortcuts.toggle-lock-size")
(tr "shortcuts.toggle-rulers")
(tr "shortcuts.toggle-snap-guides")
(tr "shortcuts.toggle-snap-ruler-guide")
(tr "shortcuts.toggle-textpalette")
(tr "shortcuts.toggle-theme")
(tr "shortcuts.toggle-visibility")
(tr "shortcuts.toggle-zoom-style")
(tr "shortcuts.underline")
(tr "shortcuts.undo")
(tr "shortcuts.ungroup")
(tr "shortcuts.unmask")
(tr "shortcuts.v-distribute")
(tr "shortcuts.zoom-lense-decrease")
(tr "shortcuts.zoom-lense-increase")
(tr "shortcuts.zoom-selected"))
(let [translat-pre (case type
:sc "shortcuts."
:sec "shortcut-section."
:sub-sec "shortcut-subsection.")]
(tr (str translat-pre (d/name keyname)))))
(defn add-translation
[type item]
(map (fn [[k v]] [k (assoc v :translation (translation-keyname type k))]) item))
(defn shortcuts->subsections
"A function to obtain the list of subsections and their
associated shortcus from the general map of shortcuts"
[shortcuts]
(let [subsections (into #{} (mapcat :subsections) (vals shortcuts))
get-sc-by-subsection
(fn [subsection [k v]]
(when (some #(= subsection %) (:subsections v)) {k v}))
reduce-sc
(fn [acc subsection]
(let [shortcuts-by-subsection (into {} (keep (partial get-sc-by-subsection subsection) shortcuts))]
(assoc acc subsection {:children shortcuts-by-subsection})))]
(reduce reduce-sc {} subsections)))
(mf/defc shortcuts-keys*
[{:keys [content command]}]
(let [managed-list (if (coll? content)
content
(conj () content))
chars-list (map ds/split-sc managed-list)
last-element (last chars-list)
short-char-list (if (= 1 (count chars-list))
chars-list
(drop-last chars-list))
penultimate (last short-char-list)]
[:span {:class (stl/css :keys)}
(for [chars short-char-list]
[:* {:key (str/join chars)}
(for [char chars]
[:> converted-chars* {:key (dm/str char "-" (name command))
:char char
:command command}])
(when (not= chars penultimate) [:span {:class (stl/css :space)} ","])])
(when (not= last-element penultimate)
[:*
[:span {:class (stl/css :space)} (tr "shortcuts.or")]
(for [char last-element]
[:> converted-chars* {:key (dm/str char "-" (name command))
:char char
:command command}])])]))
(mf/defc shortcut-row*
[{:keys [elements filter-term is-match-section is-match-subsection]}]
(let [shortcut-name (keys elements)
shortcut-translations (map #(translation-keyname :sc %) shortcut-name)
match-shortcut? (some #(matches-search % @filter-term) shortcut-translations)
filtered (if (and (or is-match-section is-match-subsection) (not match-shortcut?))
shortcut-translations
(filter #(matches-search % @filter-term) shortcut-translations))
sorted-filtered (sort filtered)]
[:ul {:class (stl/css :sub-menu)}
(for [command-translate sorted-filtered]
(let [sc-by-translate (first (filter #(= (:translation (second %)) command-translate) elements))
[command comand-info] sc-by-translate
content (or (:show-command comand-info) (:command comand-info))]
[:li {:class (stl/css :shortcuts-name)
:key command-translate}
[:span {:class (stl/css :command-name)}
command-translate]
[:> shortcuts-keys* {:content content
:command command}]]))]))
(mf/defc section-title*
[{:keys [name is-visible is-sub]}]
[:div {:class (if is-sub
(stl/css :subsection-title)
(stl/css :section-title))}
[:> icon* {:icon-id (if is-visible i/arrow-down i/arrow-right)
:size "s"}]
[:span {:class (if is-sub
(stl/css :subsection-name)
(stl/css :section-name))} name]])
(mf/defc shortcut-subsection*
[{:keys [subsections manage-sections filter-term is-match-section open-sections]}]
(let [subsections-names (keys subsections)
subsection-translations (if (= :none (first subsections-names))
(map #(translation-keyname :sc %) subsections-names)
(map #(translation-keyname :sub-sec %) subsections-names))
sorted-translations (sort subsection-translations)]
;; Basics section is treated different because it has no sub sections
(if (= :none (first subsections-names))
(let [basic-shortcuts (:none subsections)]
[:> shortcut-row* {:elements (:children basic-shortcuts)
:filter-term filter-term
:is-match-section is-match-section
:is-match-subsection true}])
[:ul {:class (stl/css :subsection-menu)}
(for [sub-translated sorted-translations]
(let [sub-by-translate (first (filter #(= (:translation (second %)) sub-translated) subsections))
[sub-name sub-info] sub-by-translate
visible? (some #(= % (:id sub-info)) @open-sections)
match-subsection? (matches-search (translation-keyname :sub-sec sub-name) @filter-term)
shortcut-names (map #(translation-keyname :sc %) (keys (:children sub-info)))
match-shortcuts? (some #(matches-search % @filter-term) shortcut-names)]
(when (or match-subsection? match-shortcuts? is-match-section)
[:li {:key sub-translated
:on-click (manage-sections (:id sub-info))}
[:> section-title* {:name sub-translated
:is-visible visible?
:is-sub true}]
[:div {:style {:display (if visible? "initial" "none")}}
[:> shortcut-row* {:elements (:children sub-info)
:filter-term filter-term
:is-match-section is-match-section
:is-match-subsection match-subsection?}]]])))])))
(mf/defc shortcut-section*
[{:keys [section manage-sections open-sections filter-term]}]
(let [[section-key section-info] section
section-id (:id section-info)
section-translation (translation-keyname :sec section-key)
match-section? (matches-search section-translation @filter-term)
subsections (:children section-info)
subs-names (keys subsections)
subs-bodys (reduce #(conj %1 (:children (%2 subsections))) {} subs-names)
sub-trans (map #(if (= "none" (d/name %))
nil
(translation-keyname :sub-sec %)) subs-names)
match-subsection? (some #(matches-search % @filter-term) sub-trans)
translations (map #(translation-keyname :sc %) (keys subs-bodys))
match-shortcut? (some #(matches-search % @filter-term) translations)
visible? (some #(= % section-id) @open-sections)]
(when (or match-section? match-subsection? match-shortcut?)
[:div {:class (stl/css :section)
:on-click (manage-sections section-id)}
[:> section-title* {:name section-translation
:is-visible visible?
:is-sub false}]
[:div {:style {:display (if visible? "initial" "none")}}
[:> shortcut-subsection* {:subsections subsections
:open-sections open-sections
:manage-sections manage-sections
:is-match-section match-section?
:filter-term filter-term}]]])))
(mf/defc shortcuts-container* (mf/defc shortcuts-container*
[{:keys [class]}] [{:keys [class]}]
(let [workspace-shortcuts app.main.data.workspace.shortcuts/shortcuts (let [profile (mf/deref refs/profile)
path-shortcuts app.main.data.workspace.path.shortcuts/shortcuts custom-shortcuts (get-in profile [:props :custom-shortcuts])
all-workspace-shortcuts (->> (d/deep-merge path-shortcuts workspace-shortcuts)
(add-translation :sc) path-shortcuts-custom (ds/apply-custom-overrides psc/shortcuts custom-shortcuts :path)
workspace-shortcuts-custom (ds/apply-custom-overrides wsc/shortcuts custom-shortcuts :workspace)
workspace-shortcuts (->> (d/deep-merge path-shortcuts-custom workspace-shortcuts-custom)
(ss/add-translation :sc)
(into {})) (into {}))
dashboard-shortcuts (->> app.main.data.dashboard.shortcuts/shortcuts
(add-translation :sc) dashboard-shortcuts-custom (ds/apply-custom-overrides dsc/shortcuts custom-shortcuts :dashboard)
dashboard-shortcuts (->> dashboard-shortcuts-custom
(ss/add-translation :sc)
(into {})) (into {}))
viewer-shortcuts (->> app.main.data.viewer.shortcuts/shortcuts
(add-translation :sc) viewer-shortcuts-custom (ds/apply-custom-overrides vsc/shortcuts custom-shortcuts :viewer)
viewer-shortcuts (->> viewer-shortcuts-custom
(ss/add-translation :sc)
(into {})) (into {}))
open-sections (mf/use-state [[1]])
filter-term (mf/use-state "") open-sections* (mf/use-state [[:workspace]])
open-sections (deref open-sections*)
filter-term* (mf/use-state "")
filter-term (deref filter-term*)
close-fn #(st/emit! (dw/toggle-layout-flag :shortcuts)) close-fn #(st/emit! (dw/toggle-layout-flag :shortcuts))
walk (fn walk [element parent-id] {:keys [all-shortcuts all-sc-names all-sub-names all-section-names]}
(if (nil? element) (ss/build-all-shortcuts workspace-shortcuts dashboard-shortcuts viewer-shortcuts)
element
(let [rec-fn (fn [index [k item]]
(let [item-id (if (nil? parent-id)
[index]
(conj parent-id index))]
[k (assoc item :id item-id :children (walk (:children item) item-id))]))]
(into {} (map-indexed (partial rec-fn) element)))))
workspace-sc-by-subsections (->> (shortcuts->subsections all-workspace-shortcuts)
(add-translation :sub-sec)
(into {}))
dashboard-sc-by-subsections (->> (shortcuts->subsections dashboard-shortcuts)
(add-translation :sub-sec)
(into {}))
viewer-sc-by-subsections (->> (shortcuts->subsections viewer-shortcuts)
(add-translation :sub-sec)
(into {}))
basics-elements (into {} (concat (:children (:basics workspace-sc-by-subsections))
(:children (:basics dashboard-sc-by-subsections))
(:children (:basics viewer-sc-by-subsections))))
workspace-sc-by-subsections (dissoc workspace-sc-by-subsections :basics)
dashboard-sc-by-subsections (dissoc dashboard-sc-by-subsections :basics)
viewer-sc-by-subsections (dissoc viewer-sc-by-subsections :bassics)
all-shortcuts {:basics {:id [1]
:children {:none {:children basics-elements}}
:translation (tr "shortcut-section.basics")}
:workspace {:id [2]
:children workspace-sc-by-subsections
:translation (tr "shortcut-section.workspace")}
:dashboard {:id [3]
:children dashboard-sc-by-subsections
:translation (tr "shortcut-section.dashboard")}
:viewer {:id [4]
:children viewer-sc-by-subsections
:translation (tr "shortcut-section.viewer")}}
all-shortcuts (walk all-shortcuts nil)
all-sc-names (map #(translation-keyname :sc %) (concat
(keys all-workspace-shortcuts)
(keys dashboard-shortcuts)
(keys viewer-shortcuts)))
all-sub-names (map #(translation-keyname :sub-sec %) (concat
(keys workspace-sc-by-subsections)
(keys dashboard-sc-by-subsections)
(keys viewer-sc-by-subsections)))
all-section-names (map #(translation-keyname :sec %) (keys all-shortcuts))
all-item-names (concat all-sc-names all-sub-names all-section-names) all-item-names (concat all-sc-names all-sub-names all-section-names)
match-any? (some #(matches-search % @filter-term) all-item-names) match-any? (some #(matches-search % filter-term) all-item-names)
manage-sections manage-sections
(fn [item] (fn [item]
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(let [is-present? (some #(= % item) @open-sections) (let [is-present? (some #(= % item) open-sections)
new-value (if is-present? new-value (if is-present?
(filterv (fn [element] (not= element item)) @open-sections) (filterv (fn [element] (not= element item)) open-sections)
(conj @open-sections item))] (conj open-sections item))]
(reset! open-sections new-value)))) (reset! open-sections* new-value))))
add-ids (fn [acc node] add-ids (fn [acc node]
(let [id (:id node) (let [id (:id node)
addition (case (count id) parents (when (> (count id) 1)
1 id (mapv (fn [n] (vec (take n id)))
2 [[(first id)] id] (range 1 (count id))))]
3 [[(first id)] [(first id) (second id)]] (into [] (concat acc parents))))
"default" nil)]
(if (= 1 (count addition))
(conj acc addition)
(into [] (concat acc addition)))))
manage-section-on-search manage-section-on-search
(fn [section term] (fn [section term]
@ -468,23 +98,28 @@
manage-sections-on-search manage-sections-on-search
(fn [term] (fn [term]
(if (= term "") (if (= term "")
(reset! open-sections [[1]]) (reset! open-sections* [[:workspace]])
(let [ids (set/union (manage-section-on-search :basics term) (let [ids (set/union (manage-section-on-search :basics term)
(manage-section-on-search :workspace term) (manage-section-on-search :workspace term)
(manage-section-on-search :dashboard term) (manage-section-on-search :dashboard term)
(manage-section-on-search :viewer term))] (manage-section-on-search :viewer term))]
(reset! open-sections ids)))) (reset! open-sections* ids))))
on-search-term-change-2 on-search-term-change-2
(mf/use-callback (mf/use-callback
(fn [value] (fn [value]
(manage-sections-on-search value) (manage-sections-on-search value)
(reset! filter-term value))) (reset! filter-term* value)))
on-search-clear-click on-search-clear-click
(mf/use-callback (mf/use-callback
(fn [_] (fn [_]
(reset! open-sections [[1]]) (reset! open-sections* [[:workspace]])
(reset! filter-term "")))] (reset! filter-term* "")))
go-to-edit-shortcuts
(mf/use-fn
#(st/emit! (rt/nav :settings-shortcuts {} {::rt/new-window true})))]
[:div {:class (dm/str class " " (stl/css :shortcuts))} [:div {:class (dm/str class " " (stl/css :shortcuts))}
[:> panel-title* {:class (stl/css :shortcuts-title) [:> panel-title* {:class (stl/css :shortcuts-title)
@ -494,7 +129,7 @@
[:div {:class (stl/css :search-field)} [:div {:class (stl/css :search-field)}
[:> search-bar* {:on-change on-search-term-change-2 [:> search-bar* {:on-change on-search-term-change-2
:on-clear on-search-clear-click :on-clear on-search-clear-click
:value @filter-term :value filter-term
:placeholder (tr "shortcuts.title") :placeholder (tr "shortcuts.title")
:icon-id i/search :icon-id i/search
:auto-focus true}]] :auto-focus true}]]
@ -502,9 +137,20 @@
(if match-any? (if match-any?
[:div {:class (stl/css :shortcuts-list)} [:div {:class (stl/css :shortcuts-list)}
(for [section all-shortcuts] (for [section all-shortcuts]
[:> shortcut-section* {:key (->> section second :id first) (let [[section-key _] section]
:section section [:> ss/shortcut-section* {:key (name section-key)
:manage-sections manage-sections :section section
:open-sections open-sections :manage-sections manage-sections
:filter-term filter-term}])] :open-sections open-sections
[:div {:class (stl/css :not-found)} (tr "shortcuts.not-found")])])) :filter-term filter-term
:editable? false
:custom-shortcuts custom-shortcuts}]))]
[:div {:class (stl/css :not-found)} (tr "shortcuts.not-found")])
(when (contains? cf/flags :custom-shortcuts)
[:> bl/button-link* {:on-click go-to-edit-shortcuts
:label (tr "shortcuts.edit-on-settings")
:class (stl/css :edit-shortcuts-button)
:icon (mf/html
[:> icon* {:icon-id i/open-link
:size "m"
:aria-hidden true}])}])]))

View File

@ -5,115 +5,63 @@
// Copyright (c) KALEIDOS INC Sucursal en España SL // Copyright (c) KALEIDOS INC Sucursal en España SL
@use "refactor/common-refactor.scss" as deprecated; @use "refactor/common-refactor.scss" as deprecated;
@use "ds/typography.scss" as t;
@use "ds/_borders.scss" as *;
@use "ds/spacing.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
.shortcuts { .shortcuts {
display: grid; display: grid;
grid-template-rows: auto auto 1fr; grid-template-rows: auto auto 1fr;
// TODO: Fix this once we start implementing the DS. // TODO: Fix this once we start implementing the DS.
// We should not be doign these hardcoded calc's. // We should not be doing these hardcoded calc's.
height: calc(100vh - #{deprecated.$s-60}); block-size: calc(100vh - #{px2rem(60)});
} }
.search-field { .search-field {
margin: deprecated.$s-16 deprecated.$s-12 deprecated.$s-4 deprecated.$s-12; display: flex;
align-items: center;
gap: var(--sp-xs);
margin: var(--sp-l) var(--sp-m) var(--sp-xs) var(--sp-m);
// The search bar should fill available space; the reset-all button sits at the end
& > :first-child {
flex: 1;
min-inline-size: 0;
}
} }
.shortcuts-title { .shortcuts-title {
margin: var(--sp-s) var(--sp-s) 0 var(--sp-s); margin: var(--sp-s) var(--sp-s) 0 var(--sp-s);
} }
.section { .shortcuts-list {
margin: 0; @include t.use-typography("body-small");
display: flex;
flex-direction: column;
block-size: 100%;
padding: var(--sp-m);
overflow-y: auto;
color: var(--color-foreground-secondary);
min-block-size: px2rem(250);
} }
.not-found { .not-found {
@include deprecated.body-small-typography; @include t.use-typography("body-small");
color: var(--empty-message-foreground-color); color: var(--color-foreground-secondary);
margin: deprecated.$s-12; margin: var(--sp-m);
} }
.shortcuts-list { .edit-shortcuts-button {
@include t.use-typography("headline-small");
width: fit-content;
display: flex; display: flex;
flex-direction: column; align-items: center;
height: 100%; gap: var(--sp-xs);
padding: deprecated.$s-12; justify-content: center;
overflow-y: auto;
font-size: deprecated.$fs-12;
color: var(--title-foreground-color);
.section-title,
.subsection-title {
@include deprecated.uppercase-title-typography;
display: flex;
align-items: center;
margin: 0;
padding: deprecated.$s-8 0;
cursor: pointer;
.subsection-name,
.section-name {
padding-left: deprecated.$s-4;
}
&:hover {
color: var(--title-foreground-color-hover);
}
}
.subsection-title {
text-transform: none;
padding-left: deprecated.$s-12;
}
.subsection-menu {
margin-bottom: deprecated.$s-4;
}
.sub-menu {
margin-bottom: deprecated.$s-4;
.shortcuts-name {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: deprecated.$s-32;
padding: deprecated.$s-6;
margin-bottom: deprecated.$s-4;
border-radius: deprecated.$br-8;
background-color: var(--pill-background-color);
.command-name {
@include deprecated.body-small-typography;
margin-left: deprecated.$s-2;
color: var(--pill-foreground-color);
}
.keys {
@include deprecated.flex-center;
gap: deprecated.$s-2;
color: var(--pill-foreground-color);
.key {
@include deprecated.body-small-typography;
@include deprecated.flex-center;
text-transform: capitalize;
height: deprecated.$s-20;
padding: deprecated.$s-2 deprecated.$s-6;
border-radius: deprecated.$s-6;
background-color: var(--menu-shortcut-background-color);
}
.space {
margin: 0 deprecated.$s-2;
}
}
}
}
} }

View File

@ -479,7 +479,7 @@
(defn setup-shortcuts (defn setup-shortcuts
[path-editing? drawing-path? text-editing? grid-editing?] [path-editing? drawing-path? text-editing? grid-editing?]
(hooks/use-shortcuts ::workspace wsc/shortcuts) (hooks/use-shortcuts ::workspace wsc/shortcuts :workspace)
(mf/with-effect [] (mf/with-effect []
(.addEventListener js/window "keydown" wsc/on-display-guides-keydown) (.addEventListener js/window "keydown" wsc/on-display-guides-keydown)
@ -490,18 +490,20 @@
(cond (cond
grid-editing? grid-editing?
(do (do
(st/emit! (dsc/push-shortcuts ::grid gsc/shortcuts)) (st/emit! (dsc/push-shortcuts ::grid gsc/shortcuts :workspace
:merge-shortcuts :auto))
(fn [] (fn []
(st/emit! (dsc/pop-shortcuts ::grid)))) (st/emit! (dsc/pop-shortcuts ::grid))))
(or drawing-path? path-editing?) (or drawing-path? path-editing?)
(do (do
(st/emit! (dsc/push-shortcuts ::path psc/shortcuts)) (st/emit! (dsc/push-shortcuts ::path psc/shortcuts :workspace
:merge-shortcuts :auto))
(fn [] (fn []
(st/emit! (dsc/pop-shortcuts ::path)))) (st/emit! (dsc/pop-shortcuts ::path))))
text-editing? text-editing?
(do (do
(st/emit! (dsc/push-shortcuts ::text tsc/shortcuts)) (st/emit! (dsc/push-shortcuts ::text tsc/shortcuts :workspace))
(fn [] (fn []
(st/emit! (dsc/pop-shortcuts ::text))))))) (st/emit! (dsc/pop-shortcuts ::text)))))))

View File

@ -795,10 +795,8 @@
(defn trigger-download (defn trigger-download
[filename blob] [filename blob]
(let [uri (wapi/create-uri blob)] (let [uri (wapi/create-uri blob)]
(try (trigger-download-uri filename (.-type ^js blob) uri)
(trigger-download-uri filename (.-type ^js blob) uri) (js/setTimeout #(wapi/revoke-uri uri) 1000)))
(finally
(wapi/revoke-uri uri)))))
(defn event (defn event
"Create an instance of DOM Event" "Create an instance of DOM Event"

View File

@ -62,6 +62,7 @@
[frontend-tests.ui.layout-container-multiple-test] [frontend-tests.ui.layout-container-multiple-test]
[frontend-tests.ui.measures-menu-props-test] [frontend-tests.ui.measures-menu-props-test]
[frontend-tests.ui.settings-password-schema-test] [frontend-tests.ui.settings-password-schema-test]
[frontend-tests.ui.settings-shortcuts-test]
[frontend-tests.util-clipboard-test] [frontend-tests.util-clipboard-test]
[frontend-tests.util-object-test] [frontend-tests.util-object-test]
[frontend-tests.util-range-tree-test] [frontend-tests.util-range-tree-test]
@ -140,6 +141,7 @@
'frontend-tests.ui.layout-container-multiple-test 'frontend-tests.ui.layout-container-multiple-test
'frontend-tests.ui.measures-menu-props-test 'frontend-tests.ui.measures-menu-props-test
'frontend-tests.ui.settings-password-schema-test 'frontend-tests.ui.settings-password-schema-test
'frontend-tests.ui.settings-shortcuts-test
'frontend-tests.util-clipboard-test 'frontend-tests.util-clipboard-test
'frontend-tests.util-object-test 'frontend-tests.util-object-test
'frontend-tests.util-range-tree-test 'frontend-tests.util-range-tree-test

View File

@ -0,0 +1,180 @@
(ns frontend-tests.ui.settings-shortcuts-test
(:require
[app.main.data.profile :as du]
[app.main.ui.settings.restore-shortcuts-modal :as restore-modal]
[app.main.ui.settings.shortcuts :as sut]
[app.main.ui.shortcuts :as ui-shortcuts]
[cljs.test :as t :include-macros true]
[clojure.string :as str]))
(t/deftest validate-imported-shortcuts-accepts-valid-workspace
(t/is (= {:valid? true}
(sut/validate-imported-shortcuts
{:workspace {:escape "escape"
:increase-zoom "+"
:zoom-lense-decrease "alt+z"}}))))
(t/deftest validate-imported-shortcuts-accepts-optional-contexts
(t/is (= {:valid? true}
(sut/validate-imported-shortcuts
{:workspace {:escape "escape"}
:dashboard {:toggle-theme "alt+m"}
:viewer {:next-frame "right"}}))))
(t/deftest validate-imported-shortcuts-accepts-empty-string-command
(t/is (= {:valid? true}
(sut/validate-imported-shortcuts
{:workspace {:escape ""}}))))
(t/deftest validate-imported-shortcuts-rejects-nil
(let [result (sut/validate-imported-shortcuts nil)]
(t/is (= false (:valid? result)))
(t/is (seq (:errors result)))))
(t/deftest validate-imported-shortcuts-rejects-non-map
(t/is (= false (:valid? (sut/validate-imported-shortcuts "shortcuts"))))
(t/is (= false (:valid? (sut/validate-imported-shortcuts 42))))
(t/is (= false (:valid? (sut/validate-imported-shortcuts []))))
(t/is (= false (:valid? (sut/validate-imported-shortcuts true)))))
(t/deftest validate-imported-shortcuts-accepts-missing-workspace
(t/is (= {:valid? true}
(sut/validate-imported-shortcuts {:dashboard {:toggle-theme "alt+m"}}))))
(t/deftest validate-imported-shortcuts-rejects-non-map-context
(let [result (sut/validate-imported-shortcuts {:workspace "not-a-map"})]
(t/is (= false (:valid? result)))
(t/is (seq (:errors result)))
(t/is (some #(= "workspace" (:path %)) (:errors result)))))
(t/deftest validate-imported-shortcuts-rejects-non-string-command
(let [result (sut/validate-imported-shortcuts {:workspace {:escape 123}})]
(t/is (= false (:valid? result)))
(t/is (seq (:errors result)))
(t/is (some #(= "workspace/escape" (:path %)) (:errors result)))))
(t/deftest validate-imported-shortcuts-rejects-vector-command
(let [result (sut/validate-imported-shortcuts {:workspace {:escape ["ctrl+e"]}})]
(t/is (= false (:valid? result)))
(t/is (seq (:errors result)))
(t/is (some #(= "workspace/escape" (:path %)) (:errors result)))))
(t/deftest validate-imported-shortcuts-rejects-unknown-top-level-key
(let [result (sut/validate-imported-shortcuts {:workspace {:escape "escape"}
:unknown-context {}})]
(t/is (= false (:valid? result)))
(t/is (seq (:errors result)))
(t/is (some #(str/includes? (:path %) "unknown-context") (:errors result)))))
(t/deftest validate-imported-shortcuts-rejects-unknown-workspace-key
(let [result (sut/validate-imported-shortcuts {:workspace {:not-a-real-shortcut "x"}})]
(t/is (= false (:valid? result)))
(t/is (seq (:errors result)))
(t/is (some #(= "workspace/not-a-real-shortcut" (:path %)) (:errors result)))))
(t/deftest validate-imported-shortcuts-rejects-unknown-optional-context-key
(let [result (sut/validate-imported-shortcuts {:viewer {:not-a-real-shortcut "x"}})]
(t/is (= false (:valid? result)))
(t/is (seq (:errors result)))
(t/is (some #(= "viewer/not-a-real-shortcut" (:path %)) (:errors result)))))
(t/deftest validate-imported-shortcuts-never-throws
(t/is (= false (:valid? (sut/validate-imported-shortcuts (js/Date.)))))
(t/is (= false (:valid? (sut/validate-imported-shortcuts #{:workspace}))))
(t/is (= false (:valid? (sut/validate-imported-shortcuts (fn []))))))
(t/deftest import-custom-shortcuts-updates-multiple-contexts
(let [imported {:workspace {:add-comment "alt+c"}
:viewer {:select-all "ctrl+alt+a"}}
all-raw {:add-comment {:command "alt+c"}
:select-all {:command "ctrl+a"}}]
(with-redefs [du/update-profile-props identity]
(t/is (= {:custom-shortcuts {:workspace {:add-comment "alt+c"}
:viewer {:select-all "ctrl+alt+a"}}}
(ui-shortcuts/import-custom-shortcuts imported all-raw))))))
(t/deftest import-custom-shortcuts-preserves-unimported-contexts
(let [current {:workspace {:existing "ctrl+e"}
:dashboard {:toggle-theme "alt+m"}
:viewer {:select-all "ctrl+a"}}
imported {:workspace {:add-comment "alt+c"}}
all-raw {:add-comment {:command "alt+c"}
:existing {:command "ctrl+e"}
:toggle-theme {:command "alt+m"}
:select-all {:command "ctrl+a"}}]
(with-redefs [du/update-profile-props identity]
(t/is (= {:custom-shortcuts {:workspace {:add-comment "alt+c"}
:dashboard {:toggle-theme "alt+m"}
:viewer {:select-all "ctrl+a"}}}
(ui-shortcuts/import-custom-shortcuts
{:profile {:props {:custom-shortcuts current}}
:workspace (:workspace imported)}
all-raw))))))
(t/deftest import-custom-shortcuts-clears-context-when-empty
(let [current {:workspace {:existing "ctrl+e"}}
imported {:workspace {}}
all-raw {}]
(with-redefs [du/update-profile-props identity]
(t/is (= {:custom-shortcuts {:workspace {}}}
(ui-shortcuts/import-custom-shortcuts
{:profile {:props {:custom-shortcuts current}}
:workspace (:workspace imported)}
all-raw))))))
(t/deftest import-custom-shortcuts-disables-conflicting-default
(let [imported {:workspace {:select-all "ctrl+a"}}
all-raw {:select-all {:command "ctrl+a"}
:add-comment {:command "ctrl+a"}}]
(with-redefs [du/update-profile-props identity]
(t/is (= {:custom-shortcuts {:workspace {:select-all "ctrl+a"
:add-comment ""}}}
(ui-shortcuts/import-custom-shortcuts imported all-raw))))))
(t/deftest validate-imported-shortcuts-accepts-viewer-only-import
(t/is (= {:valid? true}
(sut/validate-imported-shortcuts
{:viewer {:next-frame "right"
:prev-frame "left"}}))))
(t/deftest import-custom-shortcuts-disables-duplicate-imported-bindings
(let [imported {:workspace {:move-up "ctrl+up"
:move-to-top "ctrl+up"}}
all-raw {:move-up {:command "ctrl+shift+up"}
:move-to-top {:command "ctrl+shift+top"}}]
(with-redefs [du/update-profile-props identity]
(let [customs (get-in (ui-shortcuts/import-custom-shortcuts imported all-raw)
[:custom-shortcuts :workspace])]
(t/is (= "" (get customs :move-up))
"First imported entry with duplicate command should be cleared")
(t/is (= "ctrl+up" (get customs :move-to-top))
"Last imported entry with duplicate command should survive")))))
(t/deftest extract-shortcut-keys-uses-correct-context-override
(let [customs {:workspace {:undo "shift+z"
:move-nodes "shift+m"}
:dashboard {:toggle-theme "alt+t"}
:viewer {:next-frame "right"}}]
(t/is (= "shift+z" (nth (restore-modal/extract-shortcut-keys :undo customs :workspace) 3))
"Workspace override should be used for :undo")
(t/is (= "alt+t" (nth (restore-modal/extract-shortcut-keys :toggle-theme customs :dashboard) 3))
"Dashboard override should be used for :toggle-theme")
(t/is (= "right" (nth (restore-modal/extract-shortcut-keys :next-frame customs :viewer) 3))
"Viewer override should be used for :next-frame")
(t/is (= "shift+m" (nth (restore-modal/extract-shortcut-keys :move-nodes customs :workspace) 3))
"Workspace override should be used for path-specific :move-nodes")))
(t/deftest extract-shortcut-keys-returns-default-for-each-context
(let [result (restore-modal/extract-shortcut-keys :move-nodes {} :workspace)]
(t/is (string? (nth result 3))
"Should return default command string for :move-nodes via :workspace context")
(t/is (not= "" (nth result 3))
"Default command should not be empty"))
(let [result (restore-modal/extract-shortcut-keys :toggle-theme {} :dashboard)]
(t/is (string? (nth result 3))
"Should return default command string for :toggle-theme in :dashboard context")
(t/is (not= "" (nth result 3))
"Default command should not be empty"))
(let [result (restore-modal/extract-shortcut-keys :next-frame {} :viewer)]
(t/is (nth result 3)
"Should return a default command for :next-frame in :viewer context")))

View File

@ -2113,17 +2113,10 @@ msgstr "مساحة العمل"
msgid "shortcut-subsection.alignment" msgid "shortcut-subsection.alignment"
msgstr "محاذاة" msgstr "محاذاة"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59
msgid "shortcut-subsection.edit"
msgstr "عدّل"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "عام" msgstr "عام"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "عام"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
msgid "shortcut-subsection.main-menu" msgid "shortcut-subsection.main-menu"

View File

@ -2002,16 +2002,8 @@ msgstr "Espai de treball"
msgid "shortcut-subsection.alignment" msgid "shortcut-subsection.alignment"
msgstr "Alineació" msgstr "Alineació"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59
msgid "shortcut-subsection.edit"
msgstr "Edició"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Genèric"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Genèric" msgstr "Genèric"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -3242,16 +3242,8 @@ msgstr "Pracovní plocha"
msgid "shortcut-subsection.alignment" msgid "shortcut-subsection.alignment"
msgstr "Zarovnání" msgstr "Zarovnání"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59
msgid "shortcut-subsection.edit"
msgstr "Upravit"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Obecný"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Obecný" msgstr "Obecný"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -4143,16 +4143,8 @@ msgstr "Arbeitsbereich"
msgid "shortcut-subsection.alignment" msgid "shortcut-subsection.alignment"
msgstr "Ausrichtung" msgstr "Ausrichtung"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59
msgid "shortcut-subsection.edit"
msgstr "Bearbeiten"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Allgemein"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Allgemein" msgstr "Allgemein"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -2873,6 +2873,11 @@ msgstr "Create new team"
msgid "labels.create-team.placeholder" msgid "labels.create-team.placeholder"
msgstr "Enter new team name" msgstr "Enter new team name"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
msgid "labels.current"
msgstr "Current"
#, unused #, unused
msgid "labels.custom-fonts" msgid "labels.custom-fonts"
msgstr "Custom fonts" msgstr "Custom fonts"
@ -2881,6 +2886,10 @@ msgstr "Custom fonts"
msgid "labels.dashboard" msgid "labels.dashboard"
msgstr "Dashboard" msgstr "Dashboard"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
msgid "labels.default"
msgstr "Default"
#: src/app/main/ui/dashboard/file_menu.cljs:337, src/app/main/ui/dashboard/fonts.cljs:286, src/app/main/ui/dashboard/fonts.cljs:363, src/app/main/ui/dashboard/fonts.cljs:397, src/app/main/ui/dashboard/project_menu.cljs:115, src/app/main/ui/dashboard/team.cljs:1379, src/app/main/ui/settings/integrations.cljs:389, src/app/main/ui/settings/profile.cljs:117, src/app/main/ui/settings/profile.cljs:128, src/app/main/ui/settings/profile.cljs:129, src/app/main/ui/workspace/sidebar/assets/common.cljs:231, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:223, src/app/main/ui/workspace/sidebar/versions.cljs:258, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:353, src/app/main/ui/workspace/tokens/management/node_context_menu.cljs:111, src/app/main/ui/workspace/tokens/sets/context_menu.cljs:66, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:378 #: src/app/main/ui/dashboard/file_menu.cljs:337, src/app/main/ui/dashboard/fonts.cljs:286, src/app/main/ui/dashboard/fonts.cljs:363, src/app/main/ui/dashboard/fonts.cljs:397, src/app/main/ui/dashboard/project_menu.cljs:115, src/app/main/ui/dashboard/team.cljs:1379, src/app/main/ui/settings/integrations.cljs:389, src/app/main/ui/settings/profile.cljs:117, src/app/main/ui/settings/profile.cljs:128, src/app/main/ui/settings/profile.cljs:129, src/app/main/ui/workspace/sidebar/assets/common.cljs:231, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:223, src/app/main/ui/workspace/sidebar/versions.cljs:258, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:353, src/app/main/ui/workspace/tokens/management/node_context_menu.cljs:111, src/app/main/ui/workspace/tokens/sets/context_menu.cljs:66, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:378
msgid "labels.delete" msgid "labels.delete"
msgstr "Delete" msgstr "Delete"
@ -5099,16 +5108,12 @@ msgstr "Workspace"
msgid "shortcut-subsection.alignment" msgid "shortcut-subsection.alignment"
msgstr "Alignment" msgstr "Alignment"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.edit" msgid "shortcut-subsection.edit"
msgstr "Edit" msgstr "Edit"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Generic"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Generic" msgstr "Generic"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
@ -5626,11 +5631,79 @@ msgstr "Stop measurement"
msgid "shortcuts.thumbnail-set" msgid "shortcuts.thumbnail-set"
msgstr "Set thumbnails" msgstr "Set thumbnails"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.page"
msgstr "Shortcuts page"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:491, src/app/main/ui/workspace/sidebar/shortcuts.cljs:498 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:491, src/app/main/ui/workspace/sidebar/shortcuts.cljs:498
msgid "shortcuts.title" msgid "shortcuts.title"
msgstr "Keyboard shortcuts" msgstr "Keyboard shortcuts"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:185 #: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.reset"
msgstr "Reset to default"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.disable"
msgstr "Disable shortcut"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.disabled-shortcut"
msgstr "Disabled shortcut"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.overwritten-by"
msgstr "This shortcut has been reassigned to %s"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.no-personalized"
msgstr "Head to All to start customizing your shortcuts."
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.no-disabled"
msgstr "There are not disabled shortcuts"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.reload-hint"
msgstr "After editing shortcuts, reload any other open Penpot tabs to apply the changes."
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.no-shortcuts"
msgstr "There are no shortcuts that match your search"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.personalized"
msgstr "Personalized"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.disabled"
msgstr "Disabled"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.draw-arrow"
msgstr "Arrow"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.draw-line"
msgstr "Line"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.import-export"
msgstr "Import/Export"
#: src/app/main/ui/shortcuts.cljs
msgid "shortcuts.key-combo"
msgstr "Press the key combination"
#: src/app/main/ui/shortcuts.cljs
msgid "shortcuts.edit-modal.conflict"
msgstr "Combination assigned to \"%s\". Saving will remove it there."
#: src/app/main/ui/shortcuts.cljs
msgid "shortcuts.edit-modal.success"
msgstr "This key combination is available and can be assigned without conflicts."
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:182
msgid "shortcuts.toggle-alignment" msgid "shortcuts.toggle-alignment"
msgstr "Toggle dynamic alignment" msgstr "Toggle dynamic alignment"
@ -5642,6 +5715,10 @@ msgstr "Toggle assets"
msgid "shortcuts.toggle-colorpalette" msgid "shortcuts.toggle-colorpalette"
msgstr "Toggle color palette" msgstr "Toggle color palette"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs
msgid "shortcuts.toggle-comments-visibility"
msgstr "Toggle comments visibility"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:188 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:188
msgid "shortcuts.toggle-focus-mode" msgid "shortcuts.toggle-focus-mode"
msgstr "Toggle focus mode" msgstr "Toggle focus mode"
@ -5738,6 +5815,26 @@ msgstr "Zoom lense increase"
msgid "shortcuts.zoom-selected" msgid "shortcuts.zoom-selected"
msgstr "Zoom to selected" msgstr "Zoom to selected"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs
msgid "shortcuts.edit-on-settings"
msgstr "Customize on settings"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
msgid "restore-shortcuts.modal-title"
msgstr "Restore to default configuration"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
msgid "restore-shortcuts.modal-text"
msgstr "The following shortcuts will be restored to their default values."
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
msgid "restore-shortcuts.acction"
msgstr "Acction"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
msgid "restore-shortcuts.restore"
msgstr "Restore"
#: src/app/main/ui/dashboard/subscription.cljs:191 #: src/app/main/ui/dashboard/subscription.cljs:191
msgid "subscription.banner.create-org-info" msgid "subscription.banner.create-org-info"
msgstr "" msgstr ""

View File

@ -2808,6 +2808,10 @@ msgstr "Crea un nuevo equipo"
msgid "labels.create-team.placeholder" msgid "labels.create-team.placeholder"
msgstr "Introduce un nuevo nombre de equipo" msgstr "Introduce un nuevo nombre de equipo"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
msgid "labels.current"
msgstr "Valor Actual"
#, unused #, unused
msgid "labels.custom-fonts" msgid "labels.custom-fonts"
msgstr "Fuentes personalizadas" msgstr "Fuentes personalizadas"
@ -2816,6 +2820,11 @@ msgstr "Fuentes personalizadas"
msgid "labels.dashboard" msgid "labels.dashboard"
msgstr "Panel" msgstr "Panel"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
msgid "labels.default"
msgstr "Valor por defecto"
#: src/app/main/ui/dashboard/file_menu.cljs:337, src/app/main/ui/dashboard/fonts.cljs:286, src/app/main/ui/dashboard/fonts.cljs:363, src/app/main/ui/dashboard/fonts.cljs:397, src/app/main/ui/dashboard/project_menu.cljs:115, src/app/main/ui/dashboard/team.cljs:1379, src/app/main/ui/settings/integrations.cljs:389, src/app/main/ui/settings/profile.cljs:117, src/app/main/ui/settings/profile.cljs:128, src/app/main/ui/settings/profile.cljs:129, src/app/main/ui/workspace/sidebar/assets/common.cljs:231, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:223, src/app/main/ui/workspace/sidebar/versions.cljs:258, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:353, src/app/main/ui/workspace/tokens/management/node_context_menu.cljs:111, src/app/main/ui/workspace/tokens/sets/context_menu.cljs:66, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:378 #: src/app/main/ui/dashboard/file_menu.cljs:337, src/app/main/ui/dashboard/fonts.cljs:286, src/app/main/ui/dashboard/fonts.cljs:363, src/app/main/ui/dashboard/fonts.cljs:397, src/app/main/ui/dashboard/project_menu.cljs:115, src/app/main/ui/dashboard/team.cljs:1379, src/app/main/ui/settings/integrations.cljs:389, src/app/main/ui/settings/profile.cljs:117, src/app/main/ui/settings/profile.cljs:128, src/app/main/ui/settings/profile.cljs:129, src/app/main/ui/workspace/sidebar/assets/common.cljs:231, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:223, src/app/main/ui/workspace/sidebar/versions.cljs:258, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:353, src/app/main/ui/workspace/tokens/management/node_context_menu.cljs:111, src/app/main/ui/workspace/tokens/sets/context_menu.cljs:66, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:378
msgid "labels.delete" msgid "labels.delete"
msgstr "Borrar" msgstr "Borrar"
@ -4943,6 +4952,7 @@ msgstr "Seleccionar elementos que usan este estilo"
msgid "shortcut-section.basics" msgid "shortcut-section.basics"
msgstr "Básicos" msgstr "Básicos"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:418 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:418
msgid "shortcut-section.dashboard" msgid "shortcut-section.dashboard"
msgstr "Panel" msgstr "Panel"
@ -4965,11 +4975,7 @@ msgid "shortcut-subsection.edit"
msgstr "Editar" msgstr "Editar"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Genérico"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Genérico" msgstr "Genérico"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
@ -5475,10 +5481,78 @@ msgstr "Terminar medida"
msgid "shortcuts.thumbnail-set" msgid "shortcuts.thumbnail-set"
msgstr "Activar miniaturas" msgstr "Activar miniaturas"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.page"
msgstr "Página de atajos"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:491, src/app/main/ui/workspace/sidebar/shortcuts.cljs:498 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:491, src/app/main/ui/workspace/sidebar/shortcuts.cljs:498
msgid "shortcuts.title" msgid "shortcuts.title"
msgstr "Atajos de teclado" msgstr "Atajos de teclado"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.reset"
msgstr "Restablecer valor por defecto"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.disable"
msgstr "Deshabilitar atajo"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.disabled-shortcut"
msgstr "Atajo deshabilitado"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.overwritten-by"
msgstr "Este atajo ha sido reasignado a %s"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.no-personalized"
msgstr "Dirígete a TODO para personalizar tus atajos."
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.no-disabled"
msgstr "No hay atajos deshabilitados."
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.reload-hint"
msgstr "Después de editar un atajo, recarga cualquier pestaña de Penpot para aplicar los cambios."
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.no-shortcuts"
msgstr "No hay atajos que coincidan con tu búsqueda."
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.personalized"
msgstr "Personalizados"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.disabled"
msgstr "Deshabilitado"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.draw-arrow"
msgstr "Flecha"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.draw-line"
msgstr "Linea"
#: src/app/main/ui/settings/shortcuts.cljs
msgid "shortcuts.import-export"
msgstr "Importar/Exportar"
#: src/app/main/ui/shortcuts.cljs
msgid "shortcuts.key-combo"
msgstr "Pulsa la combinación de teclas deseada"
#: src/app/main/ui/shortcuts.cljs
msgid "shortcuts.edit-modal.conflict"
msgstr "Combinación usada en \"%s\". Guardar este atajo hará que se elimine allí."
#: src/app/main/ui/shortcuts.cljs
msgid "shortcuts.edit-modal.success"
msgstr "Esta combinación de teclas está disponible y puede asignarse sin conflictos."
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:185 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:185
msgid "shortcuts.toggle-alignment" msgid "shortcuts.toggle-alignment"
msgstr "Alternar alineación" msgstr "Alternar alineación"
@ -5491,6 +5565,10 @@ msgstr "Mostrar/ocultar recursos"
msgid "shortcuts.toggle-colorpalette" msgid "shortcuts.toggle-colorpalette"
msgstr "Mostrar/ocultar paleta de colores" msgstr "Mostrar/ocultar paleta de colores"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs
msgid "shortcuts.toggle-comments-visibility"
msgstr "Activar/desactivar visibilidad de comentarios"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:188 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:188
msgid "shortcuts.toggle-focus-mode" msgid "shortcuts.toggle-focus-mode"
msgstr "Mostrar/ocultar focus mode" msgstr "Mostrar/ocultar focus mode"
@ -5587,6 +5665,26 @@ msgstr "Incrementar zoom a objetivo"
msgid "shortcuts.zoom-selected" msgid "shortcuts.zoom-selected"
msgstr "Zoom a selección" msgstr "Zoom a selección"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs
msgid "shortcuts.edit-on-settings"
msgstr "Personalízalos en ajustes"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
msgid "restore-shortcuts.modal-title"
msgstr "Restaurar la configuración predeterminada"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
msgid "restore-shortcuts.modal-text"
msgstr "Los siguientes atajos de teclado se restaurarán a sus valores predeterminados."
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
msgid "restore-shortcuts.acction"
msgstr "Acción"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
msgid "restore-shortcuts.restore"
msgstr "Restaurar"
#: src/app/main/ui/dashboard/subscription.cljs:191 #: src/app/main/ui/dashboard/subscription.cljs:191
msgid "subscription.banner.create-org-info" msgid "subscription.banner.create-org-info"
msgstr "" msgstr ""

View File

@ -2294,11 +2294,7 @@ msgid "shortcut-subsection.edit"
msgstr "Editatu" msgstr "Editatu"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Orokorra"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Orokorra" msgstr "Orokorra"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -4178,11 +4178,7 @@ msgid "shortcut-subsection.edit"
msgstr "Modifier" msgstr "Modifier"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Générique"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Générique" msgstr "Générique"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -5076,11 +5076,7 @@ msgid "shortcut-subsection.edit"
msgstr "Édition" msgstr "Édition"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Général"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Général" msgstr "Général"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -1145,11 +1145,7 @@ msgid "shortcut-subsection.edit"
msgstr "Editar" msgstr "Editar"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Xenérico"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Xenérico" msgstr "Xenérico"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:64 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:64

View File

@ -2436,11 +2436,7 @@ msgid "shortcut-subsection.edit"
msgstr "Tace" msgstr "Tace"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "gamayya"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "gamayya" msgstr "gamayya"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -4079,11 +4079,7 @@ msgid "shortcut-subsection.edit"
msgstr "עריכה" msgstr "עריכה"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "כללי"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "כללי" msgstr "כללי"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -4032,11 +4032,7 @@ msgid "shortcut-subsection.edit"
msgstr "संपादन" msgstr "संपादन"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "सार्वभौमिक"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "सार्वभौमिक" msgstr "सार्वभौमिक"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -3246,11 +3246,7 @@ msgid "shortcut-subsection.edit"
msgstr "Uredi" msgstr "Uredi"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Generičko"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Generičko" msgstr "Generičko"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -3467,11 +3467,7 @@ msgid "shortcut-subsection.edit"
msgstr "Sunting" msgstr "Sunting"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Generik"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Generik" msgstr "Generik"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -4588,11 +4588,7 @@ msgid "shortcut-subsection.edit"
msgstr "Modifica" msgstr "Modifica"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Generico"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Generico" msgstr "Generico"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -3859,11 +3859,7 @@ msgid "shortcut-subsection.edit"
msgstr "편집" msgstr "편집"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "일반"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "일반" msgstr "일반"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -3786,11 +3786,7 @@ msgid "shortcut-subsection.edit"
msgstr "Labot" msgstr "Labot"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Vispārējs"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Vispārējs" msgstr "Vispārējs"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -2550,11 +2550,7 @@ msgid "shortcut-subsection.edit"
msgstr "Sunting" msgstr "Sunting"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Generik"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Generik" msgstr "Generik"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -4186,11 +4186,7 @@ msgid "shortcut-subsection.edit"
msgstr "Bewerken" msgstr "Bewerken"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Algemeen"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Algemeen" msgstr "Algemeen"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -2286,11 +2286,7 @@ msgid "shortcut-subsection.edit"
msgstr "Edytuj" msgstr "Edytuj"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Ogólny"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Ogólny" msgstr "Ogólny"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -3042,11 +3042,7 @@ msgid "shortcut-subsection.edit"
msgstr "Editar" msgstr "Editar"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Geral"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Geral" msgstr "Geral"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -3309,11 +3309,7 @@ msgid "shortcut-subsection.edit"
msgstr "Editar" msgstr "Editar"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Genérico"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Genérico" msgstr "Genérico"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -3878,11 +3878,7 @@ msgid "shortcut-subsection.edit"
msgstr "Editează" msgstr "Editează"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Generic"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Generic" msgstr "Generic"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -3905,11 +3905,7 @@ msgid "shortcut-subsection.edit"
msgstr "Редактировать" msgstr "Редактировать"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Общее"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Общее" msgstr "Общее"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -2812,11 +2812,7 @@ msgid "shortcut-subsection.edit"
msgstr "Уреди" msgstr "Уреди"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Опште"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Опште" msgstr "Опште"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -5040,11 +5040,7 @@ msgid "shortcut-subsection.edit"
msgstr "Redigera" msgstr "Redigera"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Allmän"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Allmän" msgstr "Allmän"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -5042,11 +5042,7 @@ msgid "shortcut-subsection.edit"
msgstr "Düzenle" msgstr "Düzenle"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Genel"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Genel" msgstr "Genel"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -4113,11 +4113,7 @@ msgid "shortcut-subsection.edit"
msgstr "Редагувати" msgstr "Редагувати"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "Загальні"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "Загальні" msgstr "Загальні"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -2318,10 +2318,6 @@ msgstr "Titete"
msgid "shortcut-subsection.edit" msgid "shortcut-subsection.edit"
msgstr "Sàtunkọ" msgstr "Sàtunkọ"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "àbùdá"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
msgid "shortcut-subsection.main-menu" msgid "shortcut-subsection.main-menu"
msgstr "Akojọ ólórì aṣyn" msgstr "Akojọ ólórì aṣyn"

View File

@ -3502,11 +3502,7 @@ msgid "shortcut-subsection.edit"
msgstr "编辑" msgstr "编辑"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "通用"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "通用" msgstr "通用"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62

View File

@ -3081,12 +3081,9 @@ msgid "shortcut-subsection.edit"
msgstr "編輯" msgstr "編輯"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
msgid "shortcut-subsection.general-dashboard" msgid "shortcut-subsection.generic"
msgstr "一般" msgstr "一般"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
msgid "shortcut-subsection.general-viewer"
msgstr "一般"
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
msgid "shortcut-subsection.main-menu" msgid "shortcut-subsection.main-menu"