24 december 2025
Belangrijk: DarkTable herstelt automatisch de standaardsnelkoppelingen wanneer deze start.
Zodat uw aangepaste bezetting niet door elkaar wordt gehaald, moet u deze automatische deactiveren.
Ga naar DarkTable Instellingen > Ander .
Schakel de optie uit “Herstel toetscombinaties bij opstarten” (indien beschikbaar/actief).
Installatie van het bestand:
Afdronk donkertafel
Vervang het bestand SneltoetsenRc in uw configuratiemap met de nieuwe versie.
Toggle Theme
QWERTZ
QWERTY
AZERTY
Filter Keyboard by Context:
All
Global
Lighttable
Darkroom
Shortcut Key
Action (Value)
Context
Actions
const preloadedShortcutsrcContent = “;
// — CONFIGURATION —
const KEYBOARD_LAYOUTS = {
qwertz: {
fkeys: [
[
[“Escape”],
“SPACER”,
[“F1”, “F1”, “key-fkey”],
[“F2”, “F2”, “key-fkey”],
[“F3”, “F3”, “key-fkey”],
[“F4”, “F4”, “key-fkey”],
“SPACER”,
[“F5”, “F5”, “key-fkey”],
[“F6”, “F6”, “key-fkey”],
[“F7”, “F7”, “key-fkey”],
[“F8”, “F8”, “key-fkey”],
“SPACER”,
[“F9”, “F9”, “key-fkey”],
[“F10”, “F10”, “key-fkey”],
[“F11”, “F11”, “key-fkey”],
[“F12”, “F12”, “key-fkey”],
“SPACER”,
],
],
main: [
[
[“^”, “asciicircum”],
[“1”],
[“2”],
[“3”],
[“4”],
[“5”],
[“6”],
[“7”],
[“8”],
[“9”],
[“0”],
[“ß”, “ssharp”],
[“´”, “acute”],
[“BackSpace”, “BackSpace”, “key-backspace”],
],
[
[“Tab”, “Tab”, “key-tab”, “modifier”],
[“q”],
[“w”],
[“e”],
[“r”],
[“t”],
[“z”],
[“u”],
[“i”],
[“o”],
[“p”],
[“Ü”, “udiaeresis”],
[“+”, “plus”],
[“Return”, “Return”, “key-return”],
],
[
[“Caps”, “Caps_Lock”, “key-caps”, “modifier”],
[“a”],
[“s”],
[“d”],
[“f”],
[“g”],
[“h”],
[“j”],
[“k”],
[“l”],
[“Ö”, “odiaeresis”],
[“Ä”, “adiaeresis”],
[“#”, “numbersign”],
],
[
[“Shift”, “shift”, “key-shift-l”, “modifier”],
[“<", "<"],
["y"],
["x"],
["c"],
["v"],
["b"],
["n"],
["m"],
[",", "comma"],
[".", "period"],
["-", "minus"],
["Shift", "shift", "key-shift-r", "modifier"],
],
[
["Ctrl", "control", "key-ctrl", "modifier"],
["Win", "super", "", "modifier"],
["Alt", "alt", "", "modifier"],
["Space", "space", "key-space"],
["AltGr", "alt", "", "modifier"],
["Ctrl", "control", "key-ctrl", "modifier"],
],
],
special: [
[
["Print", "Print", "key-special"],
["Scroll", "Scroll", "key-special"],
["Pause", "Pause", "key-special"],
["None", "None", "key-special"],
],
[
["Ins", "Insert", "key-special"],
["Home", "Home", "key-special"],
["P_Up", "Page_Up", "key-special"],
],
[
["Delete", "Delete", "key-special"],
["End", "End", "key-special"],
["P_Down", "Page_Down", "key-special"],
],
],
arrows: [
[
["Left", "Left", "key-special"],
["Up", "Up", "key-special"],
["Down", "Down", "key-special"],
["Right", "Right", "key-special"],
],
],
},
qwerty: {
fkeys: [
[
["Escape"],
"SPACER",
["F1", "F1", "key-fkey"],
["F2", "F2", "key-fkey"],
["F3", "F3", "key-fkey"],
["F4", "F4", "key-fkey"],
"SPACER",
["F5", "F5", "key-fkey"],
["F6", "F6", "key-fkey"],
["F7", "F7", "key-fkey"],
["F8", "F8", "key-fkey"],
"SPACER",
["F9", "F9", "key-fkey"],
["F10", "F10", "key-fkey"],
["F11", "F11", "key-fkey"],
["F12", "F12", "key-fkey"],
"SPACER",
],
],
main: [
[
["~", "asciitilde"],
["1"],
["2"],
["3"],
["4"],
["5"],
["6"],
["7"],
["8"],
["9"],
["0"],
["-", "minus"],
["=", "equal"],
["BackSpace", "BackSpace", "key-backspace"],
],
[
["Tab", "Tab", "key-tab", "modifier"],
["q"],
["w"],
["e"],
["r"],
["t"],
["y"],
["u"],
["i"],
["o"],
["p"],
["[", "bracketleft"],
["]", "bracketright"],
["\\", "backslash"],
],
[
["Caps", "Caps_Lock", "key-caps", "modifier"],
["a"],
["s"],
["d"],
["f"],
["g"],
["h"],
["j"],
["k"],
["l"],
[";", "semicolon"],
["'", "quote"],
["Enter", "Return", "key-return"],
],
[
["Shift", "shift", "key-shift-l", "modifier"],
[",", "comma"],
[".", "period"],
["/", "slash"],
["z"],
["x"],
["c"],
["v"],
["b"],
["n"],
["m"],
["Shift", "shift", "key-shift-r", "modifier"],
],
[
["Ctrl", "control", "key-ctrl", "modifier"],
["Win", "super", "", "modifier"],
["Alt", "alt", "", "modifier"],
["Space", "space", "key-space"],
["AltGr", "alt", "", "modifier"],
["Ctrl", "control", "key-ctrl", "modifier"],
],
],
special: [
[
["Print", "Print", "key-special"],
["Scroll", "Scroll", "key-special"],
["Pause", "Pause", "key-special"],
["None", "None", "key-special"],
],
[
["Insert", "Insert", "key-special"],
["Home", "Home", "key-special"],
["P_Up", "Page_Up", "key-special"],
],
[
["Del", "Delete", "key-special"],
["End", "End", "key-special"],
["P_Down", "Page_Down", "key-special"],
],
],
arrows: [
[
["Left", "Left", "key-special"],
["Up", "Up", "key-special"],
["Down", "Down", "key-special"],
["Right", "Right", "key-special"],
],
],
},
azerty: {
fkeys: [
[
["Escape"],
"SPACER",
["F1", "F1", "key-fkey"],
["F2", "F2", "key-fkey"],
["F3", "F3", "key-fkey"],
["F4", "F4", "key-fkey"],
"SPACER",
["F5", "F5", "key-fkey"],
["F6", "F6", "key-fkey"],
["F7", "F7", "key-fkey"],
["F8", "F8", "key-fkey"],
"SPACER",
["F9", "F9", "key-fkey"],
["F10", "F10", "key-fkey"],
["F11", "F11", "key-fkey"],
["F12", "F12", "key-fkey"],
"SPACER",
],
],
main: [
[
["²", "asciicircum"],
["1"],
["2"],
["3"],
["4"],
["5"],
["6"],
["7"],
["8"],
["9"],
["0"],
["°", "degree"],
["+", "plus"],
["BackSpace", "BackSpace", "key-backspace"],
],
[
["Tab", "Tab", "key-tab", "modifier"],
["a"],
["z"],
["e"],
["r"],
["t"],
["y"],
["u"],
["i"],
["o"],
["p"],
["^", "asciicircum"],
["$", "dollar"],
["Return", "Return", "key-return"],
],
[
["Caps", "Caps_Lock", "key-caps", "modifier"],
["q"],
["s"],
["d"],
["f"],
["g"],
["h"],
["j"],
["k"],
["l"],
["m"],
["ù", "grave"],
["*", "asterisk"],
],
[
["Shift", "shift", "key-shift-l", "modifier"],
[" {
if (!str) return “”;
return str
.replace(/&/g, “&”)
.replace(//g, “>”)
.replace(/”/g, “"”)
.replace(/’/g, “'”);
};
const escapeAttr = (str) => {
if (!str) return “”;
return str.replace(/”/g, “"”);
};
const normalizeKey = (key) => {
if (!key) return “”;
return key
.trim()
.replace(/^;+|;+$/g, “”)
.replace(/;{2,}/g, “;”);
};
const getContext = (value) => {
if (!value || !value.includes(“/”)) {
return “global”;
}
const [prefix, view] = value.split(“/”);
if (prefix === “views”) {
if (view && view.startsWith(“darkroom”)) return “darkroom”;
if (view && view.startsWith(“lighttable”))
return “lighttable”;
return “lighttable”; // Default for ‘views’ is lighttable
}
if ([“actions”, “iop”, “utility”].includes(prefix)) {
return “darkroom”;
}
return “global”;
};
const parseFileContent = (content) => {
const lines = content.split(“\n”);
const parsedShortcuts = [];
nextId = 0;
lines.forEach((line) => {
line = line.trim();
if (line.startsWith(“#”) || !line) return;
if (line.includes(“=”)) {
const [key, value] = line.split(“=”, 2);
if (key) {
const trimmedValue = value.trim();
parsedShortcuts.push({
id: nextId++,
key: key.trim(),
value: trimmedValue,
context: getContext(trimmedValue),
});
}
}
});
return parsedShortcuts;
};
const processAndRenderContent = (content, sourceName) => {
try {
shortcuts = parseFileContent(content);
fileInfo.textContent = `Loaded from “${sourceName}”. Found ${shortcuts.length} shortcuts.`;
mainContent.classList.remove(“d-none”);
renderAll();
} catch (err) {
console.error(“Error processing file content:”, err);
fileInfo.textContent = `Error: Could not process content from “${sourceName}”. Check console for details.`;
mainContent.classList.add(“d-none”);
}
};
const renderAll = () => {
renderKeyboard();
renderTable();
renderFilterInfo();
};
const getKeyboardMap = () => {
const keyMap = {};
let shortcutsToDisplay = shortcuts;
if (currentKeyboardContext !== “all”) {
shortcutsToDisplay = shortcuts.filter(
(s) => s.context === currentKeyboardContext,
);
}
shortcutsToDisplay.forEach((entry) => {
const parts = entry.key
.toLowerCase()
.split(“;”)
.map((k) => k.trim());
parts.forEach((part) => {
if (!part) return;
if (!keyMap[part]) keyMap[part] = [];
keyMap[part].push(
`[${entry.context}] ${escapeHTML(entry.key)} = ${escapeHTML(entry.value)}`,
);
});
});
return keyMap;
};
// — RENDER FUNCTIONS —
const renderKeyboard = () => {
const keyMap = getKeyboardMap();
keyboardContainer.innerHTML = “”;
const createKeyElement = (keyInfo) => {
if (!keyInfo)
return ‘
‘;
if (keyInfo === “SPACER”)
return ‘
‘;
const [
displayText,
dataKey = displayText.toLowerCase(),
cssClass = “”,
modifierClass = “”,
] = keyInfo;
const keyId = dataKey.toLowerCase();
if (!keyId) return “”;
const isAssigned = keyMap[keyId];
const isSelected = currentFilterKeys.includes(keyId);
const tooltip = isAssigned
? `
${keyMap[keyId].join(“\n”)} `
: “”;
return `
${displayText}${tooltip}
`;
};
Object.entries(KEYBOARD_LAYOUTS[currentLayout]).forEach(
([blockName, layout]) => {
const blockContainer = document.createElement(“div”);
blockContainer.classList.add(
“keyboard-block”,
`keyboard-${blockName}`,
);
layout.forEach((row) => {
const rowContainer = document.createElement(“div”);
rowContainer.classList.add(“key-row”);
row.forEach((keyInfo) => {
rowContainer.innerHTML +=
createKeyElement(keyInfo);
});
blockContainer.appendChild(rowContainer);
});
keyboardContainer.appendChild(blockContainer);
},
);
};
const renderTable = () => {
let displayData = […shortcuts];
if (currentSearchTerm) {
const lowerCaseSearchTerm = currentSearchTerm.toLowerCase();
displayData = displayData.filter(
(entry) =>
entry.key
.toLowerCase()
.includes(lowerCaseSearchTerm) ||
entry.value
.toLowerCase()
.includes(lowerCaseSearchTerm) ||
entry.context
.toLowerCase()
.includes(lowerCaseSearchTerm),
);
}
if (currentFilterKeys.length > 0) {
displayData = displayData.filter((entry) => {
const entryKeys = entry.key
.toLowerCase()
.split(“;”)
.map((k) => k.trim());
return currentFilterKeys.every((filterKey) =>
entryKeys.includes(filterKey),
);
});
}
switch (currentSortBy) {
case “key”:
displayData.sort((a, b) => a.key.localeCompare(b.key));
break;
case “value”:
displayData.sort((a, b) =>
a.value.localeCompare(b.value),
);
break;
}
tableBody.innerHTML =
displayData.length === 0 &&
!document.querySelector(“.add-new-row”)
? ‘
No shortcuts found for the current filter. ‘
: displayData
.map(
(entry) =>
`
${entry.context} Update Delete `,
)
.join(“”);
};
const renderFilterInfo = () => {
if (currentFilterKeys.length > 0) {
filterInfo.innerHTML = `Filter active:
${currentFilterKeys.join(” + “)} Clear Filter `;
} else {
filterInfo.innerHTML = “”;
}
};
// — EVENT HANDLERS —
fileInput.addEventListener(“change”, (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
processAndRenderContent(e.target.result, file.name);
};
reader.readAsText(file);
});
sortSelect.addEventListener(“change”, (e) => {
currentSortBy = e.target.value;
renderTable();
});
searchInput.addEventListener(“input”, (e) => {
currentSearchTerm = e.target.value;
renderTable();
});
downloadButton.addEventListener(“click”, () => {
const fileContent = shortcuts
.map((entry) => `${entry.key}=${entry.value}`)
.join(“\n”);
const blob = new Blob([fileContent], {
type: “text/plain;charset=utf-8”,
});
const a = document.createElement(“a”);
a.href = URL.createObjectURL(blob);
a.download = “shortcutsrc”;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
addShortcutButton.addEventListener(“click”, () => {
if (document.querySelector(“.add-new-row”)) return;
const newRow = document.createElement(“tr”);
newRow.classList.add(“add-new-row”);
newRow.innerHTML = `
– Save Cancel `;
tableBody.prepend(newRow);
});
keyboardContainer.addEventListener(“click”, (e) => {
const keyElement = e.target.closest(“.filter-key”);
if (!keyElement) return;
const filterKey = keyElement.dataset.key;
if (e.shiftKey) {
const index = currentFilterKeys.indexOf(filterKey);
if (index > -1) {
currentFilterKeys.splice(index, 1);
} else {
currentFilterKeys.push(filterKey);
}
} else {
currentFilterKeys =
currentFilterKeys.length === 1 &&
currentFilterKeys[0] === filterKey
? []
: [filterKey];
}
renderAll();
});
contextFilterButtons.addEventListener(“click”, (e) => {
const button = e.target.closest(“button”);
if (!button) return;
currentKeyboardContext = button.dataset.context;
contextFilterButtons
.querySelectorAll(“button”)
.forEach((btn) => {
btn.classList.remove(
“btn-primary”,
“btn-outline-secondary”,
);
if (btn === button) {
btn.classList.add(“btn-primary”);
} else {
btn.classList.add(“btn-outline-secondary”);
}
});
renderKeyboard();
});
filterInfo.addEventListener(“click”, (e) => {
if (e.target.id === “clear-filter-btn”) {
currentFilterKeys = [];
renderAll();
}
});
tableBody.addEventListener(“click”, (e) => {
const row = e.target.closest(“tr”);
if (!row) return;
const handleUpdate = (isNew) => {
const keyInput = row.querySelector(“td:nth-child(1) input”);
const valueInput = row.querySelector(
“td:nth-child(2) input”,
);
const newKey = normalizeKey(keyInput.value);
const newValue = valueInput.value.trim();
if (!newKey || !newValue) {
alert(“Key and Value cannot be empty.”);
return;
}
const newContext = getContext(newValue);
const entryId = isNew ? -1 : parseInt(row.dataset.id, 10);
const conflict = shortcuts.find(
(s) =>
s.id !== entryId &&
s.key === newKey &&
(s.context === newContext ||
s.context === “global” ||
newContext === “global”),
);
if (conflict) {
alert(
`Conflict found! The key ‘${newKey}’ is already assigned for the context ‘${newContext}’ (Action: ${conflict.value}). Global shortcuts cannot be overwritten in a specific context and vice versa with the same key.`,
);
return;
}
if (isNew) {
shortcuts.push({
id: nextId++,
key: newKey,
value: newValue,
context: newContext,
});
} else {
const entry = shortcuts.find((s) => s.id === entryId);
if (entry) {
entry.key = newKey;
entry.value = newValue;
entry.context = newContext;
}
}
renderAll();
};
if (e.target.classList.contains(“save-new-btn”))
handleUpdate(true);
if (e.target.classList.contains(“update-btn”))
handleUpdate(false);
if (e.target.classList.contains(“cancel-new-btn”)) row.remove();
if (e.target.classList.contains(“delete-btn”)) {
const entryId = parseInt(row.dataset.id, 10);
const entryIndex = shortcuts.findIndex(
(s) => s.id === entryId,
);
if (
entryIndex > -1 &&
confirm(
`Really delete shortcut “${shortcuts[entryIndex].key}”?`,
)
) {
shortcuts.splice(entryIndex, 1);
renderAll();
}
}
});
const loadFromUrl = async () => {
const urlParams = new URLSearchParams(window.location.search);
const srcUrl = urlParams.get(“src”);
if (srcUrl) {
const fileInputCard = fileInput.closest(“.card”);
if (fileInputCard) {
fileInputCard.classList.add(“d-none”);
}
try {
fileInfo.textContent = `Loading from URL: ${srcUrl}`;
const response = await fetch(srcUrl);
if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status}`,
);
}
const content = await response.text();
processAndRenderContent(content, srcUrl);
} catch (err) {
console.error(
“Error fetching or processing file from URL:”,
err,
);
fileInfo.textContent = `Error: Could not load file from URL. ${err.message}`;
mainContent.classList.add(“d-none”);
if (fileInputCard) {
fileInputCard.classList.remove(“d-none”);
}
}
}
};
document.body.addEventListener(“dragover”, (event) => {
event.preventDefault();
document.body.classList.add(“drag-over”);
});
document.body.addEventListener(“dragleave”, () => {
document.body.classList.remove(“drag-over”);
});
document.body.addEventListener(“drop”, (event) => {
event.preventDefault();
document.body.classList.remove(“drag-over”);
const file = event.dataTransfer.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
processAndRenderContent(e.target.result, file.name);
};
reader.readAsText(file);
});
// — INITIALIZATION —
document.addEventListener(“DOMContentLoaded”, () => {
if (
preloadedShortcutsrcContent &&
preloadedShortcutsrcContent.trim() !== “”
) {
const fileInputCard = fileInput.closest(“.card”);
if (fileInputCard) {
fileInputCard.classList.add(“d-none”);
}
processAndRenderContent(
preloadedShortcutsrcContent,
“pre-loaded variable”,
);
} else {
loadFromUrl();
}
// Standard-Theme auf “dark” setzen
document.body.classList.add(“dark-mode”);
localStorage.setItem(“theme”, “dark”);
// QWERTZ als Standard-Tastaturbelegung
keyboardLayoutSelect.value = “qwertz”;
currentLayout = “qwertz”;
renderAll();
});
// — THEME —
document
.getElementById(“theme-toggle”)
.addEventListener(“click”, () => {
document.body.classList.toggle(“dark-mode”);
localStorage.setItem(
“theme”,
document.body.classList.contains(“dark-mode”)
? “dark”
: “light”,
);
});
if (localStorage.getItem(“theme”) === “dark”) {
document.body.classList.add(“dark-mode”);
}
// — KEYBOARD LAYOUT —
keyboardLayoutSelect.addEventListener(“change”, (e) => {
currentLayout = e.target.value;
renderAll();
});