diff --git a/package/contents/ui/main.qml b/package/contents/ui/main.qml index 3601afd..f59bb93 100644 --- a/package/contents/ui/main.qml +++ b/package/contents/ui/main.qml @@ -25,4 +25,14 @@ Item { flags: Notification.Persistent urgency: Notification.HighUrgency } + + Notification { + id: notificationInvalidPresetWidths + componentName: "plasma_workspace" + eventId: "notification" + title: "Karousel" + text: "Your preset widths are malformed, please review your Karousel configuration" + flags: Notification.Persistent + urgency: Notification.HighUrgency + } } diff --git a/src/extern/global.d.ts b/src/extern/global.d.ts index 48000b4..14c21af 100644 --- a/src/extern/global.d.ts +++ b/src/extern/global.d.ts @@ -3,3 +3,4 @@ declare const KWin: KWin; declare const Workspace: Workspace; declare const qmlBase: QmlObject; declare const notificationInvalidWindowRules: Notification; +declare const notificationInvalidPresetWidths: Notification; diff --git a/src/lib/config/config.ts b/src/lib/config/config.ts index db75133..42aa0c6 100644 --- a/src/lib/config/config.ts +++ b/src/lib/config/config.ts @@ -7,6 +7,7 @@ type Config = { gapsInnerVertical: number; manualScrollStep: number; manualResizeStep: number; + presetWidths: string; offScreenOpacity: number; untileOnDrag: boolean; stackColumnsByDefault: boolean; diff --git a/src/lib/config/utils.ts b/src/lib/config/utils.ts new file mode 100644 index 0000000..073cf74 --- /dev/null +++ b/src/lib/config/utils.ts @@ -0,0 +1,38 @@ +type PresetWidth = ((screenWidth: number, spacing: number) => number); + +function parsePresetWidths(presetWidths: string): PresetWidth[] { + function parseNumberSafe(str: string) { + const num = Number(str); + if (isNaN(num) || num <= 0) { + throw new Error("Invalid number: " + str); + } + return num; + } + + function parseNumberWithSuffix(str: string, suffix: string) { + if (!str.endsWith(suffix)) { + return undefined; + } + return parseNumberSafe(str.substring(0, str.length-suffix.length).trim()); + } + + function getRatioFunction(ratio: number) { + return (screenWidth: number, spacing: number) => Math.floor((screenWidth + spacing) * ratio - spacing); + } + + return presetWidths.split(",").map((widthStr: string) => { + widthStr = widthStr.trim(); + + const widthPx = parseNumberWithSuffix(widthStr, "px"); + if (widthPx !== undefined) { + return () => widthPx; + } + + const widthPct = parseNumberWithSuffix(widthStr, "%"); + if (widthPct !== undefined) { + return getRatioFunction(widthPct / 100.0); + } + + return getRatioFunction(parseNumberSafe(widthStr)); + }); +} diff --git a/src/lib/keyBindings/Actions.ts b/src/lib/keyBindings/Actions.ts index 90c81e0..ab17dc9 100644 --- a/src/lib/keyBindings/Actions.ts +++ b/src/lib/keyBindings/Actions.ts @@ -316,6 +316,7 @@ namespace Actions { export type Config = { manualScrollStep: number; manualResizeStep: number; + presetWidths: PresetWidth[]; columnResizer: ColumnResizer; }; diff --git a/src/lib/world/World.ts b/src/lib/world/World.ts index 2855a0d..73d99e8 100644 --- a/src/lib/world/World.ts +++ b/src/lib/world/World.ts @@ -8,9 +8,19 @@ class World { constructor(config: Config) { this.workspaceSignalManager = initWorkspaceSignalHandlers(this); + + let presetWidths: PresetWidth[] = []; + try { + presetWidths = parsePresetWidths(config.presetWidths); + } catch (error: any) { + notificationInvalidPresetWidths.sendEvent(); + log("failed to parse presetWidths:", error); + } + this.shortcutActions = registerKeyBindings(this, { manualScrollStep: config.manualScrollStep, manualResizeStep: config.manualResizeStep, + presetWidths: presetWidths, columnResizer: config.scrollingCentered ? new RawResizer() : new ContextualResizer(), }); diff --git a/src/tests/units/config/utils.ts b/src/tests/units/config/utils.ts new file mode 100644 index 0000000..46f68d1 --- /dev/null +++ b/src/tests/units/config/utils.ts @@ -0,0 +1,32 @@ +tests.register("parsePresetWidths", 1, () => { + const screenWidth = 800; + const spacing = 10; + + const testCases = [ + { str: "100%, 50%", result: [800, 395] }, + { str: "100px,50 px", result: [100, 50] }, + { str: "100px, 25 % , 0.1 ", result: [100, 192, 71] }, + { str: "100px, 25 % , 0.1p", error: true }, + { str: "100px, % , 0.1 ", error: true }, + { str: "100px, , 0.1 ", error: true }, + { str: "100px,, 0.1 ", error: true }, + { str: "100px, 25 % , ", error: true }, + { str: "asdf", error: true }, + { str: "", error: true }, + { str: " ", error: true }, + ]; + + function applyPresetWidths(presetWidths: PresetWidth[]) { + return presetWidths.map(f => f(screenWidth, spacing)); + } + + for (const testCase of testCases) { + try { + const result = parsePresetWidths(testCase.str); + assert(!testCase.error); + assertArrayEqual(applyPresetWidths(result), testCase.result!); + } catch (error) { + assert(testCase.error === true); + } + } +}); diff --git a/src/tests/utils/assert.ts b/src/tests/utils/assert.ts index fb77a66..943ed9a 100644 --- a/src/tests/utils/assert.ts +++ b/src/tests/utils/assert.ts @@ -29,6 +29,13 @@ Expected: ${expected} Actual: ${actual}`, skip+1); } +function assertArrayEqual(actual: any[], expected: any[], skip: number = 0) { + const equal = actual.length === expected.length && actual.every((item, index) => item === expected[index]); + assert(equal, `Arrays not equal +Expected: ${expected} +Actual: ${actual}`, skip+1); +} + function assertRectEqual(actual: QmlRect, expected: QmlRect, skip: number = 0) { assert(rectEquals(expected, actual), `QmlRect not equal Expected: ${expected} diff --git a/src/tests/utils/global.ts b/src/tests/utils/global.ts index fbc7658..4044c20 100644 --- a/src/tests/utils/global.ts +++ b/src/tests/utils/global.ts @@ -3,3 +3,4 @@ let KWin: KWin; let Workspace: Workspace; let qmlBase: QmlObject; let notificationInvalidWindowRules: Notification; +let notificationInvalidPresetWidths: Notification;