refactore: create feature modules (#387)

* add @angular/cli to the project

* increase nodejs version -> 8.2.1

* add lerna

* merge web and shared module

* move electron module into packages as uhk-agent

Electron agent functionality is not working

* delete symlinker

* convert private properties to public of component if used in html

* revert uhk-message.component

* fix component path

* fix the correct name of the uhk-message.component.scss

* building web and electron module

* delete uhk-renderer package

* handle device connect disconnect state

* add privilege detection

* fix set privilege functionality

* turn back download keymap functionality

* add bootstrap, select2 js and fix null pointer exception

* turn back upload data to keyboard

* fix send keymap

* fix test-serializer

* add missing package.json

* merging

* fix appveyor build

* fix linting

* turn back electron storage service

* commit the missing electron-datastorage-repository

* update node to 8.3.0 in .nvmrc and log node version in appveyor build

* set exact version number in appveyor build

* vertical align privilege and missing device components

* set back node version to 8 in appveyor

* move node-usb dependency from usb dir to root

maybe it is fix the appveyor build

* revert usb to root

* fix electron builder script

* fix electron builder script

* turn off electron devtools

* remove CTRL+U functionality

* fix CTRL+o

* fix lint error

* turnoff store freeze

* start process when got `Error: EPERM: operation not permitted` error

* move files from root usb dir -> packages/usb
This commit is contained in:
Róbert Kiss
2017-08-19 20:02:17 +02:00
committed by László Monda
parent 97770f67c0
commit 0f558e4132
524 changed files with 25606 additions and 5036 deletions

View File

@@ -0,0 +1,7 @@
<div class="row">
<h1 class="col-xs-12 pane-title">
<i class="fa fa-puzzle-piece"></i>
<span class="macro__name pane-title__name">{{ name$ | async }}</span>
</h1>
</div>
To be done...

View File

@@ -0,0 +1,5 @@
:host {
width: 100%;
height: 100%;
display: block;
}

View File

@@ -0,0 +1,23 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/pluck';
@Component({
selector: 'add-on',
templateUrl: './add-on.component.html',
styleUrls: ['./add-on.component.scss'],
host: {
'class': 'container-fluid'
}
})
export class AddOnComponent {
name$: Observable<string>;
constructor(route: ActivatedRoute) {
this.name$ = route
.params
.pluck<{}, string>('name');
}
}

View File

@@ -0,0 +1,10 @@
import { Routes } from '@angular/router';
import { AddOnComponent } from './add-on.component';
export const addOnRoutes: Routes = [
{
path: 'add-on/:name',
component: AddOnComponent
}
];

View File

@@ -0,0 +1,2 @@
export * from './add-on.component';
export * from './add-on.routes';

View File

@@ -0,0 +1,32 @@
<div class="row">
<div class="col-xs-12">
<div class="checkbox">
<label>
<input type="checkbox"
[checked]="settings.checkForUpdateOnStartUp"
(change)="emitCheckForUpdateOnStartUp($event.target.checked)"> Automatically check for update on
application start
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox"
[checked]="settings.usePreReleaseUpdate"
(change)="emitUsePreReleaseUpdate($event.target.checked)"> Allow alpha / pre release
</label>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Version:</label>
<div class="col-sm-10">
<p class="form-control-static">{{version}}</p>
</div>
</div>
<button class="btn btn-link" (click)="emitCheckForUpdate()">
Check for update
<span *ngIf="checkingForUpdate"
class="fa fa-spinner fa-spin"></span>
</button>
</div>
</div>

View File

@@ -0,0 +1,34 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { State } from '../../store/reducers/auto-update-settings';
@Component({
selector: 'auto-update-settings',
templateUrl: './auto-update-settings.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AutoUpdateSettings {
@Input() version: string;
@Input() settings: State | undefined;
@Input() checkingForUpdate: boolean;
@Output() toggleCheckForUpdateOnStartUp = new EventEmitter<boolean>();
@Output() toggleUsePreReleaseUpdate = new EventEmitter<boolean>();
@Output() checkForUpdate = new EventEmitter();
constructor() {
}
emitCheckForUpdateOnStartUp(value: boolean) {
this.toggleCheckForUpdateOnStartUp.emit(value);
}
emitUsePreReleaseUpdate(value: boolean) {
this.toggleUsePreReleaseUpdate.emit(value);
}
emitCheckForUpdate() {
this.checkForUpdate.emit();
}
}

View File

@@ -0,0 +1 @@
export * from './slider';

View File

@@ -0,0 +1 @@
export { KeyboardSliderComponent } from './keyboard-slider.component';

View File

@@ -0,0 +1,13 @@
<svg-keyboard *ngFor="let layer of layers; let index = index; trackBy: trackKeyboard"
[@layerState]="layerAnimationState[index]"
[moduleConfig]="layer.modules"
[keybindAnimationEnabled]="keybindAnimationEnabled"
[halvesSplit]="halvesSplit"
[capturingEnabled]="capturingEnabled"
[selectedKey]="selectedKey"
[selected]="selectedKey?.layerId === index"
(keyClick)="keyClick.emit($event)"
(keyHover)="keyHover.emit($event)"
(capture)="capture.emit($event)"
>
</svg-keyboard>

After

Width:  |  Height:  |  Size: 511 B

View File

@@ -0,0 +1,8 @@
svg-keyboard {
width: 95%;
max-width: 1400px;
position: absolute;
left: 0;
transform: translateX(-101%);
user-select: none;
}

View File

@@ -0,0 +1,105 @@
import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { animate, keyframes, state, style, transition, trigger } from '@angular/animations';
import { Layer } from '../../../config-serializer/config-items/layer';
type AnimationKeyboard =
'leftIn' |
'leftOut' |
'rightIn' |
'rightOut';
@Component({
selector: 'keyboard-slider',
templateUrl: './keyboard-slider.component.html',
styleUrls: ['./keyboard-slider.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
// We use 101%, because there was still a trace of the keyboard in the screen when animation was done
animations: [
trigger('layerState', [
state('leftIn, rightIn', style({
transform: 'translateX(-50%)',
left: '50%'
})),
state('leftOut', style({
transform: 'translateX(-101%)',
left: '0'
})),
state('rightOut', style({
transform: 'translateX(0)',
left: '101%'
})),
transition('leftOut => leftIn, rightOut => leftIn', [
animate('400ms ease-out', keyframes([
style({ transform: 'translateX(0%)', left: '101%', offset: 0 }),
style({ transform: 'translateX(-50%)', left: '50%', offset: 1 })
]))
]),
transition('leftIn => leftOut, rightIn => leftOut', [
animate('400ms ease-out', keyframes([
style({ transform: 'translateX(-50%)', left: '50%', offset: 0 }),
style({ transform: 'translateX(-101%)', left: '0%', offset: 1 })
]))
]),
transition('* => rightIn', [
animate('400ms ease-out', keyframes([
style({ transform: 'translateX(-101%)', left: '0%', offset: 0 }),
style({ transform: 'translateX(-50%)', left: '50%', offset: 1 })
]))
]),
transition('* => rightOut', [
animate('400ms ease-out', keyframes([
style({ transform: 'translateX(-50%)', left: '50%', offset: 0 }),
style({ transform: 'translateX(0%)', left: '101%', offset: 1 })
]))
]),
transition(':leave', [
animate('2000ms ease-out', keyframes([
style({ opacity: 1, offset: 0 }),
style({ opacity: 0, offset: 1 })
]))
])
])
]
})
export class KeyboardSliderComponent implements OnChanges {
@Input() layers: Layer[];
@Input() currentLayer: number;
@Input() keybindAnimationEnabled: boolean;
@Input() capturingEnabled: boolean;
@Input() halvesSplit: boolean;
@Input() selectedKey: { layerId: number, moduleId: number, keyId: number };
@Output() keyClick = new EventEmitter();
@Output() keyHover = new EventEmitter();
@Output() capture = new EventEmitter();
layerAnimationState: AnimationKeyboard[];
ngOnChanges(changes: SimpleChanges) {
if (changes['layers']) {
this.layerAnimationState = this.layers.map<AnimationKeyboard>(() => 'leftOut');
this.layerAnimationState[this.currentLayer] = 'leftIn';
}
const layerChange = changes['currentLayer'];
if (layerChange) {
const prevValue = layerChange.isFirstChange() ? layerChange.currentValue : layerChange.previousValue;
this.onLayerChange(prevValue, layerChange.currentValue);
}
}
trackKeyboard(index: number) {
return index;
}
onLayerChange(oldIndex: number, index: number): void {
if (index > oldIndex) {
this.layerAnimationState[oldIndex] = 'leftOut';
this.layerAnimationState[index] = 'leftIn';
} else {
this.layerAnimationState[oldIndex] = 'rightOut';
this.layerAnimationState[index] = 'rightIn';
}
}
}

View File

@@ -0,0 +1,35 @@
<h1>
<i class="fa fa-keyboard-o"></i>
<span>Add new keymap</span>
</h1>
<div class="keymap__search clearfix">
<div class="input-group">
<span class="input-group-addon" id="sizing-addon1">
<i class="fa fa-search"></i>
</span>
<input type="text" class="form-control" placeholder="Search ..." (input)="filterKeyboards($event.target.value)">
</div>
<div class="keymap__search_amount">
{{ (presets$ | async).length }} / {{ (presetsAll$ | async).length }} keymaps shown
</div>
</div>
<div class="keymap__list">
<div #keyboard class="keymap__list_item" *ngFor="let keymap of presets$ | async">
<h2>{{ keymap.name }}</h2>
<p class="keymap__description">
{{ keymap.description }}
</p>
<svg-keyboard-wrap
[keymap]="keymap"
[popoverEnabled]="false"
[tooltipEnabled]="true"
>
</svg-keyboard-wrap>
<div class="btn-group btn-group-lg">
<button class="btn btn-default" (click)="addKeymap(keymap)">Add keymap</button>
</div>
</div>
</div>
<div *ngIf="(presets$ | async).length === 0">
Sorry, no keyboard found under this search query.
</div>

View File

@@ -0,0 +1,61 @@
:host {
overflow-y: auto;
display: block;
height: 100%;
}
.uhk__layer-switcher--wrapper {
position: relative;
&:before {
content: attr(data-title);
display: inline-block;
position: absolute;
bottom: -0.3em;
right: 100%;
font-size: 2.4rem;
padding-right: 0.25em;
margin: 0;
}
}
.keymap {
&__search {
margin-top: 10px;
.input-group {
width: 100%;
max-width: 350px;
float: left;
}
&_amount {
float: left;
margin: 7px 0 0 20px;
}
}
&__description {
margin-bottom: 20px;
}
&__list {
margin-top: 40px;
&_item {
margin-bottom: 50px;
}
.btn-group-lg {
margin: 30px 0 0;
width: 100%;
text-align: center;
.btn {
float: none;
padding-left: 50px;
padding-right: 50px;
}
}
}
}

View File

@@ -0,0 +1,45 @@
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/combineLatest';
import 'rxjs/add/operator/publishReplay';
import { Keymap } from '../../../config-serializer/config-items/keymap';
import { AppState } from '../../../store';
import { KeymapActions } from '../../../store/actions';
@Component({
selector: 'keymap-add',
templateUrl: './keymap-add.component.html',
styleUrls: ['./keymap-add.component.scss'],
host: {
'class': 'container-fluid'
}
})
export class KeymapAddComponent {
presets$: Observable<Keymap[]>;
presetsAll$: Observable<Keymap[]>;
private filterExpression$: BehaviorSubject<string>;
constructor(private store: Store<AppState>) {
this.presetsAll$ = store.select((appState: AppState) => appState.presetKeymaps);
this.filterExpression$ = new BehaviorSubject('');
this.presets$ = this.presetsAll$
.combineLatest(this.filterExpression$, (keymaps: Keymap[], filterExpression: string) => {
return keymaps.filter((keymap: Keymap) => keymap.name.toLocaleLowerCase().includes(filterExpression));
})
.publishReplay(1)
.refCount();
}
filterKeyboards(filterExpression: string) {
this.filterExpression$.next(filterExpression);
}
addKeymap(keymap: Keymap) {
this.store.dispatch(KeymapActions.addKeymap(keymap));
}
}

View File

@@ -0,0 +1,2 @@
export { KeymapEditComponent } from './keymap-edit.component';
export { KeymapEditGuard } from './keymap-edit-guard.service';

View File

@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/let';
import 'rxjs/add/operator/switchMap';
import { Store } from '@ngrx/store';
import { Keymap } from '../../../config-serializer/config-items/keymap';
import { AppState } from '../../../store/index';
import { getKeymaps } from '../../../store/reducers/user-configuration';
@Injectable()
export class KeymapEditGuard implements CanActivate {
constructor(private store: Store<AppState>, private router: Router) { }
canActivate(): Observable<boolean> {
return this.store
.let(getKeymaps())
.do((keymaps: Keymap[]) => {
const defaultKeymap = keymaps.find(keymap => keymap.isDefault);
if (defaultKeymap) {
this.router.navigate(['/keymap', defaultKeymap.abbreviation]);
}
})
.switchMap(() => Observable.of(false));
}
}

View File

@@ -0,0 +1,8 @@
<ng-template [ngIf]="keymap$ | async">
<keymap-header [keymap]="keymap$ | async" [deletable]="deletable$ | async" (downloadClick)="downloadKeymap()"></keymap-header>
<svg-keyboard-wrap [keymap]="keymap$ | async" [halvesSplit]="keyboardSplit"></svg-keyboard-wrap>
</ng-template>
<div *ngIf="!(keymap$ | async)" class="not-found">
Sorry, there is no keymap with this abbreviation.
</div>

View File

@@ -0,0 +1,11 @@
:host {
width: 100%;
display: block;
}
.not-found {
margin-top: 30px;
font-size: 16px;
text-align: center;
}

View File

@@ -0,0 +1,90 @@
import { Component, HostListener, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import '@ngrx/core/add/operator/select';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/let';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/publishReplay';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/pluck';
import 'rxjs/add/operator/combineLatest';
import { saveAs } from 'file-saver';
import { Keymap } from '../../../config-serializer/config-items/keymap';
import { AppState } from '../../../store';
import { getKeymap, getKeymaps, getUserConfiguration } from '../../../store/reducers/user-configuration';
import 'rxjs/add/operator/pluck';
import { SvgKeyboardWrapComponent } from '../../svg/wrap/svg-keyboard-wrap.component';
@Component({
selector: 'keymap-edit',
templateUrl: './keymap-edit.component.html',
styleUrls: ['./keymap-edit.component.scss'],
host: {
'class': 'container-fluid'
}
})
export class KeymapEditComponent {
@ViewChild(SvgKeyboardWrapComponent) wrap: SvgKeyboardWrapComponent;
keyboardSplit: boolean;
deletable$: Observable<boolean>;
protected keymap$: Observable<Keymap>;
constructor(protected store: Store<AppState>,
route: ActivatedRoute) {
this.keymap$ = route
.params
.pluck<{}, string>('abbr')
.switchMap((abbr: string) => store.let(getKeymap(abbr)))
.publishReplay(1)
.refCount();
this.deletable$ = store.let(getKeymaps())
.map((keymaps: Keymap[]) => keymaps.length > 1);
}
downloadKeymap() {
const exportableJSON$: Observable<string> = this.keymap$
.switchMap(keymap => this.toExportableJSON(keymap))
.map(exportableJSON => JSON.stringify(exportableJSON));
this.keymap$
.combineLatest(exportableJSON$)
.first()
.subscribe(latest => {
const keymap = latest[0];
const exportableJSON = latest[1];
const fileName = keymap.name + '_keymap.json';
saveAs(new Blob([exportableJSON], { type: 'application/json' }), fileName);
});
}
@HostListener('window:keydown.alt.s', ['$event'])
toggleKeyboardSplit() {
this.keyboardSplit = !this.keyboardSplit;
}
private toExportableJSON(keymap: Keymap): Observable<any> {
return this.store
.let(getUserConfiguration())
.first()
.map(userConfiguration => {
return {
site: 'https://ultimatehackingkeyboard.com',
description: 'Ultimate Hacking Keyboard keymap',
keyboardModel: 'UHK60',
dataModelVersion: userConfiguration.dataModelVersion,
objectType: 'keymap',
objectValue: keymap.toJsonObject()
};
});
}
}

View File

@@ -0,0 +1,47 @@
<uhk-header>
<div class="row">
<h1 class="col-xs-12 pane-title">
<i class="fa fa-keyboard-o"></i>
<input #name cancelable
class="keymap__name pane-title__name"
type="text"
(change)="editKeymapName($event.target.value)"
(keyup.enter)="name.blur()"
/> keymap
(<input #abbr cancelable
class="keymap__abbrev pane-title__abbrev"
type="text"
(change)="editKeymapAbbr($event.target.value)"
(keyup.enter)="abbr.blur()"
[attr.maxLength]="3"
/>)
<i class="fa keymap__is-default"
[ngClass]="{'fa-star-o': !keymap.isDefault, 'fa-star': keymap.isDefault}"
data-toggle="tooltip"
data-placement="bottom"
[title]="starTitle"
(click)="setDefault()"
></i>
<i class="glyphicon glyphicon-trash keymap__remove pull-right"
[title]="trashTitle"
[class.disabled]="!deletable"
data-toggle="tooltip"
data-placement="bottom"
html="true"
(click)="removeKeymap()"
></i>
<i class="fa fa-files-o keymap__duplicate pull-right"
title="Duplicate keymap"
data-toggle="tooltip"
data-placement="bottom"
(click)="duplicateKeymap()"
></i>
<i class="fa fa-download keymap__download pull-right"
title="Download keymap"
[html]="true"
data-toggle="tooltip"
data-placement="bottom"
(click)="onDownloadIconClick()"></i>
</h1>
</div>
</uhk-header>

View File

@@ -0,0 +1,83 @@
@import '../../../../styles/variables';
:host {
display: block;
}
.keymap {
&__is-default {
&.fa-star-o {
cursor: pointer;
&:hover {
color: $icon-hover;
}
}
}
&__remove {
font-size: 0.75em;
top: 8px;
&:not(.disabled):hover {
cursor: pointer;
color: $icon-hover-delete;
}
&.disabled {
opacity: 0.25;
}
}
&__duplicate {
font-size: 0.75em;
top: 7px;
margin-right: 15px;
position: relative;
&:hover {
cursor: pointer;
color: $icon-hover;
}
}
}
.keymap__download {
top: 10px;
font-size: 0.8em;
position: relative;
margin-right: 10px;
&:hover {
cursor: pointer;
color: $icon-hover;
}
}
.pane-title {
margin-bottom: 1em;
&__name,
&__abbrev {
border: none;
border-bottom: 2px dotted #999;
padding: 0;
margin: 0 0.25rem;
&:focus {
box-shadow: 0 0 0 1px #ccc, 0 0 5px 0 #ccc;
border-color: transparent;
}
}
&__name {
width: 290px;
text-overflow: ellipsis;
}
&__abbrev {
width: 90px;
text-align: center;
}
}

View File

@@ -0,0 +1,114 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
Output,
Renderer2,
SimpleChanges,
ViewChild
} from '@angular/core';
import { Store } from '@ngrx/store';
import { Keymap } from '../../../config-serializer/config-items/keymap';
import { AppState } from '../../../store';
import { KeymapActions } from '../../../store/actions';
const DEFAULT_TRASH_TITLE = '<span class="text-nowrap">Delete keymap</span>';
@Component({
selector: 'keymap-header',
templateUrl: './keymap-header.component.html',
styleUrls: ['./keymap-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class KeymapHeaderComponent implements OnChanges {
@Input() keymap: Keymap;
@Input() deletable: boolean;
@Output() downloadClick = new EventEmitter<void>();
@ViewChild('name') keymapName: ElementRef;
@ViewChild('abbr') keymapAbbr: ElementRef;
starTitle: string;
trashTitle: string = DEFAULT_TRASH_TITLE;
constructor(private store: Store<AppState>, private renderer: Renderer2) { }
ngOnChanges(changes: SimpleChanges) {
if (changes['keymap']) {
this.setKeymapTitle();
this.setName();
this.setAbbreviation();
}
if (changes['deletable']) {
this.setTrashTitle();
}
}
setDefault() {
if (!this.keymap.isDefault) {
this.store.dispatch(KeymapActions.setDefault(this.keymap.abbreviation));
}
}
removeKeymap() {
if (this.deletable) {
this.store.dispatch(KeymapActions.removeKeymap(this.keymap.abbreviation));
}
}
duplicateKeymap() {
this.store.dispatch(KeymapActions.duplicateKeymap(this.keymap));
}
editKeymapName(name: string) {
if (name.length === 0) {
this.setName();
return;
}
this.store.dispatch(KeymapActions.editKeymapName(this.keymap.abbreviation, name));
}
editKeymapAbbr(newAbbr: string) {
const regexp = new RegExp(/^[a-zA-Z\d]+$/g);
if (newAbbr.length < 1 || newAbbr.length > 3 || !regexp.test(newAbbr)) {
this.setAbbreviation();
return;
}
newAbbr = newAbbr.toUpperCase();
this.store.dispatch(KeymapActions.editKeymapAbbr(this.keymap.name, this.keymap.abbreviation, newAbbr));
}
setKeymapTitle(): void {
this.starTitle = this.keymap.isDefault
? 'This is the default keymap which gets activated when powering the keyboard.'
: 'Makes this keymap the default keymap which gets activated when powering the keyboard.';
}
setTrashTitle(): void {
this.trashTitle = this.deletable
? DEFAULT_TRASH_TITLE
: '<span class="text-nowrap">The last keymap cannot be deleted.</span>';
}
onDownloadIconClick(): void {
this.downloadClick.emit();
}
private setName(): void {
this.renderer.setProperty(this.keymapName.nativeElement, 'value', this.keymap.name);
}
private setAbbreviation() {
this.renderer.setProperty(this.keymapAbbr.nativeElement, 'value', this.keymap.abbreviation);
}
}

View File

@@ -0,0 +1,4 @@
export * from './add/keymap-add.component';
export * from './edit/keymap-edit.component';
export * from './header/keymap-header.component';
export * from './keymap.routes';

View File

@@ -0,0 +1,26 @@
import { Routes } from '@angular/router';
import { KeymapAddComponent } from './add/keymap-add.component';
import { KeymapEditComponent } from './edit';
import { KeymapEditGuard } from './edit';
export const keymapRoutes: Routes = [
{
path: '',
redirectTo: 'keymap',
pathMatch: 'full'
},
{
path: 'keymap',
component: KeymapEditComponent,
canActivate: [KeymapEditGuard]
},
{
path: 'keymap/add',
component: KeymapAddComponent
},
{
path: 'keymap/:abbr',
component: KeymapEditComponent
}
];

View File

@@ -0,0 +1 @@
export * from './layers.component';

View File

@@ -0,0 +1,8 @@
<div class="text-center">
<span role="group" class="uhk__layer-switcher--wrapper btn-group btn-group-lg" data-title="Layers: ">
<button type="button" class="btn btn-default" *ngFor="let button of buttons; let index = index" (click)="selectLayer(index)"
[class.btn-primary]="index === current">
{{ button }}
</button>
</span>
</div>

View File

@@ -0,0 +1,36 @@
:host {
display: block;
&.disabled {
button {
cursor: no-drop;
background: rgba(#ccc, 0.43);
pointer-events: none;
&.btn-primary {
background: #7c7c7c;
border-color: #7c7c7c;
}
}
}
}
.uhk {
&__layer-switcher {
&--wrapper {
position: relative;
margin-bottom: 2rem;
&:before {
content: attr(data-title);
display: inline-block;
position: absolute;
bottom: 0.55em;
right: 100%;
font-size: 18px;
padding-right: 0.45em;
margin: 0;
}
}
}
}

View File

@@ -0,0 +1,31 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'layers',
templateUrl: './layers.component.html',
styleUrls: ['./layers.component.scss']
})
export class LayersComponent {
@Input() current: number;
@Output() select = new EventEmitter();
buttons: string[];
constructor() {
this.buttons = ['Base', 'Mod', 'Fn', 'Mouse'];
this.current = 0;
}
selectLayer(index: number) {
if (this.current === index) {
return;
}
this.select.emit({
oldIndex: this.current,
index: index
});
this.current = index;
}
}

View File

@@ -0,0 +1 @@
export { MacroActionEditorComponent } from './macro-action-editor.component';

View File

@@ -0,0 +1,46 @@
<div class="action--editor">
<div class="row">
<div class="col-lg-3 editor__tab-links">
<ul class="nav nav-pills nav-stacked">
<li #macroText [class.active]="activeTab === TabName.Text" (click)="selectTab(TabName.Text)">
<a>
<i class="fa fa-font"></i>
<span>Type text</span>
</a>
</li>
<li #macroKeypress [class.active]="activeTab === TabName.Keypress" (click)="selectTab(TabName.Keypress)">
<a>
<i class="fa fa-keyboard-o"></i>
<span>Key action</span>
</a>
</li>
<li #macroMouse [class.active]="activeTab === TabName.Mouse" (click)="selectTab(TabName.Mouse)">
<a>
<i class="fa fa-mouse-pointer"></i>
<span>Mouse action</span>
</a>
</li>
<li #macroDelay [class.active]="activeTab === TabName.Delay" (click)="selectTab(TabName.Delay)">
<a>
<i class="fa fa-clock-o"></i>
<span>Delay</span>
</a>
</li>
</ul>
</div>
<div class="col-xs-12 col-lg-9 editor__tabs" [ngSwitch]="activeTab">
<macro-text-tab #tab *ngSwitchCase="TabName.Text" [macroAction]="editableMacroAction"></macro-text-tab>
<macro-key-tab #tab *ngSwitchCase="TabName.Keypress" [macroAction]="editableMacroAction"></macro-key-tab>
<macro-mouse-tab #tab *ngSwitchCase="TabName.Mouse" [macroAction]="editableMacroAction"></macro-mouse-tab>
<macro-delay-tab #tab *ngSwitchCase="TabName.Delay" [macroAction]="editableMacroAction"></macro-delay-tab>
</div>
</div>
<div class="row">
<div class="col-xs-12 flex-button-wrapper editor__actions-container">
<div class="editor__actions">
<button class="btn btn-sm btn-default flex-button" type="button" (click)="onCancelClick()"> Cancel </button>
<button class="btn btn-sm btn-primary flex-button" type="button" (click)="onSaveClick()"> Save </button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,88 @@
@import '../../../../styles/variables';
:host {
display: block;
width: 100%;
}
.action--editor {
padding-top: 0;
padding-bottom: 0;
border-radius: 0;
border: 0;
}
.nav {
padding-bottom: 1rem;
li {
a {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
&.selected {
font-style: italic;
}
&:hover {
cursor: pointer;
}
}
&.active {
z-index: 2;
a {
&.selected {
font-style: normal;
}
&:after {
content: '';
display: block;
position: absolute;
width: 0;
height: 0;
top: 0;
right: -4rem;
border-color: transparent transparent transparent $icon-hover;
border-style: solid;
border-width: 2rem;
}
}
}
}
}
.editor {
&__tabs,
&__tab-links {
padding-top: 1rem;
}
&__tabs {
border-left: 1px solid #ddd;
margin-left: -1.6rem;
padding-left: 3rem;
}
&__actions {
float: right;
&-container {
background: #f5f5f5;
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
padding: 1rem 1.5rem;
}
}
}
.flex-button-wrapper {
display: flex;
flex-direction: row-reverse;
}
.flex-button {
align-self: flex-end;
}

View File

@@ -0,0 +1,98 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import {
MacroAction,
DelayMacroAction,
KeyMacroAction,
ScrollMouseMacroAction,
MoveMouseMacroAction,
MouseButtonMacroAction,
TextMacroAction,
Helper as MacroActionHelper
} from '../../../config-serializer/config-items/macro-action';
import { MacroDelayTabComponent, MacroMouseTabComponent, MacroKeyTabComponent, MacroTextTabComponent } from './tab';
enum TabName {
Keypress,
Text,
Mouse,
Delay
}
@Component({
selector: 'macro-action-editor',
templateUrl: './macro-action-editor.component.html',
styleUrls: ['./macro-action-editor.component.scss'],
host: { 'class': 'macro-action-editor' }
})
export class MacroActionEditorComponent implements OnInit {
@Input() macroAction: MacroAction;
@Output() save = new EventEmitter<MacroAction>();
@Output() cancel = new EventEmitter<void>();
@ViewChild('tab') selectedTab: MacroTextTabComponent | MacroKeyTabComponent | MacroMouseTabComponent | MacroDelayTabComponent;
editableMacroAction: MacroAction;
activeTab: TabName;
/* tslint:disable:variable-name: It is an enum type. So it can start with uppercase. */
TabName = TabName;
/* tslint:enable:variable-name */
ngOnInit() {
this.updateEditableMacroAction();
const tab: TabName = this.getTabName(this.editableMacroAction);
this.activeTab = tab;
}
ngOnChanges() {
this.ngOnInit();
}
onCancelClick(): void {
this.cancel.emit();
}
onSaveClick(): void {
try {
// TODO: Refactor after getKeyMacroAction has been added to all tabs
const action = this.selectedTab instanceof MacroKeyTabComponent ?
this.selectedTab.getKeyMacroAction() :
this.selectedTab.macroAction;
this.save.emit(action);
} catch (e) {
// TODO: show error dialog
console.error(e);
}
}
selectTab(tab: TabName): void {
this.activeTab = tab;
if (tab === this.getTabName(this.macroAction)) {
this.updateEditableMacroAction();
} else {
this.editableMacroAction = undefined;
}
}
getTabName(action: MacroAction): TabName {
if (action instanceof DelayMacroAction) {
return TabName.Delay;
} else if (action instanceof TextMacroAction) {
return TabName.Text;
} else if (action instanceof KeyMacroAction) {
return TabName.Keypress;
} else if (action instanceof MouseButtonMacroAction ||
action instanceof MoveMouseMacroAction ||
action instanceof ScrollMouseMacroAction) {
return TabName.Mouse;
}
return undefined;
}
private updateEditableMacroAction() {
const macroAction: MacroAction = this.macroAction ? this.macroAction : new TextMacroAction();
this.editableMacroAction = MacroActionHelper.createMacroAction(macroAction);
}
}

View File

@@ -0,0 +1 @@
export { MacroDelayTabComponent } from './macro-delay.component';

View File

@@ -0,0 +1,26 @@
<div class="macro-delay">
<div class="row">
<div class="col-xs-12">
<h4>Enter delay in seconds</h4>
</div>
</div>
<div class="row">
<div class="col-xs-4">
<input #macroDelayInput
type="number"
min="0"
max="1000"
step="0.1"
placeholder="Delay amount"
class="form-control"
[attr.value]="delay"
(change)="setDelay($event)">
</div>
</div>
<div class="row macro-delay__presets">
<div class="col-xs-12">
<h6>Choose a preset</h6>
<button *ngFor="let delay of presets" class="btn btn-sm btn-default" (click)="setDelay(delay)">{{delay}}s</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
:host {
display: flex;
flex-direction: column;
position: relative;
}
.macro-delay {
&__presets {
margin-top: 1rem;
button {
margin-right: 0.25rem;
margin-bottom: 0.25rem;
}
}
}

View File

@@ -0,0 +1,41 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
OnInit,
ViewChild
} from '@angular/core';
import { DelayMacroAction } from '../../../../../config-serializer/config-items/macro-action';
const INITIAL_DELAY = 0.5; // In seconds
@Component({
selector: 'macro-delay-tab',
templateUrl: './macro-delay.component.html',
styleUrls: ['./macro-delay.component.scss'],
host: { 'class': 'macro__delay' },
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MacroDelayTabComponent implements OnInit {
@Input() macroAction: DelayMacroAction;
@ViewChild('macroDelayInput') input: ElementRef;
delay: number;
presets: number[] = [0.3, 0.5, 0.8, 1, 2, 3, 4, 5];
constructor() { }
ngOnInit() {
if (!this.macroAction) {
this.macroAction = new DelayMacroAction();
}
this.delay = this.macroAction.delay > 0 ? this.macroAction.delay / 1000 : INITIAL_DELAY;
}
setDelay(value: number): void {
this.delay = value;
this.macroAction.delay = this.delay * 1000;
}
}

View File

@@ -0,0 +1,4 @@
export { MacroDelayTabComponent } from './delay';
export { MacroKeyTabComponent } from './key';
export { MacroMouseTabComponent } from './mouse';
export { MacroTextTabComponent } from './text';

View File

@@ -0,0 +1 @@
export { MacroKeyTabComponent } from './macro-key.component';

View File

@@ -0,0 +1,32 @@
<div class="col-xs-12 macro-key__container">
<div class="col-xs-3 macro-key__types">
<ul class="nav nav-pills nav-stacked">
<li #keyMove [class.active]="activeTab === TabName.Keypress" (click)="selectTab(TabName.Keypress)">
<a>
<i class="fa fa-hand-pointer-o"></i>
<span>Press key</span>
</a>
</li>
<li #keyHold [class.active]="activeTab === TabName.Hold" (click)="selectTab(TabName.Hold)">
<a>
<i class="fa fa-hand-rock-o"></i>
<span>Hold key</span>
</a>
</li>
<li #keyRelease [class.active]="activeTab === TabName.Release" (click)="selectTab(TabName.Release)">
<a>
<i class="fa fa-hand-paper-o"></i>
<span>Release key</span>
</a>
</li>
</ul>
</div>
<div class="col-xs-9 macro-key__action-container">
<div class="macro-key__action">
<h4 *ngIf="activeTab === TabName.Keypress">Press key</h4>
<h4 *ngIf="activeTab === TabName.Hold">Hold key</h4>
<h4 *ngIf="activeTab === TabName.Release">Release key</h4>
<keypress-tab #keypressTab [defaultKeyAction]="defaultKeyAction" [longPressEnabled]="false"></keypress-tab>
</div>
</div>
</div>

View File

@@ -0,0 +1,25 @@
.macro-key {
&__container {
padding: 0;
}
&__types {
margin-left: 0;
padding: 0 0 1rem;
}
&__action {
&-container {
margin-top: -1rem;
padding-top: 1rem;
border-left: 1px solid #ddd;
}
padding-left: 3rem;
padding-bottom: 1rem;
}
}
.fa {
min-width: 14px;
}

View File

@@ -0,0 +1,74 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { KeystrokeAction } from '../../../../../config-serializer/config-items/key-action';
import { KeyMacroAction, MacroSubAction } from '../../../../../config-serializer/config-items/macro-action';
import { KeypressTabComponent, Tab } from '../../../../popover/tab';
enum TabName {
Keypress,
Hold,
Release
}
@Component({
selector: 'macro-key-tab',
templateUrl: './macro-key.component.html',
styleUrls: [
'../../macro-action-editor.component.scss',
'./macro-key.component.scss'
],
host: { 'class': 'macro__mouse' }
})
export class MacroKeyTabComponent implements OnInit {
@Input() macroAction: KeyMacroAction;
@ViewChild('tab') selectedTab: Tab;
@ViewChild('keypressTab') keypressTab: KeypressTabComponent;
/* tslint:disable:variable-name: It is an enum type. So it can start with uppercase. */
TabName = TabName;
/* tslint:enable:variable-name */
activeTab: TabName;
defaultKeyAction: KeystrokeAction;
ngOnInit() {
if (!this.macroAction) {
this.macroAction = new KeyMacroAction();
}
this.defaultKeyAction = new KeystrokeAction(<any>this.macroAction);
this.selectTab(this.getTabName(this.macroAction));
}
selectTab(tab: TabName): void {
this.activeTab = tab;
}
getTabName(macroAction: KeyMacroAction): TabName {
if (!macroAction.action) {
return TabName.Keypress;
} else if (macroAction.action === MacroSubAction.hold) {
return TabName.Hold;
} else if (macroAction.action === MacroSubAction.release) {
return TabName.Release;
}
}
getActionType(tab: TabName): MacroSubAction {
switch (tab) {
case TabName.Keypress:
return MacroSubAction.press;
case TabName.Hold:
return MacroSubAction.hold;
case TabName.Release:
return MacroSubAction.release;
default:
throw new Error('Invalid tab type');
}
}
getKeyMacroAction(): KeyMacroAction {
const keyMacroAction = Object.assign(new KeyMacroAction(), this.keypressTab.toKeyAction());
keyMacroAction.action = this.getActionType(this.activeTab);
return keyMacroAction;
}
}

View File

@@ -0,0 +1 @@
export { MacroMouseTabComponent } from './macro-mouse.component';

View File

@@ -0,0 +1,77 @@
<div class="col-xs-12 macro-mouse__container">
<div class="col-xs-3 macro-mouse__types">
<ul class="nav nav-pills nav-stacked">
<li #mouseMove [class.active]="activeTab === TabName.Move" (click)="selectTab(TabName.Move)">
<a>
<i class="fa fa-arrows"></i>
<span>Move pointer</span>
</a>
</li>
<li #mouseScroll [class.active]="activeTab === TabName.Scroll" (click)="selectTab(TabName.Scroll)">
<a>
<i class="fa fa-arrows-v"></i>
<span>Scroll</span>
</a>
</li>
<li #mouseClick [class.active]="activeTab === TabName.Click" (click)="selectTab(TabName.Click)">
<a>
<i class="fa fa-mouse-pointer"></i>
<span>Click button</span>
</a>
</li>
<li #mouseHold [class.active]="activeTab === TabName.Hold" (click)="selectTab(TabName.Hold)">
<a>
<i class="fa fa-hand-rock-o"></i>
<span>Hold button</span>
</a>
</li>
<li #mouseRelease [class.active]="activeTab === TabName.Release" (click)="selectTab(TabName.Release)">
<a>
<i class="fa fa-hand-paper-o"></i>
<span>Release button</span>
</a>
</li>
</ul>
</div>
<div class="col-xs-9 macro-mouse__actions" [ngSwitch]="activeTab">
<div #tab *ngSwitchCase="TabName.Move">
<h4>Move pointer</h4>
<p>Use negative values to move down or left from current position.</p>
<div class="form-horizontal">
<div class="form-group">
<label for="move-mouse-x">X</label>
<input id="move-mouse-x" type="number" class="form-control" [(ngModel)]="macroAction['x']"> pixels
</div>
<div class="form-group">
<label for="move-mouse-y">Y</label>
<input id="move-mouse-y" type="number" class="form-control" [(ngModel)]="macroAction['y']"> pixels
</div>
</div>
</div>
<div #tab *ngSwitchCase="TabName.Scroll">
<h4>Scroll</h4>
<p>Use negative values to move down or left from current position.</p>
<div class="form-horizontal">
<div class="form-group">
<label for="scroll-mouse-x">X</label>
<input id="scroll-mouse-x" type="number" class="form-control" [(ngModel)]="macroAction['x']"> pixels
</div>
<div class="form-group">
<label for="scroll-mouse-y">Y</label>
<input id="scroll-mouse-y" type="number" class="form-control" [(ngModel)]="macroAction['y']"> pixels
</div>
</div>
</div>
<div #tab *ngIf="activeTab === TabName.Click || activeTab === TabName.Hold || activeTab === TabName.Release">
<h4 *ngIf="activeTab === TabName.Click">Click mouse button</h4>
<h4 *ngIf="activeTab === TabName.Hold">Hold mouse button</h4>
<h4 *ngIf="activeTab === TabName.Release">Release mouse button</h4>
<div class="btn-group macro-mouse__buttons">
<button *ngFor="let buttonLabel of buttonLabels; let buttonIndex = index"
class="btn btn-default"
[class.btn-primary]="hasButton(buttonIndex)"
(click)="setMouseClick(buttonIndex)">{{buttonLabel}}</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,43 @@
.macro-mouse {
&__container {
padding: 0;
}
&__types {
border-right: 1px solid #ddd;
border-left: 0;
margin-top: -1rem;
margin-left: 0;
padding: 1rem 0;
}
&__actions {
padding-left: 3rem;
padding-bottom: 1rem;
}
&__buttons {
margin-top: 3rem;
margin-bottom: 1rem;
}
}
.fa {
min-width: 14px;
}
.form-horizontal {
.form-group {
margin: 0 0 0.5rem;
}
label {
display: inline-block;
margin-right: 0.5rem;
}
.form-control {
display: inline-block;
width: 60%;
}
}

View File

@@ -0,0 +1,123 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import {
MouseButtonMacroAction,
MoveMouseMacroAction,
ScrollMouseMacroAction,
MacroSubAction
} from '../../../../../config-serializer/config-items/macro-action';
import { Tab } from '../../../../popover/tab';
type MouseMacroAction = MouseButtonMacroAction | MoveMouseMacroAction | ScrollMouseMacroAction;
enum TabName {
Move,
Scroll,
Click,
Hold,
Release
}
@Component({
selector: 'macro-mouse-tab',
templateUrl: './macro-mouse.component.html',
styleUrls: [
'../../macro-action-editor.component.scss',
'./macro-mouse.component.scss'
],
host: { 'class': 'macro__mouse' }
})
export class MacroMouseTabComponent implements OnInit {
@Input() macroAction: MouseMacroAction;
@ViewChild('tab') selectedTab: Tab;
/* tslint:disable:variable-name: It is an enum type. So it can start with uppercase. */
TabName = TabName;
/* tslint:enable:variable-name */
activeTab: TabName;
buttonLabels: string[];
private selectedButtons: boolean[];
constructor() {
this.buttonLabels = ['Left', 'Middle', 'Right'];
this.selectedButtons = Array(this.buttonLabels.length).fill(false);
}
ngOnInit() {
if (!this.macroAction) {
this.macroAction = new MouseButtonMacroAction();
this.macroAction.action = MacroSubAction.press;
}
const tabName = this.getTabName(this.macroAction);
this.selectTab(tabName);
const buttonActions = [TabName.Click, TabName.Hold, TabName.Release];
if (buttonActions.includes(this.activeTab)) {
this.selectedButtons = (<MouseButtonMacroAction>this.macroAction).getMouseButtons();
}
}
ngOnChanges() {
this.ngOnInit();
}
selectTab(tab: TabName): void {
this.activeTab = tab;
if (tab === this.getTabName(this.macroAction)) {
return;
}
switch (tab) {
case TabName.Scroll:
this.macroAction = new ScrollMouseMacroAction();
break;
case TabName.Move:
this.macroAction = new MoveMouseMacroAction();
break;
default:
this.macroAction = new MouseButtonMacroAction();
this.macroAction.action = this.getAction(tab);
break;
}
}
setMouseClick(index: number): void {
this.selectedButtons[index] = !this.selectedButtons[index];
(<MouseButtonMacroAction>this.macroAction).setMouseButtons(this.selectedButtons);
}
hasButton(index: number): boolean {
return this.selectedButtons[index];
}
getAction(tab: TabName): MacroSubAction {
switch (tab) {
case TabName.Click:
return MacroSubAction.press;
case TabName.Hold:
return MacroSubAction.hold;
case TabName.Release:
return MacroSubAction.release;
default:
throw new Error(`Invalid tab name: ${TabName[tab]}`);
}
}
getTabName(action: MouseMacroAction): TabName {
if (action instanceof MouseButtonMacroAction) {
if (!action.action || action.isOnlyPressAction()) {
return TabName.Click;
} else if (action.isOnlyHoldAction()) {
return TabName.Hold;
} else if (action.isOnlyReleaseAction()) {
return TabName.Release;
}
} else if (action instanceof MoveMouseMacroAction) {
return TabName.Move;
} else if (action instanceof ScrollMouseMacroAction) {
return TabName.Scroll;
}
return TabName.Move;
}
}

View File

@@ -0,0 +1 @@
export { MacroTextTabComponent } from './macro-text.component';

View File

@@ -0,0 +1,5 @@
<div>
<h4>Type text</h4>
<p>Input the text you want to type with this macro action.</p>
<textarea #macroTextInput name="macro-text" (change)="onTextChange()" class="macro__text-input">{{ macroAction.text }}</textarea>
</div>

View File

@@ -0,0 +1,11 @@
:host {
display: flex;
flex-direction: column;
position: relative;
}
.macro__text-input {
width: 100%;
min-height: 10rem;
margin-bottom: 1rem;
}

View File

@@ -0,0 +1,39 @@
import {
OnInit,
AfterViewInit,
Component,
ElementRef,
Input,
Renderer,
ViewChild
} from '@angular/core';
import { TextMacroAction } from '../../../../../config-serializer/config-items/macro-action';
@Component({
selector: 'macro-text-tab',
templateUrl: './macro-text.component.html',
styleUrls: ['./macro-text.component.scss'],
host: { 'class': 'macro__text' }
})
export class MacroTextTabComponent implements OnInit, AfterViewInit {
@Input() macroAction: TextMacroAction;
@ViewChild('macroTextInput') input: ElementRef;
constructor(private renderer: Renderer) {}
ngOnInit() {
if (!this.macroAction) {
this.macroAction = new TextMacroAction();
}
}
ngAfterViewInit() {
this.renderer.invokeElementMethod(this.input.nativeElement, 'focus');
}
onTextChange() {
this.macroAction.text = this.input.nativeElement.value;
}
}

View File

@@ -0,0 +1,17 @@
<ng-template [ngIf]="macro">
<macro-header
[macro]="macro"
[isNew]="isNew"
></macro-header>
<macro-list
[macro]="macro"
(add)="addAction($event.macroId, $event.action)"
(edit)="editAction($event.macroId, $event.index, $event.action)"
(delete)="deleteAction($event.macroId, $event.index, $event.action)"
(reorder)="reorderAction($event.macroId, $event.oldIndex, $event.newIndex)"
></macro-list>
</ng-template>
<div *ngIf="!macro" class="not-found">
There is no macro with id {{ route.params.select('id') | async }}.
</div>

View File

@@ -0,0 +1,11 @@
:host {
width: 100%;
height: 100%;
display: block;
}
.not-found {
margin-top: 30px;
font-size: 16px;
text-align: center;
}

View File

@@ -0,0 +1,60 @@
import { Component, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Store } from '@ngrx/store';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/pluck';
import { Macro } from '../../../config-serializer/config-items/macro';
import { MacroAction } from '../../../config-serializer/config-items/macro-action/macro-action';
import { MacroActions } from '../../../store/actions';
import { AppState } from '../../../store/index';
import { getMacro } from '../../../store/reducers/user-configuration';
@Component({
selector: 'macro-edit',
templateUrl: './macro-edit.component.html',
styleUrls: ['./macro-edit.component.scss'],
host: {
'class': 'container-fluid'
}
})
export class MacroEditComponent implements OnDestroy {
macro: Macro;
isNew: boolean;
private subscription: Subscription;
constructor(private store: Store<AppState>, public route: ActivatedRoute) {
this.subscription = route
.params
.pluck<{}, string>('id')
.switchMap((id: string) => store.let(getMacro(+id)))
.subscribe((macro: Macro) => {
this.macro = macro;
});
this.isNew = this.route.snapshot.params['empty'] === 'new';
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
addAction(macroId: number, action: MacroAction) {
this.store.dispatch(MacroActions.addMacroAction(macroId, action));
}
editAction(macroId: number, index: number, action: MacroAction) {
this.store.dispatch(MacroActions.saveMacroAction(macroId, index, action));
}
deleteAction(macroId: number, index: number, action: MacroAction) {
this.store.dispatch(MacroActions.deleteMacroAction(macroId, index, action));
}
reorderAction(macroId: number, oldIndex: number, newIndex: number) {
this.store.dispatch(MacroActions.reorderMacroAction(macroId, oldIndex, newIndex));
}
}

View File

@@ -0,0 +1,26 @@
<uhk-header>
<div class="row">
<h1 class="col-xs-12 pane-title">
<i class="fa fa-play"></i>
<input #macroName cancelable
class="pane-title__name"
type="text"
(change)="editMacroName($event.target.value)"
(keyup.enter)="macroName.blur()"
/>
<i class="glyphicon glyphicon-trash macro__remove pull-right" title=""
data-toggle="tooltip"
data-placement="bottom"
html="true"
data-original-title="<span class='text-nowrap'>Delete macro</span>"
(click)="removeMacro()"
></i>
<i class="fa fa-files-o macro__duplicate pull-right" title=""
data-toggle="tooltip"
data-placement="bottom"
data-original-title="Duplicate macro"
(click)="duplicateMacro()"
></i>
</h1>
</div>
</uhk-header>

View File

@@ -0,0 +1,43 @@
@import '../../../../styles/variables';
.macro {
&__remove {
font-size: 0.75em;
top: 8px;
&:hover {
cursor: pointer;
color: $icon-hover-delete;
}
}
&__duplicate {
font-size: 0.75em;
top: 7px;
margin-right: 15px;
position: relative;
&:hover {
cursor: pointer;
color: $icon-hover;
}
}
}
.pane-title {
margin-bottom: 1em;
&__name {
border: none;
border-bottom: 2px dotted #999;
padding: 0;
margin: 0 0.25rem;
width: 330px;
text-overflow: ellipsis;
&:focus {
box-shadow: 0 0 0 1px #ccc, 0 0 5px 0 #ccc;
border-color: transparent;
}
}
}

View File

@@ -0,0 +1,73 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
OnChanges,
Renderer2,
SimpleChanges,
ViewChild
} from '@angular/core';
import { Store } from '@ngrx/store';
import { Macro } from '../../../config-serializer/config-items/macro';
import { MacroActions } from '../../../store/actions';
import { AppState } from '../../../store/index';
@Component({
selector: 'macro-header',
templateUrl: './macro-header.component.html',
styleUrls: ['./macro-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MacroHeaderComponent implements AfterViewInit, OnChanges {
@Input() macro: Macro;
@Input() isNew: boolean;
@ViewChild('macroName') macroName: ElementRef;
constructor(private store: Store<AppState>, private renderer: Renderer2) { }
ngOnChanges(changes: SimpleChanges) {
if (this.isNew) {
this.setFocusOnName();
}
if (changes['macro']) {
this.setName();
}
}
ngAfterViewInit() {
if (this.isNew) {
this.setFocusOnName();
}
}
removeMacro() {
this.store.dispatch(MacroActions.removeMacro(this.macro.id));
}
duplicateMacro() {
this.store.dispatch(MacroActions.duplicateMacro(this.macro));
}
editMacroName(name: string) {
if (name.length === 0) {
this.setName();
return;
}
this.store.dispatch(MacroActions.editMacroName(this.macro.id, name));
}
private setFocusOnName() {
this.macroName.nativeElement.select();
}
private setName(): void {
this.renderer.setProperty(this.macroName.nativeElement, 'value', this.macro.name);
}
}

View File

@@ -0,0 +1,8 @@
export * from './edit/macro-edit.component';
export * from './list/macro-list.component';
export * from './header/macro-header.component';
export * from './macro.routes';
export * from './not-found';
export * from './item';
export * from './action-editor';
export * from './action-editor/tab';

View File

@@ -0,0 +1 @@
export { MacroItemComponent } from './macro-item.component';

View File

@@ -0,0 +1,17 @@
<div class="list-group-item action--item" [class.is-editing]="editing">
<span *ngIf="movable" class="glyphicon glyphicon-option-vertical action--movable" aria-hidden="true"></span>
<div class="action--item--wrap" [class.pointer]="editable" (click)="editAction()">
<icon [name]="iconName"></icon>
<div class="action--title">{{ title }}</div>
<icon *ngIf="editable && macroAction && !editing" name="pencil"></icon>
</div>
<icon *ngIf="deletable" name="trash" (click)="deleteAction()"></icon>
</div>
<div class="list-group-item macro-action-editor__container"
[@toggler]="((editable && editing) || newItem) ? 'active' : 'inactive'">
<macro-action-editor
[macroAction]="macroAction"
(cancel)="cancelEdit()"
(save)="saveEditedAction($event)">
</macro-action-editor>
</div>

View File

@@ -0,0 +1,86 @@
@import '../../../../styles/variables';
:host {
overflow: hidden;
display: block;
&.macro-item:first-of-type {
.list-group-item {
border-radius: 4px 4px 0 0;
}
}
&.macro-item:last-of-type {
.list-group-item {
border-bottom: 0;
}
}
&.gu-transit {
opacity: 0.2;
.list-group-item {
background: #f5f5f5;
}
}
}
.action {
&--item {
display: flex;
flex-shrink: 0;
border: 0;
border-bottom: 1px solid #ddd;
icon {
margin: 0 5px;
}
> div {
display: flex;
flex: 1;
}
&:first-child {
border-radius: 0;
}
&.is-editing {
background: #f5f5f5;
}
&--wrap {
justify-content: space-between;
&.pointer {
&:hover {
cursor: pointer;
color: $icon-hover;
}
}
}
}
&--title {
display: flex;
flex: 1;
}
&--movable {
&:hover {
cursor: move;
}
}
}
.list-group-item {
margin-bottom: 0;
}
.macro-action-editor__container {
padding-top: 0;
padding-bottom: 0;
border-radius: 0;
border: none;
overflow: hidden;
}

View File

@@ -0,0 +1,206 @@
import { Component, Input, Output, EventEmitter, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { KeyModifiers } from '../../../config-serializer/config-items/key-modifiers';
import {
DelayMacroAction,
KeyMacroAction,
MacroAction,
MouseButtonMacroAction,
MoveMouseMacroAction,
ScrollMouseMacroAction,
TextMacroAction
} from '../../../config-serializer/config-items/macro-action';
import { MapperService } from '../../../services/mapper.service';
@Component({
animations: [
trigger('toggler', [
state('inactive', style({
height: '0px'
})),
state('active', style({
height: '*'
})),
transition('inactive <=> active', animate('500ms ease-out'))
])
],
selector: 'macro-item',
templateUrl: './macro-item.component.html',
styleUrls: ['./macro-item.component.scss'],
host: { 'class': 'macro-item' }
})
export class MacroItemComponent implements OnInit, OnChanges {
@Input() macroAction: MacroAction;
@Input() editable: boolean;
@Input() deletable: boolean;
@Input() movable: boolean;
@Output() save = new EventEmitter<MacroAction>();
@Output() cancel = new EventEmitter<void>();
@Output() edit = new EventEmitter<void>();
@Output() delete = new EventEmitter<void>();
title: string;
iconName: string;
editing: boolean;
newItem: boolean = false;
constructor(private mapper: MapperService) { }
ngOnInit() {
this.updateView();
if (!this.macroAction) {
this.editing = true;
this.newItem = true;
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes['macroAction']) {
this.updateView();
}
}
saveEditedAction(editedAction: MacroAction): void {
this.macroAction = editedAction;
this.editing = false;
this.updateView();
this.save.emit(editedAction);
}
editAction(): void {
if (!this.editable || this.editing) {
this.cancelEdit();
return;
}
this.editing = true;
this.edit.emit();
}
cancelEdit(): void {
this.editing = false;
this.cancel.emit();
}
deleteAction(): void {
this.delete.emit();
}
private updateView(): void {
if (!this.macroAction) {
this.title = 'New macro action';
} else if (this.macroAction instanceof DelayMacroAction) {
// Delay
this.iconName = 'clock';
const action: DelayMacroAction = this.macroAction as DelayMacroAction;
const delay = action.delay > 0 ? action.delay / 1000 : 0;
this.title = `Delay of ${delay}s`;
} else if (this.macroAction instanceof TextMacroAction) {
// Write text
const action: TextMacroAction = this.macroAction as TextMacroAction;
this.iconName = 'font';
this.title = `Write text: ${action.text}`;
} else if (this.macroAction instanceof KeyMacroAction) {
// Key pressed/held/released
const action: KeyMacroAction = this.macroAction as KeyMacroAction;
this.setKeyActionContent(action);
} else if (this.macroAction instanceof MouseButtonMacroAction) {
// Mouse button clicked/held/released
const action: MouseButtonMacroAction = this.macroAction as MouseButtonMacroAction;
this.setMouseButtonActionContent(action);
} else if (this.macroAction instanceof MoveMouseMacroAction || this.macroAction instanceof ScrollMouseMacroAction) {
// Mouse moved or scrolled
this.setMouseMoveScrollActionContent(this.macroAction);
} else {
this.title = this.macroAction.constructor.name;
}
}
private setKeyActionContent(action: KeyMacroAction): void {
if (!action.hasScancode() && !action.hasModifiers()) {
this.title = 'Invalid keypress';
return;
}
if (action.isPressAction()) {
// Press key
this.iconName = 'hand-pointer';
this.title = 'Press key: ';
} else if (action.isHoldAction()) {
// Hold key
this.iconName = 'hand-rock';
this.title = 'Hold key: ';
} else if (action.isReleaseAction()) {
// Release key
this.iconName = 'hand-paper';
this.title = 'Release key: ';
}
if (action.hasScancode()) {
const scancode: string = (this.mapper.scanCodeToText(action.scancode, action.type) || ['Unknown']).join(' ');
if (scancode) {
this.title += scancode;
}
}
if (action.hasModifiers()) {
// Press/hold/release modifiers
for (let i = KeyModifiers.leftCtrl; i <= KeyModifiers.rightGui; i <<= 1) {
if (action.isModifierActive(i)) {
this.title += ' ' + KeyModifiers[i];
}
}
}
}
private setMouseMoveScrollActionContent(action: MacroAction): void {
let typedAction: any;
if (action instanceof MoveMouseMacroAction) {
// Move mouse pointer
this.iconName = 'mouse-pointer';
this.title = 'Move pointer';
typedAction = this.macroAction as MoveMouseMacroAction;
} else {
// Scroll mouse
this.iconName = 'mouse-pointer';
this.title = 'Scroll';
typedAction = this.macroAction as ScrollMouseMacroAction;
}
let needAnd: boolean;
if (Math.abs(typedAction.x) !== 0) {
this.title += ` by ${Math.abs(typedAction.x)}px ${typedAction.x > 0 ? 'left' : 'right'}`;
needAnd = true;
}
if (Math.abs(typedAction.y) !== 0) {
this.title += ` ${needAnd ? 'and' : 'by'} ${Math.abs(typedAction.y)}px ${typedAction.y > 0 ? 'down' : 'up'}`;
}
}
private setMouseButtonActionContent(action: MouseButtonMacroAction): void {
// Press/hold/release mouse buttons
if (action.isOnlyPressAction()) {
this.iconName = 'mouse-pointer';
this.title = 'Click mouse button: ';
} else if (action.isOnlyHoldAction()) {
this.iconName = 'hand-rock';
this.title = 'Hold mouse button: ';
} else if (action.isOnlyReleaseAction()) {
this.iconName = 'hand-paper';
this.title = 'Release mouse button: ';
}
const buttonLabels: string[] = ['Left', 'Middle', 'Right'];
const selectedButtons: boolean[] = action.getMouseButtons();
const selectedButtonLabels: string[] = [];
selectedButtons.forEach((isSelected, idx) => {
if (isSelected && buttonLabels[idx]) {
selectedButtonLabels.push(buttonLabels[idx]);
}
});
this.title += selectedButtonLabels.join(', ');
}
}

View File

@@ -0,0 +1,37 @@
<div class="row list-container">
<div class="col-xs-10 col-xs-offset-1 list-group">
<div class="macro-actions-container" [dragula]="'macroActions'" [dragulaModel]="macro.macroActions">
<macro-item *ngFor="let macroAction of macro.macroActions; let macroActionIndex = index"
[macroAction]="macroAction"
[editable]="true"
[deletable]="true"
[movable]="true"
(save)="saveAction($event, macroActionIndex)"
(edit)="editAction(macroActionIndex)"
(cancel)="cancelAction()"
(delete)="deleteAction(macroAction, macroActionIndex)"
[attr.data-index]="macroActionIndex"
></macro-item>
<macro-item *ngIf="showNew"
[@togglerNew]="showNew ? 'active' : 'inactive'"
[macroAction]="newMacro"
[editable]="true"
[deletable]="false"
[movable]="false"
(save)="addNewAction($event)"
(cancel)="hideNewAction()"
></macro-item>
</div>
<div class="list-group add-new__action-container" [@toggler]="(!showNew) ? 'active' : 'inactive'">
<div class="list-group-item action--item add-new__action-item no-reorder clearfix">
<a class="add-new__action-item--link" (click)="showNewAction()">
<i class="fa fa-plus"></i> Add macro action
</a>
<a class="add-new__action-item--link">
<i class="fa fa fa-circle"></i> Add captured keystroke
</a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,153 @@
@import '../../../../styles/variables';
:host {
display: flex;
flex-direction: column;
height: 100%;
.list-container {
display: flex;
flex: 1;
}
}
.main-wrapper {
width: 500px;
}
h1 {
margin-bottom: 3rem;
}
.action {
&--edit__form {
background-color: #fff;
margin-left: -0.5rem;
margin-right: -15px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #ddd;
}
&--item {
padding-left: 8px;
&.active,
&.active:hover {
background-color: white;
font-weight: bold;
color: black;
border-color: black;
z-index: 10;
}
}
}
.list-group {
overflow: auto;
}
.macro__name {
border-bottom: 2px dotted #999;
padding: 0 0.5rem;
margin: 0 0.25rem;
}
.macro-settings {
border: 1px solid black;
border-top-color: #999;
z-index: 100;
.helper {
position: absolute;
display: block;
height: 13px;
background: #fff;
width: 100%;
left: 0;
top: -14px;
}
}
.action--item.active.callout,
.macro-settings.callout {
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.5);
}
.macro-actions-container {
margin-bottom: 0;
border-radius: 4px;
border: 1px solid #ddd;
border-bottom: 0;
}
.list-group-item .move-handle:hover {
cursor: move;
}
.flex-button-wrapper {
display: flex;
flex-direction: row-reverse;
}
.flex-button {
align-self: flex-end;
}
.add-new__action-container {
overflow: hidden;
flex-shrink: 0;
border-top: 1px solid #ddd;
}
.add-new__action-item {
border-radius: 0 0 4px 4px;
border-top: 0;
padding: 0;
&:hover {
cursor: pointer;
}
&--link {
width: 50%;
float: left;
padding: 10px 5px;
text-align: center;
color: $icon-hover;
&:first-of-type {
border-right: 1px solid #ddd;
}
&:hover {
text-decoration: none;
background: #e6e6e6;
}
}
.fa-circle {
color: #c00;
}
}
// Dragula styles
.gu {
&-mirror {
position: fixed;
margin: 0;
z-index: 9999;
opacity: 0.8;
}
&-hide {
display: none;
}
&-unselectable {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
}

View File

@@ -0,0 +1,131 @@
import { Component, EventEmitter, Input, Output, QueryList, ViewChildren, forwardRef } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { DragulaService } from 'ng2-dragula/ng2-dragula';
import { Macro } from '../../../config-serializer/config-items/macro';
import { MacroAction } from '../../../config-serializer/config-items/macro-action';
import { MacroItemComponent } from '../item';
@Component({
animations: [
trigger('toggler', [
state('inactive', style({
height: '0px'
})),
state('active', style({
height: '*'
})),
transition('inactive <=> active', animate('500ms ease-out'))
]),
trigger('togglerNew', [
state('void', style({
height: '0px'
})),
state('active', style({
height: '*'
})),
transition(':enter', animate('500ms ease-out')),
transition(':leave', animate('500ms ease-out'))
])
],
selector: 'macro-list',
templateUrl: './macro-list.component.html',
styleUrls: ['./macro-list.component.scss'],
viewProviders: [DragulaService]
})
export class MacroListComponent {
@Input() macro: Macro;
@ViewChildren(forwardRef(() => MacroItemComponent)) macroItems: QueryList<MacroItemComponent>;
@Output() add = new EventEmitter();
@Output() edit = new EventEmitter();
@Output() delete = new EventEmitter();
@Output() reorder = new EventEmitter();
newMacro: Macro = undefined;
showNew: boolean = false;
private activeEdit: number = undefined;
private dragIndex: number;
constructor(dragulaService: DragulaService) {
/* tslint:disable:no-unused-variable: Used by Dragula. */
dragulaService.setOptions('macroActions', {
moves: function (el: any, container: any, handle: any) {
return handle.className.includes('action--movable');
}
});
dragulaService.drag.subscribe((value: any) => {
this.dragIndex = +value[1].getAttribute('data-index');
});
dragulaService.drop.subscribe((value: any) => {
if (value[4]) {
this.reorder.emit({
macroId: this.macro.id,
oldIndex: this.dragIndex,
newIndex: +value[4].getAttribute('data-index')
});
}
});
}
showNewAction() {
this.hideActiveEditor();
this.newMacro = undefined;
this.showNew = true;
}
hideNewAction() {
this.showNew = false;
}
addNewAction(macroAction: MacroAction) {
this.add.emit({
macroId: this.macro.id,
action: macroAction
});
this.newMacro = undefined;
this.showNew = false;
}
editAction(index: number) {
// Hide other editors when clicking edit button of a macro action
this.hideActiveEditor();
this.showNew = false;
this.activeEdit = index;
}
cancelAction() {
this.activeEdit = undefined;
}
saveAction(macroAction: MacroAction, index: number) {
this.edit.emit({
macroId: this.macro.id,
index: index,
action: macroAction
});
this.hideActiveEditor();
}
deleteAction(macroAction: MacroAction, index: number) {
this.delete.emit({
macroId: this.macro.id,
index: index,
action: macroAction
});
this.hideActiveEditor();
}
private hideActiveEditor() {
if (this.activeEdit !== undefined) {
this.macroItems.toArray()[this.activeEdit].cancelEdit();
}
}
}

View File

@@ -0,0 +1,20 @@
import { Routes } from '@angular/router';
import { MacroEditComponent } from './edit/macro-edit.component';
import { MacroNotFoundComponent, MacroNotFoundGuard } from './not-found';
export const macroRoutes: Routes = [
{
path: 'macro',
component: MacroNotFoundComponent,
canActivate: [MacroNotFoundGuard]
},
{
path: 'macro/:id',
component: MacroEditComponent
},
{
path: 'macro/:id/:empty',
component: MacroEditComponent
}
];

View File

@@ -0,0 +1,2 @@
export { MacroNotFoundComponent } from './macro-not-found.component';
export { MacroNotFoundGuard } from './macro-not-found-guard.service';

View File

@@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/let';
import 'rxjs/add/operator/map';
import { Store } from '@ngrx/store';
import { AppState } from '../../../store/index';
import { getMacros } from '../../../store/reducers/user-configuration';
import { Macro } from '../../../config-serializer/config-items/macro';
@Injectable()
export class MacroNotFoundGuard implements CanActivate {
constructor(private store: Store<AppState>, private router: Router) { }
canActivate(): Observable<boolean> {
return this.store
.let(getMacros())
.map((macros: Macro[]) => {
const hasMacros = macros.length > 0;
if (hasMacros) {
this.router.navigate(['/macro', macros[0].id]);
}
return !hasMacros;
});
}
}

View File

@@ -0,0 +1,8 @@
<div class="container-fluid">
<uhk-header>
<h1>&nbsp;</h1>
</uhk-header>
<div class="not-found">
You don't have any macros. Try to add one!
</div>
</div>

View File

@@ -0,0 +1,5 @@
.not-found {
margin-top: 30px;
font-size: 16px;
text-align: center;
}

View File

@@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
selector: 'macro-not-found',
templateUrl: './macro-not-found.component.html',
styleUrls: ['./macro-not-found.component.scss']
})
export class MacroNotFoundComponent { }

View File

@@ -0,0 +1 @@
export * from './missing-device.component';

View File

@@ -0,0 +1 @@
<uhk-message title="Cannot find your UHK" subtitle="Please plug it in!"></uhk-message>

View File

@@ -0,0 +1,14 @@
import { Component } from '@angular/core';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/ignoreElements';
import 'rxjs/add/operator/takeWhile';
@Component({
selector: 'missing-device',
templateUrl: './missing-device.component.html'
})
export class MissingDeviceComponent {
constructor() {}
}

View File

@@ -0,0 +1 @@
export * from './notification.component';

View File

@@ -0,0 +1,3 @@
<span class="text">Keymap removed</span>
<a href="#" class="action action--undo">Undo</a>
<span class="dismiss">&times;</span>

View File

@@ -0,0 +1,38 @@
:host {
padding: 1rem 1.5rem;
box-shadow: 0 0 0 1px #000;
border-radius: 0.5rem;
position: absolute;
top: 2rem;
right: 2rem;
z-index: 10000;
background-color: #333;
color: #eee;
display: none;
}
.action {
margin-left: 1rem;
margin-right: 1rem;
color: #5bc0de;
text-transform: uppercase;
font-weight: bold;
&:focus,
&:active,
&:hover {
text-decoration: none;
color: #5bc0de;
}
}
.dismiss {
position: relative;
bottom: 1px;
color: #ccc;
&:hover {
cursor: pointer;
color: #fff;
}
}

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'notification',
templateUrl: './notification.component.html',
styleUrls: ['./notification.component.scss']
})
export class NotificationComponent {
constructor() {
}
}

View File

@@ -0,0 +1 @@
export * from './popover.component';

View File

@@ -0,0 +1,82 @@
<div class="popover"
#popover
[@popover]="animationState"
[ngClass]="{'leftArrow': leftArrow, 'rightArrow': rightArrow}"
[style.top.px]="topPosition"
[style.left.px]="leftPosition"
>
<div class="arrowCustom"></div>
<div class="popover-title menu-tabs">
<ul class="nav nav-tabs popover-menu">
<li #keypress [class.active]="activeTab === tabName.Keypress" (click)="selectTab(tabName.Keypress)">
<a class="menu-tabs--item">
<i class="fa fa-keyboard-o"></i>
<span>Keypress</span>
</a>
</li>
<li #layer [class.active]="activeTab === tabName.Layer" (click)="selectTab(tabName.Layer)">
<a class="menu-tabs--item">
<i class="fa fa-clone"></i>
<span>Layer</span>
</a>
</li>
<li #mouse [class.active]="activeTab === tabName.Mouse" (click)="selectTab(tabName.Mouse)">
<a class="menu-tabs--item">
<i class="fa fa-mouse-pointer"></i>
<span>Mouse</span>
</a>
</li>
<li #macro [class.active]="activeTab === tabName.Macro" (click)="selectTab(tabName.Macro)">
<a class="menu-tabs--item">
<i class="fa fa-play"></i>
<span>Macro</span>
</a>
</li>
<li #keymap [class.active]="activeTab === tabName.Keymap" (click)="selectTab(tabName.Keymap)">
<a class="menu-tabs--item">
<i class="fa fa-keyboard-o"></i>
<span>Keymap</span>
</a>
</li>
<li #none [class.active]="activeTab === tabName.None" (click)="selectTab(tabName.None)">
<a class="menu-tabs--item">
<i class="fa fa-ban"></i>
<span>None</span>
</a>
</li>
</ul>
</div>
<div [ngSwitch]="activeTab">
<keypress-tab #tab *ngSwitchCase="tabName.Keypress" class="popover-content"
[defaultKeyAction]="defaultKeyAction"
[longPressEnabled]="true"
(validAction)="keyActionValid=$event"
></keypress-tab>
<layer-tab #tab *ngSwitchCase="tabName.Layer" class="popover-content"
[defaultKeyAction]="defaultKeyAction"
[currentLayer]="currentLayer"
(validAction)="keyActionValid=$event"
></layer-tab>
<mouse-tab #tab *ngSwitchCase="tabName.Mouse" class="popover-content"
[defaultKeyAction]="defaultKeyAction"
(validAction)="keyActionValid=$event"
></mouse-tab>
<macro-tab #tab *ngSwitchCase="tabName.Macro" class="popover-content"
[defaultKeyAction]="defaultKeyAction"
(validAction)="keyActionValid=$event"
></macro-tab>
<keymap-tab #tab *ngSwitchCase="tabName.Keymap" class="popover-content"
[defaultKeyAction]="defaultKeyAction"
[keymaps]="keymaps$ | async"
(validAction)="keyActionValid=$event"
></keymap-tab>
<none-tab #tab *ngSwitchCase="tabName.None" class="popover-content"
(validAction)="keyActionValid=$event"
></none-tab>
</div>
<div class="popover-action">
<button class="btn btn-sm btn-default" type="button" (click)="onCancelClick()"> Cancel </button>
<button class="btn btn-sm btn-primary" [class.disabled]="!keyActionValid" type="button" (click)="onRemapKey()"> Remap Key </button>
</div>
</div>
<div class="popover-overlay" [class.display]="visible" (click)="onOverlay()"></div>

View File

@@ -0,0 +1,135 @@
.popover {
display: flex;
flex-direction: column;
padding: 0;
max-width: 568px;
width: 100%;
user-select: none;
&.leftArrow {
.arrowCustom {
transform: none;
left: 22px;
}
}
&.rightArrow {
.arrowCustom {
transform: none;
right: 22px;
left: auto;
}
}
> .container-fluid {
overflow: hidden;
}
}
.nav-tabs > li {
overflow: hidden;
}
.arrowCustom {
position: absolute;
top: -16px;
left: 50%;
transform: translateX(-50%);
width: 41px;
height: 16px;
&:before {
content: '';
width: 0;
height: 0;
border-left: 21px solid transparent;
border-right: 21px solid transparent;
border-bottom: 17px solid rgba(0, 0, 0, 0.2);
display: block;
position: absolute;
top: -1px;
}
&:after {
content: '';
width: 0;
height: 0;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
border-bottom: 16px solid #f7f7f7;
display: block;
position: absolute;
top: 0;
}
}
.popover-action {
padding: 8px 14px;
margin: 0;
font-size: 14px;
background-color: #f7f7f7;
border-top: 1px solid #ebebeb;
border-radius: 0 0 5px 5px;
text-align: right;
}
.popover-title {
&.menu-tabs {
padding: 0.5rem 0.5rem 0;
display: block;
.nav-tabs {
position: relative;
top: 1px;
display: flex;
.menu-tabs--item {
display: flex;
align-items: center;
cursor: pointer;
i {
margin-right: 0.25em;
}
}
}
}
}
.popover-content {
padding: 10px 24px;
}
.popover-overlay {
position: fixed;
width: 100%;
height: 0;
top: 0;
left: 0;
z-index: 1050;
background: rgba(0, 0, 0, 0);
transition: background 200ms ease-out, height 0ms 200ms linear;
&.display {
height: 100%;
background: rgba(0, 0, 0, 0.2);
transition: background 200ms ease-out;
}
}
.select2-item {
position: relative;
font-size: 1.5rem;
&.keymap-name--wrapper {
padding-left: 50px;
}
.layout-segment-code {
height: 2rem;
position: absolute;
left: 0;
top: 50%;
margin-top: -1rem;
}
}

View File

@@ -0,0 +1,200 @@
import {
Component,
ElementRef,
EventEmitter,
HostListener,
Input,
OnChanges,
Output,
SimpleChanges,
ViewChild
} from '@angular/core';
import { animate, keyframes, state, style, transition, trigger } from '@angular/animations';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/combineLatest';
import 'rxjs/add/operator/map';
import {
KeyAction,
KeystrokeAction,
MouseAction,
PlayMacroAction,
SwitchKeymapAction,
SwitchLayerAction
} from '../../config-serializer/config-items/key-action';
import { Keymap } from '../../config-serializer/config-items/keymap';
import { Tab } from './tab/tab';
import { AppState } from '../../store';
import { getKeymaps } from '../../store/reducers/user-configuration';
enum TabName {
Keypress,
Layer,
Mouse,
Macro,
Keymap,
None
}
@Component({
selector: 'popover',
templateUrl: './popover.component.html',
styleUrls: ['./popover.component.scss'],
animations: [
trigger('popover', [
state('closed', style({
transform: 'translateY(30px)',
visibility: 'hidden',
opacity: 0
})),
state('opened', style({
transform: 'translateY(0)',
visibility: 'visible',
opacity: 1
})),
transition('opened => closed', [
animate('200ms ease-out', keyframes([
style({ transform: 'translateY(0)', visibility: 'visible', opacity: 1, offset: 0 }),
style({ transform: 'translateY(30px)', visibility: 'hidden', opacity: 0, offset: 1 })
]))
]),
transition('closed => opened', [
style({
visibility: 'visible'
}),
animate('200ms ease-out', keyframes([
style({ transform: 'translateY(30px)', opacity: 0, offset: 0 }),
style({ transform: 'translateY(0)', opacity: 1, offset: 1 })
]))
])
])
]
})
export class PopoverComponent implements OnChanges {
@Input() defaultKeyAction: KeyAction;
@Input() currentKeymap: Keymap;
@Input() currentLayer: number;
@Input() keyPosition: ClientRect;
@Input() wrapPosition: ClientRect;
@Input() visible: boolean;
@Output() cancel = new EventEmitter<any>();
@Output() remap = new EventEmitter<KeyAction>();
@ViewChild('tab') selectedTab: Tab;
@ViewChild('popover') popoverHost: ElementRef;
tabName = TabName;
keyActionValid: boolean;
activeTab: TabName;
keymaps$: Observable<Keymap[]>;
leftArrow: boolean = false;
rightArrow: boolean = false;
topPosition: number = 0;
leftPosition: number = 0;
animationState: string;
private readonly currentKeymap$ = new BehaviorSubject<Keymap>(undefined);
constructor(store: Store<AppState>) {
this.animationState = 'closed';
this.keymaps$ = store.let(getKeymaps())
.combineLatest(this.currentKeymap$)
.map(([keymaps, currentKeymap]: [Keymap[], Keymap]) =>
keymaps.filter((keymap: Keymap) => currentKeymap.abbreviation !== keymap.abbreviation)
);
}
ngOnChanges(change: SimpleChanges) {
if (this.keyPosition && this.wrapPosition && (change['keyPosition'] || change['wrapPosition'])) {
this.calculatePosition();
}
if (change['defaultKeyAction']) {
let tab: TabName;
if (this.defaultKeyAction instanceof KeystrokeAction) {
tab = TabName.Keypress;
} else if (this.defaultKeyAction instanceof SwitchLayerAction) {
tab = TabName.Layer;
} else if (this.defaultKeyAction instanceof MouseAction) {
tab = TabName.Mouse;
} else if (this.defaultKeyAction instanceof PlayMacroAction) {
tab = TabName.Macro;
} else if (this.defaultKeyAction instanceof SwitchKeymapAction) {
tab = TabName.Keymap;
} else {
tab = TabName.None;
}
this.selectTab(tab);
}
if (change['visible']) {
if (change['visible'].currentValue) {
this.animationState = 'opened';
} else {
this.animationState = 'closed';
}
}
if (change.currentKeymap) {
this.currentKeymap$.next(this.currentKeymap);
}
}
onCancelClick(): void {
this.cancel.emit(undefined);
}
onRemapKey(): void {
if (this.keyActionValid) {
try {
const keyAction = this.selectedTab.toKeyAction();
this.remap.emit(keyAction);
} catch (e) {
// TODO: show error dialog
console.error(e);
}
}
}
@HostListener('keydown.escape')
onEscape(): void {
this.cancel.emit();
}
selectTab(tab: TabName): void {
this.activeTab = tab;
}
onOverlay() {
this.cancel.emit(undefined);
}
private calculatePosition() {
const offsetLeft: number = this.wrapPosition.left + 265; // 265 is a width of the side menu with a margin
const popover: HTMLElement = this.popoverHost.nativeElement;
let newLeft: number = this.keyPosition.left + (this.keyPosition.width / 2);
this.leftArrow = newLeft < offsetLeft;
this.rightArrow = (newLeft + popover.offsetWidth) > offsetLeft + this.wrapPosition.width;
if (this.leftArrow) {
newLeft = this.keyPosition.left;
} else if (this.rightArrow) {
newLeft = this.keyPosition.left - popover.offsetWidth + this.keyPosition.width;
} else {
newLeft -= popover.offsetWidth / 2;
}
// 7 is a space between a bottom key position and a popover
this.topPosition = this.keyPosition.top + this.keyPosition.height + 7 + window.scrollY;
this.leftPosition = newLeft;
}
}

View File

@@ -0,0 +1,7 @@
export * from './tab';
export { KeymapTabComponent } from './keymap';
export { KeypressTabComponent } from './keypress';
export { LayerTabComponent } from './layer';
export { MacroTabComponent } from './macro';
export { MouseTabComponent } from './mouse';
export { NoneTabComponent } from './none';

View File

@@ -0,0 +1 @@
export * from './keymap-tab.component';

View File

@@ -0,0 +1,22 @@
<ng-template [ngIf]="keymapOptions.length === 0">
<span> No keymaps are available to choose from. Create a keymap first! </span>
</ng-template>
<ng-template [ngIf]="keymapOptions.length > 0">
<div>
<b>Switch to keymap:</b>
<select2
[data]="keymapOptions"
[value]="selectedKeymap?.abbreviation || -1"
(valueChanged)="onChange($event)"
[width]="'100%'"
></select2>
</div>
<div>
<div class="empty" *ngIf="!selectedKeymap?.abbreviation">
<img src="assets/images/base-layer--blank.svg">
</div>
<svg-keyboard *ngIf="selectedKeymap?.abbreviation"
[moduleConfig]="selectedKeymap.layers[0].modules">
</svg-keyboard>
</div>
</ng-template>

View File

@@ -0,0 +1,43 @@
:host {
display: flex;
flex-direction: column;
> span {
text-align: center;
}
> div {
display: flex;
margin-top: 2px;
b {
display: flex;
align-items: center;
margin-right: 7px;
}
select2 {
flex: 1;
}
}
> div:last-child {
margin-top: 10px;
img {
max-height: 100%;
max-width: 100%;
}
}
}
.empty {
display: flex;
img {
display: flex;
width: 100%;
height: 100%;
position: relative;
}
}

View File

@@ -0,0 +1,77 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Select2OptionData } from 'ng2-select2/ng2-select2';
import { KeyAction, SwitchKeymapAction } from '../../../../config-serializer/config-items/key-action';
import { Keymap } from '../../../../config-serializer/config-items/keymap';
import { Tab } from '../tab';
@Component({
selector: 'keymap-tab',
templateUrl: './keymap-tab.component.html',
styleUrls: ['./keymap-tab.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class KeymapTabComponent extends Tab implements OnChanges {
@Input() defaultKeyAction: KeyAction;
@Input() keymaps: Keymap[];
keymapOptions: Array<Select2OptionData>;
selectedKeymap: Keymap;
constructor() {
super();
this.keymapOptions = [];
}
ngOnChanges(changes: SimpleChanges) {
if (changes.keymaps) {
this.keymapOptions = this.keymaps
.map((keymap: Keymap): Select2OptionData => {
return {
id: keymap.abbreviation,
text: keymap.name
};
});
if (this.keymaps.length > 0) {
this.selectedKeymap = this.keymaps[0];
}
}
this.fromKeyAction(this.defaultKeyAction);
this.validAction.emit(true);
}
// TODO: change to the correct type when the wrapper has added it.
onChange(event: any) {
if (event.value === '-1') {
this.selectedKeymap = undefined;
} else {
this.selectedKeymap = this.keymaps.find((keymap: Keymap) => keymap.abbreviation === event.value);
}
}
keyActionValid(): boolean {
return !!this.selectedKeymap;
}
fromKeyAction(keyAction: KeyAction): boolean {
if (!(keyAction instanceof SwitchKeymapAction)) {
return false;
}
const switchKeymapAction: SwitchKeymapAction = <SwitchKeymapAction>keyAction;
this.selectedKeymap = this.keymaps
.find((keymap: Keymap) => keymap.abbreviation === switchKeymapAction.keymapAbbreviation);
}
toKeyAction(): SwitchKeymapAction {
if (!this.keyActionValid()) {
throw new Error('KeyAction is not valid. No selected keymap!');
}
const keymapAction = new SwitchKeymapAction();
keymapAction.keymapAbbreviation = this.selectedKeymap.abbreviation;
return keymapAction;
}
}

View File

@@ -0,0 +1 @@
export * from './keypress-tab.component';

View File

@@ -0,0 +1,53 @@
<div class="scancode-options">
<b class="setting-label">Scancode:</b>
<select2
[data]="scanCodeGroups"
[value]="selectedScancodeOption.id"
(valueChanged)="onScancodeChange($event)"
[width]="200"
[options]="options"
></select2>
<capture-keystroke-button (capture)="onKeysCapture($event)" tabindex="0"></capture-keystroke-button>
</div>
<div class="modifier-options">
<b class="setting-label">Modifiers:</b>
<div class="btn-toolbar modifiers">
<div class="btn-group btn-group-sm modifiers__left">
<button type="button" class="btn btn-default"
*ngFor="let modifier of leftModifiers; let index = index"
[class.btn-primary]="leftModifierSelects[index]"
(click)="toggleModifier(false, index)"
>
{{modifier}}
</button>
</div>
<div class="btn-group btn-group-sm modifiers__right">
<button type="button" class="btn btn-default"
*ngFor="let modifier of rightModifiers; let index = index"
[class.btn-primary]="rightModifierSelects[index]"
(click)="toggleModifier(true, index)"
>
{{modifier}}
</button>
</div>
</div>
</div>
<div class="long-press-container" *ngIf="longPressEnabled">
<b class="setting-label">Long press action:</b>
<select2 #longPressSelect
[data]="longPressGroups"
[value]="selectedLongPressIndex.toString()"
(valueChanged)="onLongpressChange($event)"
[width]="140"
></select2>
<icon name="question-circle"
data-toggle="tooltip"
title="This action activates when another key gets pressed while holding this key."
data-placement="bottom"></icon>
</div>
<div class="disabled-state--text">
<i class="fa fa-info-circle"></i>
When a key is configured as layer switcher key, you can't assign other functions to it.
To assign a scancode to the key, set the <em>Layer action</em> to <em>None</em>.
</div>

View File

@@ -0,0 +1,78 @@
:host {
display: flex;
flex-direction: column;
position: relative;
.scancode-options {
margin-bottom: 10px;
margin-top: 2px;
> b {
position: relative;
top: 2px;
}
}
.modifier-options {
> b {
position: relative;
top: -9px;
margin-right: 4px;
}
.btn-toolbar {
display: inline-block;
}
}
.long-press-container {
display: flex;
margin-top: 3rem;
> b {
margin-right: 0.6em;
align-items: center;
display: flex;
}
.secondary-role {
width: 135px;
}
icon {
margin-left: 0.6em;
}
}
.setting-label {
&.disabled {
color: #999;
}
}
.disabled-state--text {
display: none;
position: absolute;
top: 50%;
margin-top: -4rem;
color: #31708f;
padding-right: 40px;
.fa {
font-size: 2.6rem;
float: left;
padding: 1rem 1.5rem 2rem;
}
}
&.disabled {
.scancode-options,
.modifier-options,
.long-press-container {
visibility: hidden;
}
.disabled-state--text {
display: block;
}
}
}

View File

@@ -0,0 +1,231 @@
import { Component, Input, OnChanges } from '@angular/core';
import { Select2OptionData, Select2TemplateFunction } from 'ng2-select2';
import { KeyAction, KeystrokeAction } from '../../../../config-serializer/config-items/key-action';
import { Tab } from '../tab';
import { MapperService } from '../../../../services/mapper.service';
import { KeystrokeType } from '../../../../config-serializer/config-items/key-action/keystroke-type';
@Component({
selector: 'keypress-tab',
templateUrl: './keypress-tab.component.html',
styleUrls: ['./keypress-tab.component.scss']
})
export class KeypressTabComponent extends Tab implements OnChanges {
@Input() defaultKeyAction: KeyAction;
@Input() longPressEnabled: boolean;
leftModifiers: string[];
rightModifiers: string[];
leftModifierSelects: boolean[];
rightModifierSelects: boolean[];
scanCodeGroups: Array<Select2OptionData>;
longPressGroups: Array<Select2OptionData>;
options: Select2Options;
selectedScancodeOption: Select2OptionData;
selectedLongPressIndex: number;
constructor(private mapper: MapperService) {
super();
this.leftModifiers = ['LShift', 'LCtrl', 'LSuper', 'LAlt'];
this.rightModifiers = ['RShift', 'RCtrl', 'RSuper', 'RAlt'];
this.scanCodeGroups = [{
id: '0',
text: 'None'
}];
this.scanCodeGroups = this.scanCodeGroups.concat(require('./scancodes.json'));
this.longPressGroups = require('./longPress.json');
this.leftModifierSelects = Array(this.leftModifiers.length).fill(false);
this.rightModifierSelects = Array(this.rightModifiers.length).fill(false);
this.selectedScancodeOption = this.scanCodeGroups[0];
this.selectedLongPressIndex = -1;
this.options = {
templateResult: this.scanCodeTemplateResult,
matcher: (term: string, text: string, data: Select2OptionData) => {
let found = text.toUpperCase().indexOf(term.toUpperCase()) > -1;
if (!found && data.additional && data.additional.explanation) {
found = data.additional.explanation.toUpperCase().indexOf(term.toUpperCase()) > -1;
}
return found;
}
};
}
ngOnChanges() {
this.fromKeyAction(this.defaultKeyAction);
this.validAction.emit(this.keyActionValid());
}
keyActionValid(keystrokeAction?: KeystrokeAction): boolean {
if (!keystrokeAction) {
keystrokeAction = this.toKeyAction();
}
return (keystrokeAction) ? (keystrokeAction.scancode > 0 || keystrokeAction.modifierMask > 0) : false;
}
onKeysCapture(event: { code: number, left: boolean[], right: boolean[] }) {
if (event.code) {
this.selectedScancodeOption = this.findScancodeOptionByScancode(event.code, KeystrokeType.basic);
} else {
this.selectedScancodeOption = this.scanCodeGroups[0];
}
this.leftModifierSelects = event.left;
this.rightModifierSelects = event.right;
this.validAction.emit(this.keyActionValid());
}
fromKeyAction(keyAction: KeyAction): boolean {
if (!(keyAction instanceof KeystrokeAction)) {
return false;
}
const keystrokeAction: KeystrokeAction = <KeystrokeAction>keyAction;
// Restore selectedScancodeOption
this.selectedScancodeOption = this.findScancodeOptionByScancode(keystrokeAction.scancode || 0, keystrokeAction.type);
const leftModifiersLength: number = this.leftModifiers.length;
// Restore modifiers
for (let i = 0; i < leftModifiersLength; ++i) {
this.leftModifierSelects[this.mapper.modifierMapper(i)] = ((keystrokeAction.modifierMask >> i) & 1) === 1;
}
for (let i = leftModifiersLength; i < leftModifiersLength + this.rightModifierSelects.length; ++i) {
const index: number = this.mapper.modifierMapper(i) - leftModifiersLength;
this.rightModifierSelects[index] = ((keystrokeAction.modifierMask >> i) & 1) === 1;
}
// Restore longPressAction
if (keystrokeAction.longPressAction !== undefined) {
this.selectedLongPressIndex = this.mapper.modifierMapper(keystrokeAction.longPressAction);
}
return true;
}
toKeyAction(): KeystrokeAction {
const keystrokeAction: KeystrokeAction = new KeystrokeAction();
const scTypePair = this.toScancodeTypePair(this.selectedScancodeOption);
keystrokeAction.scancode = scTypePair[0];
if (scTypePair[1] === 'media') {
keystrokeAction.type = KeystrokeType.shortMedia;
} else {
keystrokeAction.type = KeystrokeType[scTypePair[1]];
}
keystrokeAction.modifierMask = 0;
const modifiers = this.leftModifierSelects.concat(this.rightModifierSelects).map(x => x ? 1 : 0);
for (let i = 0; i < modifiers.length; ++i) {
keystrokeAction.modifierMask |= modifiers[i] << this.mapper.modifierMapper(i);
}
keystrokeAction.longPressAction = this.selectedLongPressIndex === -1
? undefined
: this.mapper.modifierMapper(this.selectedLongPressIndex);
if (this.keyActionValid(keystrokeAction)) {
return keystrokeAction;
}
}
scanCodeTemplateResult: Select2TemplateFunction = (state: Select2OptionData): JQuery | string => {
if (!state.id) {
return state.text;
}
if (state.additional && state.additional.explanation) {
return jQuery(
'<span class="select2-item">'
+ '<span>' + state.text + '</span>'
+ '<span class="scancode--searchterm"> '
+ state.additional.explanation
+ '</span>' +
'</span>'
);
} else {
return jQuery('<span class="select2-item">' + state.text + '</span>');
}
}
toggleModifier(right: boolean, index: number) {
const modifierSelects: boolean[] = right ? this.rightModifierSelects : this.leftModifierSelects;
modifierSelects[index] = !modifierSelects[index];
this.validAction.emit(this.keyActionValid());
}
onLongpressChange(event: { value: string }) {
this.selectedLongPressIndex = +event.value;
}
onScancodeChange(event: { value: string }) {
const id: string = event.value;
// ng2-select2 should provide the selectedOption in an upcoming release
// TODO: change this when it has become available
this.selectedScancodeOption = this.findScancodeOptionById(id);
this.validAction.emit(this.keyActionValid());
}
private findScancodeOptionBy(predicate: (option: Select2OptionData) => boolean): Select2OptionData {
let selectedOption: Select2OptionData;
const scanCodeGroups: Select2OptionData[] = [...this.scanCodeGroups];
while (scanCodeGroups.length > 0) {
const scanCodeGroup = scanCodeGroups.shift();
if (predicate(scanCodeGroup)) {
selectedOption = scanCodeGroup;
break;
}
if (scanCodeGroup.children) {
scanCodeGroups.push(...scanCodeGroup.children);
}
}
return selectedOption;
}
private findScancodeOptionById(id: string): Select2OptionData {
return this.findScancodeOptionBy(option => option.id === id);
}
private findScancodeOptionByScancode(scancode: number, type: KeystrokeType): Select2OptionData {
const typeToFind: string =
(type === KeystrokeType.shortMedia || type === KeystrokeType.longMedia) ? 'media' : KeystrokeType[type];
return this.findScancodeOptionBy((option: Select2OptionData) => {
const additional = option.additional;
if (additional && additional.scancode === scancode && additional.type === typeToFind) {
return true;
} else if ((!additional || additional.scancode === undefined) && +option.id === scancode) {
return true;
} else {
return false;
}
});
}
private toScancodeTypePair(option: Select2OptionData): [number, string] {
let scanCode: number;
let type: string;
if (option.additional) {
scanCode = option.additional.scancode;
type = option.additional.type || 'basic';
} else {
type = 'basic';
}
if (scanCode === undefined) {
scanCode = +option.id;
}
return [scanCode, type];
}
}

View File

@@ -0,0 +1,60 @@
[
{
"id": "-1",
"text": "None"
},
{
"text": "Modifier",
"children": [
{
"id": "0",
"text": "LShift"
},
{
"id": "1",
"text": "LCtrl"
},
{
"id": "2",
"text": "LSuper"
},
{
"id": "3",
"text": "LAlt"
},
{
"id": "4",
"text": "RShift"
},
{
"id": "5",
"text": "RCtrl"
},
{
"id": "6",
"text": "RSuper"
},
{
"id": "7",
"text": "RAlt"
}
]
},
{
"text": "Layer switcher",
"children": [
{
"id": "8",
"text": "Mod"
},
{
"id": "9",
"text": "Mouse"
},
{
"id": "10",
"text": "Fn"
}
]
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
export * from './layer-tab.component';

View File

@@ -0,0 +1,20 @@
<ng-template [ngIf]="!isNotBase">
<select (change)="toggleChanged($event.target.value)">
<option *ngFor="let item of toggleData" [value]="item.id" [selected]="toggle === item.id">
{{ item.text }}
</option>
</select>
<span>the</span>
<select (change)="layerChanged($event.target.value)">
<option *ngFor="let item of layerData" [value]="item.id" [selected]="layer === item.id">
{{ item.text }}
</option>
</select>
<span [ngSwitch]="toggle">
<ng-template [ngSwitchCase]="true">layer by pressing this key.</ng-template>
<ng-template ngSwitchDefault>layer by holding this key.</ng-template>
</span>
</ng-template>
<ng-template [ngIf]="isNotBase">
<span> Layer switching is only possible from the base layer. </span>
</ng-template>

View File

@@ -0,0 +1,22 @@
:host {
display: flex;
margin: 0 -5px;
&.no-base {
justify-content: center;
}
> span,
> select {
margin: 0 5px;
display: flex;
align-items: center;
}
}
select {
background-color: #fff;
border: 1px solid #aaa;
border-radius: 4px;
padding: 4px 20px 4px 8px;
}

View File

@@ -0,0 +1,97 @@
import { Component, HostBinding, Input, OnChanges, SimpleChanges } from '@angular/core';
import { KeyAction, LayerName, SwitchLayerAction } from '../../../../config-serializer/config-items/key-action';
import { Tab } from '../tab';
@Component({
selector: 'layer-tab',
templateUrl: './layer-tab.component.html',
styleUrls: ['./layer-tab.component.scss']
})
export class LayerTabComponent extends Tab implements OnChanges {
@Input() defaultKeyAction: KeyAction;
@Input() currentLayer: number;
@HostBinding('class.no-base') isNotBase: boolean;
toggleData: { id: boolean, text: string }[] = [
{
id: false,
text: 'Activate'
},
{
id: true,
text: 'Toggle'
}
];
layerData: { id: number, text: string }[] = [
{
id: 0,
text: 'Mod'
},
{
id: 1,
text: 'Fn'
},
{
id: 2,
text: 'Mouse'
}
];
toggle: boolean;
layer: LayerName;
constructor() {
super();
this.toggle = false;
this.layer = LayerName.mod;
}
ngOnChanges(changes: SimpleChanges) {
if (changes['defaultKeyAction']) {
this.fromKeyAction(this.defaultKeyAction);
}
if (changes['currentLayer']) {
this.isNotBase = this.currentLayer > 0;
}
this.validAction.emit(true);
}
keyActionValid(): boolean {
return !this.isNotBase;
}
fromKeyAction(keyAction: KeyAction): boolean {
if (!(keyAction instanceof SwitchLayerAction)) {
return false;
}
const switchLayerAction: SwitchLayerAction = <SwitchLayerAction>keyAction;
this.toggle = switchLayerAction.isLayerToggleable;
this.layer = switchLayerAction.layer;
return true;
}
toKeyAction(): SwitchLayerAction {
const keyAction = new SwitchLayerAction();
keyAction.isLayerToggleable = this.toggle;
keyAction.layer = this.layer;
if (!this.keyActionValid()) {
throw new Error('KeyAction is invalid!');
}
return keyAction;
}
toggleChanged(value: string) {
this.toggle = value === 'true';
}
layerChanged(value: number) {
this.layer = +value;
}
}

View File

@@ -0,0 +1 @@
export * from './macro-tab.component';

View File

@@ -0,0 +1,16 @@
<ng-template [ngIf]="macroOptions.length === 0">
<span> No macros are available to choose from. Create a macro first! </span>
</ng-template>
<ng-template [ngIf]="macroOptions.length > 0">
<div class="macro-selector">
<b> Play macro: </b>
<select2 [data]="macroOptions" [value]="macroOptions[selectedMacroIndex].id" (valueChanged)="onChange($event)" [width]="'100%'"></select2>
</div>
<div class="macro-action-container">
<div class="list-group">
<macro-item *ngFor="let macroAction of macros[selectedMacroIndex].macroActions"
[macroAction]="macroAction" [editable]="false">
</macro-item>
</div>
</div>
</ng-template>

View File

@@ -0,0 +1,40 @@
:host {
display: flex;
flex-direction: column;
> span {
text-align: center;
}
.macro-selector {
display: flex;
margin-top: 2px;
b {
display: flex;
align-items: center;
margin-right: 7px;
}
select2 {
flex: 1;
}
}
.macro-action-container {
display: flex;
flex-direction: column;
min-height: 200px;
max-height: 300px;
margin: 20px 0;
overflow-x: hidden;
overflow-y: auto;
border-radius: 4px;
border: 1px solid #ddd;
.list-group {
margin-bottom: 0;
border: 0;
}
}
}

View File

@@ -0,0 +1,83 @@
import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Subscription } from 'rxjs/Subscription';
import { Select2OptionData } from 'ng2-select2/ng2-select2';
import { KeyAction, PlayMacroAction } from '../../../../config-serializer/config-items/key-action';
import { Macro } from '../../../../config-serializer/config-items/macro';
import { Tab } from '../tab';
import { AppState } from '../../../../store/index';
import { getMacros } from '../../../../store/reducers/user-configuration';
@Component({
selector: 'macro-tab',
templateUrl: './macro-tab.component.html',
styleUrls: ['./macro-tab.component.scss']
})
export class MacroTabComponent extends Tab implements OnInit, OnChanges, OnDestroy {
@Input() defaultKeyAction: KeyAction;
macros: Macro[];
macroOptions: Array<Select2OptionData>;
selectedMacroIndex: number;
private subscription: Subscription;
constructor(store: Store<AppState>) {
super();
this.subscription = store.let(getMacros())
.subscribe((macros: Macro[]) => this.macros = macros);
this.macroOptions = [];
this.selectedMacroIndex = 0;
}
ngOnInit() {
this.macroOptions = this.macros.map(function (macro: Macro, index: number): Select2OptionData {
return {
id: index.toString(),
text: macro.name
};
});
}
ngOnChanges() {
this.fromKeyAction(this.defaultKeyAction);
this.validAction.emit(true);
}
// TODO: change to the correct type when the wrapper has added it.
onChange(event: any) {
this.selectedMacroIndex = +event.value;
}
keyActionValid(): boolean {
return this.selectedMacroIndex >= 0;
}
fromKeyAction(keyAction: KeyAction): boolean {
if (!(keyAction instanceof PlayMacroAction)) {
return false;
}
const playMacroAction: PlayMacroAction = <PlayMacroAction>keyAction;
this.selectedMacroIndex = this.macros.findIndex(macro => playMacroAction.macroId === macro.id);
return true;
}
toKeyAction(): PlayMacroAction {
if (!this.keyActionValid()) {
throw new Error('KeyAction is not valid. No selected macro!');
}
const keymapAction = new PlayMacroAction();
keymapAction.macroId = this.macros[this.selectedMacroIndex].id;
return keymapAction;
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}

Some files were not shown because too many files have changed in this diff Show More