diff --git a/packages/uhk-common/src/models/uhk-module-ids.ts b/packages/uhk-common/src/models/uhk-module-ids.ts index c648ec98bfe..c81905e3e08 100644 --- a/packages/uhk-common/src/models/uhk-module-ids.ts +++ b/packages/uhk-common/src/models/uhk-module-ids.ts @@ -10,3 +10,12 @@ export const UHK_MODULE_IDS = Object.freeze({ export type UHK_MODULE_IDS_KEY_TYPE = keyof typeof UHK_MODULE_IDS; export type UHK_MODULE_IDS_TYPE = typeof UHK_MODULE_IDS[UHK_MODULE_IDS_KEY_TYPE]; + +export const UHK_HALVES_AND_MODULE_IDS = [ + UHK_MODULE_IDS.RIGHT_HALF, + UHK_MODULE_IDS.LEFT_HALF, + UHK_MODULE_IDS.LEFT_KEY_CLUSTER, + UHK_MODULE_IDS.RIGHT_TRACKBALL, + UHK_MODULE_IDS.RIGHT_TRACKPOINT, + UHK_MODULE_IDS.RIGHT_TOUCHPAD, +] diff --git a/packages/uhk-web/src/app/components/keymap/edit/keymap-edit.component.html b/packages/uhk-web/src/app/components/keymap/edit/keymap-edit.component.html index ef9acbe18ed..0f499326b74 100644 --- a/packages/uhk-web/src/app/components/keymap/edit/keymap-edit.component.html +++ b/packages/uhk-web/src/app/components/keymap/edit/keymap-edit.component.html @@ -6,8 +6,10 @@ [halvesInfo]="halvesInfo$ | async" [keyboardLayout]="keyboardLayout$ | async" [allowNewLayers]="true" + [allowLayerCopy]="true" [allowLayerDoubleTap]="allowLayerDoubleTap$ | async" [backlightingMode]="backlightingMode$ | async" + [canPasteLayer]="hasCopiedLayer$ | async" [currentLayer]="currentLayer$ | async" [isBacklightingColoring]="isBacklightingColoring$ | async" [lastEditedKey]="lastEditedKey$ | async" diff --git a/packages/uhk-web/src/app/components/keymap/edit/keymap-edit.component.ts b/packages/uhk-web/src/app/components/keymap/edit/keymap-edit.component.ts index ae1f658eaa6..b35622c142b 100644 --- a/packages/uhk-web/src/app/components/keymap/edit/keymap-edit.component.ts +++ b/packages/uhk-web/src/app/components/keymap/edit/keymap-edit.component.ts @@ -16,6 +16,7 @@ import { backlightingColorPalette, backlightingMode, getHalvesInfo, + getHasCopiedLayer, getKeyboardLayout, getLayerOptions, getSecondaryRoleOptions, @@ -57,6 +58,7 @@ export class KeymapEditComponent implements OnDestroy { backlightingMode$: Observable; currentLayer$: Observable; deletable$: Observable; + hasCopiedLayer$: Observable; isBacklightingColoring$: Observable; keymap$: Observable; keyboardLayout$: Observable; @@ -117,6 +119,7 @@ export class KeymapEditComponent implements OnDestroy { }); this.deletable$ = store.select(isKeymapDeletable); + this.hasCopiedLayer$ = store.select(getHasCopiedLayer); this.keyboardLayout$ = store.select(getKeyboardLayout); this.allowLayerDoubleTap$ = store.select(layerDoubleTapSupported); diff --git a/packages/uhk-web/src/app/components/layers/layers.component.html b/packages/uhk-web/src/app/components/layers/layers.component.html index 8489e01f0b2..06f62c59cda 100644 --- a/packages/uhk-web/src/app/components/layers/layers.component.html +++ b/packages/uhk-web/src/app/components/layers/layers.component.html @@ -27,6 +27,35 @@ + + + +
+ +
@@ -80,7 +109,6 @@
-
  • The Base layer is active by default (when no other layers are active). This layer can never be deleted.
  • diff --git a/packages/uhk-web/src/app/components/layers/layers.component.scss b/packages/uhk-web/src/app/components/layers/layers.component.scss index ced0d1aee56..138b1b24063 100644 --- a/packages/uhk-web/src/app/components/layers/layers.component.scss +++ b/packages/uhk-web/src/app/components/layers/layers.component.scss @@ -32,6 +32,10 @@ font-size: 18px; } + circle-tooltip { + align-self: center; + } + .btn-group { flex-wrap: wrap; .btn { @@ -39,5 +43,13 @@ } } } + + .layer-actions { + flex-wrap: wrap; + + .btn { + flex-grow: 0; + } + } } } diff --git a/packages/uhk-web/src/app/components/layers/layers.component.ts b/packages/uhk-web/src/app/components/layers/layers.component.ts index 9ce052116c1..89047a57ae9 100644 --- a/packages/uhk-web/src/app/components/layers/layers.component.ts +++ b/packages/uhk-web/src/app/components/layers/layers.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import { faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { faCopy, faPaste, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { colord, RgbColor } from 'colord'; import { LayerName, RgbColorInterface } from 'uhk-common'; @@ -14,7 +14,9 @@ import { LayerOption, ModifyColorOfBacklightingColorPalettePayload } from '../.. changeDetection: ChangeDetectionStrategy.OnPush, }) export class LayersComponent { + @Input() allowLayerCopy = false; @Input() allowNewLayers: boolean; + @Input() canPasteLayer = false; @Input() current: LayerOption; @Input() layerOptions: LayerOption[]; @Input() paletteColors: Array = []; @@ -27,10 +29,14 @@ export class LayersComponent { @Output() selectLayer = new EventEmitter(); @Output() toggleColorFromPalette = new EventEmitter(); @Output() addLayer = new EventEmitter(); + @Output() copyLayer = new EventEmitter(); + @Output() pasteLayer = new EventEmitter(); @Output() removeLayer = new EventEmitter(); @ViewChild('deleteTooltip') deleteTooltip: NgbTooltip; + faCopy = faCopy; + faPaste = faPaste; faPlus = faPlus; faTrash = faTrash; LayerName = LayerName; @@ -56,6 +62,14 @@ export class LayersComponent { this.addLayer.emit(layerOption.id); } + onCopyLayer(): void { + this.copyLayer.emit(); + } + + onPasteLayer(): void { + this.pasteLayer.emit(); + } + onColorSelected(index): void { this.toggleColorFromPalette.emit(index); } diff --git a/packages/uhk-web/src/app/components/svg/wrap/svg-keyboard-wrap.component.html b/packages/uhk-web/src/app/components/svg/wrap/svg-keyboard-wrap.component.html index decace2d5ac..9424a9b138f 100644 --- a/packages/uhk-web/src/app/components/svg/wrap/svg-keyboard-wrap.component.html +++ b/packages/uhk-web/src/app/components/svg/wrap/svg-keyboard-wrap.component.html @@ -1,6 +1,8 @@ { this.keyPosition = this.keyElement.getBoundingClientRect(); diff --git a/packages/uhk-web/src/app/store/actions/keymap.ts b/packages/uhk-web/src/app/store/actions/keymap.ts index 5c599d4aaa6..05eeeb13177 100644 --- a/packages/uhk-web/src/app/store/actions/keymap.ts +++ b/packages/uhk-web/src/app/store/actions/keymap.ts @@ -9,6 +9,8 @@ import { ExchangeKeysActionModel, OpenPopoverModel } from '../../models'; export enum ActionTypes { Add = '[Keymap] Add keymap', AddLayer = '[Keymap] Add keymap layer', + CopyLayer = '[Keymap] Copy layer', + PasteLayer = '[Keymap] Paste layer', Duplicate = '[Keymap] Duplicate keymap', EditAbbr = '[Keymap] Edit keymap abbreviation', EditName = '[Keymap] Edit keymap title', @@ -41,6 +43,20 @@ export class AddLayerAction implements Action { } } +export class CopyLayerAction implements Action { + type = ActionTypes.CopyLayer; + + constructor(public payload: number) { + } +} + +export class PasteLayerAction implements Action { + type = ActionTypes.PasteLayer; + + constructor(public payload: number) { + } +} + export class DuplicateKeymapAction implements Action { type = ActionTypes.Duplicate; @@ -170,6 +186,8 @@ export class SelectLayerAction implements Action { export type Actions = AddKeymapAction | AddLayerAction + | CopyLayerAction + | PasteLayerAction | DuplicateKeymapAction | EditKeymapAbbreviationAction | EditKeymapNameAction diff --git a/packages/uhk-web/src/app/store/effects/user-config.ts b/packages/uhk-web/src/app/store/effects/user-config.ts index f5ccf860dc0..d8fd2f6f202 100644 --- a/packages/uhk-web/src/app/store/effects/user-config.ts +++ b/packages/uhk-web/src/app/store/effects/user-config.ts @@ -111,6 +111,7 @@ export class UserConfigEffects { Keymaps.ActionTypes.EditAbbr, Keymaps.ActionTypes.SetDefault, Keymaps.ActionTypes.Remove, Keymaps.ActionTypes.SaveKey, Keymaps.ActionTypes.EditDescription, Keymaps.ActionTypes.ExchangeKeys, Keymaps.ActionTypes.AddLayer, Keymaps.ActionTypes.RemoveLayer, Keymaps.ActionTypes.SetKeyColor, + Keymaps.ActionTypes.PasteLayer, Macros.ActionTypes.Add, Macros.ActionTypes.Duplicate, Macros.ActionTypes.EditName, Macros.ActionTypes.Remove, Macros.ActionTypes.AddAction, Macros.ActionTypes.SaveAction, Macros.ActionTypes.DeleteAction, Macros.ActionTypes.ReorderAction, Macros.ActionTypes.DuplicateAction, diff --git a/packages/uhk-web/src/app/store/index.ts b/packages/uhk-web/src/app/store/index.ts index 3487e9ef605..2628ea085be 100644 --- a/packages/uhk-web/src/app/store/index.ts +++ b/packages/uhk-web/src/app/store/index.ts @@ -139,6 +139,7 @@ export const getMacroMap = createSelector(userConfigState, fromUserConfig.getMac export const lastEditedKey = createSelector(userConfigState, fromUserConfig.lastEditedKey); export const getOpenPopover = createSelector(userConfigState, fromUserConfig.getOpenPopover); export const getSelectedLayerOption = createSelector(userConfigState, fromUserConfig.getSelectedLayerOption); +export const getHasCopiedLayer = createSelector(userConfigState, fromUserConfig.getHasCopiedLayer); export const getLayerOptions = createSelector(userConfigState, fromUserConfig.getLayerOptions); export const getSecondaryRoleOptions = createSelector(getSelectedLayerOption, getLayerOptions, (selectedLayer, layerOptions): SelectOptionData[] => { diff --git a/packages/uhk-web/src/app/store/reducers/default-user-configuration.reducer.ts b/packages/uhk-web/src/app/store/reducers/default-user-configuration.reducer.ts index 226eb957d14..0114435b229 100644 --- a/packages/uhk-web/src/app/store/reducers/default-user-configuration.reducer.ts +++ b/packages/uhk-web/src/app/store/reducers/default-user-configuration.reducer.ts @@ -26,7 +26,8 @@ export const initialState: State = { layerOptions: initLayerOptions(), loading: false, selectedLayerOption: getBaseLayerOption(), - theme: '' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + theme: (window as any).getUhkTheme() }; export function reducer(state = initialState, action: Actions | AppActions.Actions) { diff --git a/packages/uhk-web/src/app/store/reducers/user-configuration.ts b/packages/uhk-web/src/app/store/reducers/user-configuration.ts index da1cb72cab2..ae35b0a73e2 100644 --- a/packages/uhk-web/src/app/store/reducers/user-configuration.ts +++ b/packages/uhk-web/src/app/store/reducers/user-configuration.ts @@ -40,6 +40,7 @@ import { SIMPLE_ADVANCED_SECONDARY_ROLE_CONFIGURATION_PRESET_NAME, SwitchKeymapAction, SwitchLayerAction, + UHK_HALVES_AND_MODULE_IDS, UserConfiguration } from 'uhk-common'; import { @@ -60,6 +61,7 @@ import { findModuleById, isValidName, setSvgKeyboardCoverColorsOfAllLayer, + setSvgKeyboardCoverColorsOfKeymapLayers, setSvgKeyboardCoverColorsOfLayer, } from '../../util'; import * as AppActions from '../actions/app'; @@ -93,6 +95,8 @@ export interface State { theme: string; customAdvancedSecondaryRoleConfiguration?: AdvancedSecondaryRoleConfiguration; isCustomPresetTheLastLoadedPreset: boolean; + copiedLayer?: Layer; + copiedLayerMacroNames?: Map; } export const initialState: State = { @@ -106,7 +110,8 @@ export const initialState: State = { newPairedDevices: [], newPairedDevicesAdding: false, selectedLayerOption: getBaseLayerOption(), - theme: '', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + theme: (window as any).getUhkTheme(), isCustomPresetTheLastLoadedPreset: false, }; @@ -326,6 +331,7 @@ export function reducer( newKeymap.name = generateName(state.userConfiguration.keymaps, newKeymap.name); } newKeymap.isDefault = (state.userConfiguration.keymaps.length === 0); + setSvgKeyboardCoverColorsOfKeymapLayers(state.userConfiguration.backlightingMode, newKeymap, state.theme) const userConfiguration: UserConfiguration = Object.assign(new UserConfiguration(), state.userConfiguration); userConfiguration.keymaps = insertItemInNameOrder(state.userConfiguration.keymaps, newKeymap); @@ -382,6 +388,169 @@ export function reducer( return newState; } + case KeymapActions.ActionTypes.CopyLayer: { + const layerId = (action as KeymapActions.CopyLayerAction).payload; + const currentKeymap = state.userConfiguration.keymaps + .find(keymap => keymap.abbreviation === state.selectedKeymapAbbr); + const layerToCopy = currentKeymap.layers.find(layer => layer.id === layerId); + + // Snapshot the names of the macros referenced by the copied layer. Macro ids are + // config specific, so a later paste (possibly into another keymap or device) has to + // re-link them by name. + const copiedLayerMacroNames = new Map(); + for (const module of layerToCopy.modules) { + for (const keyAction of module.keyActions) { + if (keyAction instanceof PlayMacroAction && !copiedLayerMacroNames.has(keyAction.macroId)) { + const macro = state.userConfiguration.macros.find(m => m.id === keyAction.macroId); + if (macro) { + copiedLayerMacroNames.set(keyAction.macroId, macro.name); + } + } + } + } + + return { + ...state, + copiedLayer: new Layer(layerToCopy), + copiedLayerMacroNames, + }; + } + + case KeymapActions.ActionTypes.PasteLayer: { + const targetLayerId = (action as KeymapActions.PasteLayerAction).payload; + const copiedLayer = state.copiedLayer; + const macroIdByName = new Map( + state.userConfiguration.macros.map(macro => [macro.name, macro.id]) + ); + const keymapAbbreviations = new Set(state.userConfiguration.keymaps.map(keymap => keymap.abbreviation)); + + let pasteToKeymap: Keymap; + let pasteToLayer: Layer; + + let userConfiguration: UserConfiguration = Object.assign(new UserConfiguration(), state.userConfiguration); + // 1. add module to the existing layer if it does not exist + userConfiguration.keymaps = userConfiguration.keymaps.map(keymap => { + if (keymap.abbreviation !== state.selectedKeymapAbbr) { + return keymap; + } + + keymap = new Keymap(keymap); + pasteToKeymap = keymap; + + keymap.layers = keymap.layers.map(layer => { + if (layer.id !== targetLayerId) { + return layer; + } + + const pastedLayer = new Layer(); + pastedLayer.id = layer.id; + pastedLayer.modules = []; + pasteToLayer = pastedLayer; + + for (const moduleId of UHK_HALVES_AND_MODULE_IDS) { + const destinationModule = layer.modules.find(findModuleById(moduleId)); + const copiedModule = copiedLayer.modules.find(findModuleById(moduleId)); + + // the module neither on the destination nor copied layer so we skip it + if (!destinationModule && !copiedModule) { + continue + } + + // the module exists only on the destination layer + if (destinationModule && !copiedModule) { + pastedLayer.modules.push(destinationModule); + } + + // use the copied module to ensure every key from it will copy + // for example when uhk 80 key copies to uhk 60 + pastedLayer.modules.push(copiedModule); + } + + setSvgKeyboardCoverColorsOfLayer(userConfiguration.backlightingMode, pastedLayer, state.theme); + + return pastedLayer; + }); + + return keymap; + }); + + // 2. we simulate Remap key events because we have so many business rule + // that don't want to duplicate. + + // The base layer drives the SwitchLayerActions + // if we copy to the base layer then the copied layer will the base layer + const baseLayer = targetLayerId === LayerName.base + ? copiedLayer + : pasteToKeymap.layers.find(layer => layer.id === LayerName.base) + + // - The SwitchLayerAction of Base layer has precedence + // - PlayMacroAction is re-linked to the target macro sharing the copied macro's name; if no such + // macro exists the key becomes a NoneAction (keeping its color). + // - SwitchKeymapAction is kept only + // - if the referenced keymap abbreviation still exists, otherwise it + // becomes a NoneAction (keeping its color). + // - if the referenced keymap is different from the pasted keymap + for (const copiedModule of copiedLayer.modules) { + const baseModule = baseLayer.modules.find(findModuleById(copiedModule.id)); + + for (let keyId = 0; keyId < copiedModule.keyActions.length; keyId++) { + const baseKey = baseModule?.keyActions[keyId]; + + let copiedKeyAction = copiedModule.keyActions?.[keyId]; + + if (baseKey instanceof SwitchLayerAction) { + const isLayerExists = pasteToKeymap.layers.some(layer => layer.id === baseKey.layer); + if (isLayerExists) { + copiedKeyAction = new SwitchLayerAction(baseKey); + } + else { + copiedKeyAction = new NoneAction(baseKey); + } + } + else if (copiedKeyAction instanceof PlayMacroAction) { + const macroName = state.copiedLayerMacroNames.get(copiedKeyAction.macroId); + const macroId = macroIdByName.get(macroName); + + if (macroId === undefined) { + copiedKeyAction = new NoneAction(copiedKeyAction) + } + } + else if (copiedKeyAction instanceof SwitchKeymapAction) { + if (!keymapAbbreviations.has(copiedKeyAction.keymapAbbreviation) || copiedKeyAction.keymapAbbreviation === pasteToKeymap.abbreviation) { + copiedKeyAction = new NoneAction(copiedKeyAction) + } + } + else if (copiedKeyAction instanceof SwitchLayerAction && pasteToLayer.id !== LayerName.base) { + copiedKeyAction = new NoneAction(copiedKeyAction) + } + else if (copiedKeyAction instanceof KeystrokeAction && pasteToLayer.id !== LayerName.base) { + if (copiedKeyAction.secondaryRoleAction !== undefined) { + copiedKeyAction = new NoneAction(copiedKeyAction) + } + } + + const pasteToKeyAction = new KeymapActions.SaveKeyAction({ + keymap: pasteToKeymap, + layer: pasteToLayer.id, + module: copiedModule.id, + key: keyId, + keyAction: { + remapOnAllKeymap: false, + remapOnAllLayer: false, + action: copiedKeyAction + } + }); + + userConfiguration = saveKeyAction(userConfiguration, pasteToKeyAction) + } + } + + return { + ...state, + userConfiguration, + }; + } + case KeymapActions.ActionTypes.EditName: { const payload = (action as KeymapActions.EditKeymapNameAction).payload; @@ -1304,6 +1473,7 @@ export const getLayerOptions = (state: State): LayerOption[] => Array .from(state.layerOptions.values()) .sort((a, b) => a.order - b.order); export const getSelectedLayerOption = (state: State): LayerOption => state.selectedLayerOption; +export const getHasCopiedLayer = (state: State): boolean => !!state.copiedLayer; export const getSelectedMacroAction = (state: State): SelectedMacroAction => state.selectedMacroAction; export const getSelectedModuleConfiguration = (state: State): ModuleConfiguration => { if(!state.selectedModuleConfigurationId) { @@ -1608,6 +1778,7 @@ function getKeyActionByExchangeKey(userConfig: UserConfiguration, exchangeKey: E } function reassignUserConfig(state: State): State { + // TODO: use UserConfiguration.clone() const userConfiguration = Object.assign(new UserConfiguration(), state.userConfiguration); userConfiguration.keymaps = userConfiguration.keymaps.map(keymap => new Keymap(keymap)); userConfiguration.macros = userConfiguration.macros.map(macro => new Macro(macro)); diff --git a/packages/uhk-web/src/app/util/index.ts b/packages/uhk-web/src/app/util/index.ts index 7c69497352e..2105cf05169 100644 --- a/packages/uhk-web/src/app/util/index.ts +++ b/packages/uhk-web/src/app/util/index.ts @@ -5,5 +5,6 @@ export * from './has-none-ascii-characters'; export * from './html-helper'; export * from './key-modifier-model-mapper'; export * from './set-svg-keyboard-cover-colors-of-all-layer'; +export * from './set-svg-keyboard-cover-colors-of-keymap-layers'; export * from './set-svg-keyboard-cover-colors-of-layer'; export * from './validators'; diff --git a/packages/uhk-web/src/app/util/set-svg-keyboard-cover-colors-of-all-layer.ts b/packages/uhk-web/src/app/util/set-svg-keyboard-cover-colors-of-all-layer.ts index 66ca63d89a5..ecc713d0bc2 100644 --- a/packages/uhk-web/src/app/util/set-svg-keyboard-cover-colors-of-all-layer.ts +++ b/packages/uhk-web/src/app/util/set-svg-keyboard-cover-colors-of-all-layer.ts @@ -1,14 +1,10 @@ -import { Keymap, UserConfiguration } from 'uhk-common'; +import { UserConfiguration } from 'uhk-common'; + +import { setSvgKeyboardCoverColorsOfKeymapLayers } from './set-svg-keyboard-cover-colors-of-keymap-layers'; -import { setSvgKeyboardCoverColorsOfLayer } from './set-svg-keyboard-cover-colors-of-layer'; export function setSvgKeyboardCoverColorsOfAllLayer(userConfig: UserConfiguration, theme: string): void { userConfig.keymaps = userConfig.keymaps.map(keymap => { - keymap = new Keymap(keymap); - for (const layer of keymap.layers) { - setSvgKeyboardCoverColorsOfLayer(userConfig.backlightingMode, layer, theme); - } - - return keymap; + return setSvgKeyboardCoverColorsOfKeymapLayers(userConfig.backlightingMode, keymap, theme); }); } diff --git a/packages/uhk-web/src/app/util/set-svg-keyboard-cover-colors-of-keymap-layers.ts b/packages/uhk-web/src/app/util/set-svg-keyboard-cover-colors-of-keymap-layers.ts new file mode 100644 index 00000000000..15d83c272be --- /dev/null +++ b/packages/uhk-web/src/app/util/set-svg-keyboard-cover-colors-of-keymap-layers.ts @@ -0,0 +1,12 @@ +import { BacklightingMode, Keymap } from 'uhk-common'; + +import { setSvgKeyboardCoverColorsOfLayer } from './set-svg-keyboard-cover-colors-of-layer'; + +export function setSvgKeyboardCoverColorsOfKeymapLayers(backligtingMode: BacklightingMode, keymap: Keymap, theme: string): Keymap { + keymap = new Keymap(keymap); + for (const layer of keymap.layers) { + setSvgKeyboardCoverColorsOfLayer(backligtingMode, layer, theme); + } + + return keymap +} diff --git a/packages/uhk-web/src/styles/_global.scss b/packages/uhk-web/src/styles/_global.scss index 59f7da8520c..a29e495b7f8 100644 --- a/packages/uhk-web/src/styles/_global.scss +++ b/packages/uhk-web/src/styles/_global.scss @@ -280,6 +280,11 @@ kbd { max-width: 350px; } +.tooltip-layer-action .tooltip-inner { + width: 320px; + max-width: 320px; +} + mwl-confirmation-popover-window { .popover { &.bottom {