Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@ export abstract class KeyAction implements RgbColorInterface {
@assertUInt8 b = DEFAULT_RGB_COLOR.b;
@assertUInt8 g = DEFAULT_RGB_COLOR.g;
@assertUInt8 r = DEFAULT_RGB_COLOR.r;
label = '';

protected constructor(keyAction?: RgbColorInterface) {
protected constructor(keyAction?: RgbColorInterface & { label?: string }) {
this.b = keyAction?.b ?? DEFAULT_RGB_COLOR.b;
this.g = keyAction?.g ?? DEFAULT_RGB_COLOR.g;
this.r = keyAction?.r ?? DEFAULT_RGB_COLOR.r;
this.label = keyAction?.label ?? '';
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,8 @@ import { KeyAction } from './key-action.js';

export class KeyLabelAction extends KeyAction {

label: string;

constructor(other?: KeyLabelAction) {
super(other);

if (other) {
this.label = other.label;
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, it } from 'node:test';

import { UhkBuffer } from '../uhk-buffer.js';
import {
KeystrokeAction,
KeystrokeType,
Macro,
MacroArgumentAction,
Module,
PlayMacroAction,
UserConfiguration,
} from './index.js';
import { DEFAULT_SERIALISATION_INFO } from './serialisation-info.js';

describe('module key labels', () => {
const macros: Macro[] = [Object.assign(new Macro(), { id: 1 })];
const userConfiguration = Object.assign(new UserConfiguration(), { macros });
const serialisationInfo = DEFAULT_SERIALISATION_INFO;

function createLabeledKeystroke(): KeystrokeAction {
const keystrokeAction = new KeystrokeAction();
keystrokeAction.type = KeystrokeType.basic;
keystrokeAction.scancode = 4;
keystrokeAction.label = 'Hello note';
return keystrokeAction;
}

it('should round-trip keystroke label through json', ({ assert }) => {
const module = new Module();
module.id = 0;
module.keyActions = [createLabeledKeystroke()];

const json = module.toJsonObject(serialisationInfo, macros);
const restored = new Module().fromJsonObject(json, macros, serialisationInfo);

assert.strictEqual(restored.keyActions[0].label, 'Hello note');
assert.ok(restored.keyActions[0] instanceof KeystrokeAction);
});

it('should round-trip keystroke label through binary', ({ assert }) => {
const module = new Module();
module.id = 0;
module.keyActions = [createLabeledKeystroke()];

const buffer = new UhkBuffer();
module.toBinary(buffer, serialisationInfo, userConfiguration);
buffer.offset = 0;

const restored = new Module().fromBinary(buffer, macros, serialisationInfo);

assert.strictEqual(restored.keyActions.length, 1);
assert.strictEqual(restored.keyActions[0].label, 'Hello note');
assert.ok(restored.keyActions[0] instanceof KeystrokeAction);
});

it('should keep play macro arguments and label in binary', ({ assert }) => {
const module = new Module();
module.id = 0;
const playMacroAction = new PlayMacroAction();
playMacroAction.macroId = 1;
const macroArgument = new MacroArgumentAction();
macroArgument.value = 'arg1';
playMacroAction.macroArguments = [macroArgument];
playMacroAction.label = 'Macro note';
module.keyActions = [playMacroAction];

const buffer = new UhkBuffer();
module.toBinary(buffer, serialisationInfo, userConfiguration);
buffer.offset = 0;

const restored = new Module().fromBinary(buffer, macros, serialisationInfo);
const restoredPlayMacro = restored.keyActions[0] as PlayMacroAction;

assert.ok(restoredPlayMacro instanceof PlayMacroAction);
assert.strictEqual(restoredPlayMacro.label, 'Macro note');
assert.strictEqual(restoredPlayMacro.macroArguments.length, 1);
assert.strictEqual(restoredPlayMacro.macroArguments[0].value, 'arg1');
assert.strictEqual(module.getKeyActionsCount(), 3);
});

it('should omit empty labels from json', ({ assert }) => {
const module = new Module();
module.id = 0;
const keystrokeAction = new KeystrokeAction();
keystrokeAction.type = KeystrokeType.basic;
keystrokeAction.scancode = 4;
module.keyActions = [keystrokeAction];

const json = module.toJsonObject(serialisationInfo, macros);

assert.strictEqual(json.keyActions[0].label, undefined);
});
});
47 changes: 39 additions & 8 deletions packages/uhk-common/src/config-serializer/config-items/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ export class Module {
id: this.id,
keyActions: this.keyActions.map(keyAction => {
if (keyAction && (macros || !(keyAction instanceof PlayMacroAction || keyAction instanceof SwitchKeymapAction))) {
return keyAction.toJsonObject(serialisationInfo, macros);
return {
...keyAction.toJsonObject(serialisationInfo, macros),
...labelToJson(keyAction)
};
}

return new NoneAction().toJsonObject(serialisationInfo);
Expand All @@ -105,6 +108,7 @@ export class Module {
const keyActions = this.getCompressedKeyActions()
for (const keyAction of keyActions) {
keyAction.toBinary(buffer, serialisationInfo, userConfiguration);
writeKeyLabelAction(buffer, keyAction);
}
}

Expand Down Expand Up @@ -137,8 +141,12 @@ export class Module {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fromJsonObjectV1(jsonObject: any, macros: Macro[], serialisationInfo: SerialisationInfo): void {
this.id = jsonObject.id;
this.keyActions = jsonObject.keyActions.map((keyAction) => {
return KeyActionHelper.fromJSONObject(keyAction, macros, serialisationInfo);
this.keyActions = jsonObject.keyActions.map((keyActionJson) => {
const keyAction = KeyActionHelper.fromJSONObject(keyActionJson, macros, serialisationInfo);
if (keyActionJson?.label) {
keyAction.label = keyActionJson.label;
}
return keyAction;
});
}

Expand All @@ -152,9 +160,11 @@ export class Module {
while (processedKeyActionsCount < keyActionsLength) {
const keyAction = KeyActionHelper.createKeyAction(buffer, macros, serialisationInfo)

if (KeyLabelAction instanceof KeyLabelAction) {
// TODO: implement it in other PR
// related to https://github.com/UltimateHackingKeyboard/agent/issues/2289
if (keyAction instanceof KeyLabelAction) {
if (!lastKeyAction) {
throw Error(`${processedKeyActionsCount} key label has no preceding key action`);
}
lastKeyAction.label = keyAction.label;
}
else if (keyAction instanceof MacroArgumentAction) {
if (lastKeyAction instanceof PlayMacroAction) {
Expand Down Expand Up @@ -192,13 +202,14 @@ export class Module {
for (let i = 0; i < this.keyActions.length;) {
const keyAction = this.keyActions[i] || new NoneAction();

if (keyAction instanceof NoneAction) {
if (keyAction instanceof NoneAction && !keyAction.label) {
let blockCount = 1

for (let j = i + 1; j < this.keyActions.length; j++) {
const nextAction = this.keyActions[j] || new NoneAction();

if (nextAction instanceof NoneAction
&& !nextAction.label
&& keyAction.r === nextAction.r
&& keyAction.g === nextAction.g
&& keyAction.b === nextAction.b) {
Expand Down Expand Up @@ -244,10 +255,30 @@ export class Module {
count += keyAction.macroArguments.length;
}

// TODO: Extend when implement KeyLabelAction
if (keyAction?.label) {
count++;
}
}

return count;
}

}

function labelToJson(keyAction: KeyAction): { label?: string } {
if (keyAction.label) {
return { label: keyAction.label };
}

return {};
}

function writeKeyLabelAction(buffer: UhkBuffer, keyAction: KeyAction): void {
if (!keyAction.label) {
return;
}

const keyLabelAction = new KeyLabelAction();
keyLabelAction.label = keyAction.label;
keyLabelAction.toBinary(buffer);
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@
(validAction)="setKeyActionValidState($event)"
></none-tab>
</div>
<div class="popover-note" *ngIf="showNote">
<label class="popover-note__label" for="key-note"><b>Note:</b></label>
<textarea id="key-note"
#noteTextarea
class="form-control"
[rows]="noteRows"
[ngModel]="note"
(ngModelChange)="onNoteChange($event)"
aria-label="Note for this key"></textarea>
</div>
<div class="row popover-action">
<div class="col-8 ps-2">
<form class="popover-action-form">
Expand Down Expand Up @@ -117,7 +127,18 @@
</form>
</div>

<div class="col-4 d-flex p-0 justify-content-end">
<div class="col-4 d-flex p-0 justify-content-end align-items-center">
<button class="btn btn-sm btn-default me-2"
type="button"
[class.active]="showNote"
[attr.aria-pressed]="showNote"
[ngbTooltip]="noteTooltip"
#noteTooltipRef="ngbTooltip"
triggers="hover"
aria-label="Toggle note"
(click)="toggleNote(noteTooltipRef)">
<fa-icon [icon]="faNoteSticky" aria-hidden="true"></fa-icon>
</button>
<button class="btn btn-sm btn-default me-2" type="button" (click)="onCancelClick()"> Cancel</button>
<button class="btn btn-sm btn-primary" [class.disabled]="!keyActionValid" type="button"
(click)="onRemapKey()"> Remap key
Expand Down
25 changes: 25 additions & 0 deletions packages/uhk-web/src/app/components/popover/popover.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,31 @@
background-color: var(--color-popover-bg-light);
}

.popover-note {
display: flex;
align-items: flex-start;
gap: 0.6em;
padding: 0 24px 10px;
background-color: var(--color-popover-bg-light);

.popover-note__label {
margin-top: 0.35rem;
margin-bottom: 0;
}

textarea {
flex: 1;
resize: vertical;
min-height: 3.5rem;
border-width: 1px;

&:focus {
border-width: 1px;
box-shadow: 0 0 0 0.15rem rgba(13, 110, 253, 0.2);
}
}
}

.pe-10 {
padding-right: 10px;
}
Expand Down
43 changes: 41 additions & 2 deletions packages/uhk-web/src/app/components/popover/popover.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
HostListener,
Input,
Expand All @@ -12,8 +13,9 @@ import {
inject,
} from '@angular/core';
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
import { faBan, faClone, faKeyboard, faMousePointer, faPlay } from '@fortawesome/free-solid-svg-icons';
import { faBan, faClone, faKeyboard, faMousePointer, faNoteSticky, faPlay } from '@fortawesome/free-solid-svg-icons';

import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';

Expand Down Expand Up @@ -92,6 +94,7 @@ export class PopoverComponent implements OnChanges {
@Output() remap = new EventEmitter<KeyActionRemap>();

@ViewChild('tab', { static: false }) selectedTab: Tab;
@ViewChild('noteTextarea') noteTextarea?: ElementRef<HTMLTextAreaElement>;

tabName = TabName;
keyActionValid: boolean;
Expand Down Expand Up @@ -143,6 +146,10 @@ export class PopoverComponent implements OnChanges {
macroPlaybackSupported$: Observable<boolean>;
layerOptions$: Observable<LayerOption[]>;
userConfiguration$: Observable<UserConfiguration>;
faNoteSticky = faNoteSticky;
showNote = false;
note = '';
noteTooltip = 'Add a note to this key.';

private readonly store = inject<Store<AppState>>(Store);
private readonly cdRef = inject(ChangeDetectorRef);
Expand All @@ -168,6 +175,9 @@ export class PopoverComponent implements OnChanges {

if (change['defaultKeyAction']) {
this.disableRemapOnAllLayer = false;
this.note = this.defaultKeyAction?.label || '';
this.showNote = this.note.length > 0;
this.updateNoteTooltip();

if (this.defaultKeyAction instanceof KeystrokeAction) {
this.keystrokeActionChange(this.defaultKeyAction);
Expand Down Expand Up @@ -209,10 +219,12 @@ export class PopoverComponent implements OnChanges {
onRemapKey(assignNewMacro?: boolean, navigateToMacro?: boolean): void {
if (this.keyActionValid) {
try {
const action = this.selectedTab.toKeyAction();
action.label = this.showNote ? this.note : '';
this.remap.emit({
remapOnAllKeymap: this.internalRemapInfo.remapOnAllKeymap,
remapOnAllLayer: this.internalRemapInfo.remapOnAllLayer,
action: this.selectedTab.toKeyAction(),
action,
assignNewMacro: assignNewMacro,
navigateToMacro: navigateToMacro,
});
Expand All @@ -223,6 +235,33 @@ export class PopoverComponent implements OnChanges {
}
}

toggleNote(tooltip?: NgbTooltip): void {
tooltip?.close();
this.showNote = !this.showNote;
if (!this.showNote) {
this.note = '';
}
this.updateNoteTooltip();
this.cdRef.detectChanges();

if (this.showNote) {
this.noteTextarea?.nativeElement.focus();
}
}

onNoteChange(note: string): void {
this.note = note;
this.cdRef.markForCheck();
}

get noteRows(): number {
return Math.max(2, this.note.split('\n').length);
}

private updateNoteTooltip(): void {
this.noteTooltip = this.showNote ? 'Remove note.' : 'Add a note to this key.';
}

@HostListener('keydown.escape')
onEscape(): void {
this.cancel.emit();
Expand Down
Loading