ui: Force leave hovered button when pressing keyboard
This commit is contained in:
parent
3b4c5f0d7c
commit
156a2ae4bd
1
TODO.md
1
TODO.md
|
@ -90,7 +90,6 @@ Common UI
|
|||
|
||||
* Add caret/focus to text input
|
||||
* Add a standard confirm dialog
|
||||
* Hover out when using keyboard shortcuts
|
||||
* Mobile: think UI layout so that fingers do not block the view (right and left handed)
|
||||
* Mobile: display tooltips larger and on the side of screen where the finger is not
|
||||
* Mobile: targetting in two times, using a draggable target indicator
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit c77c859359e3cee3700dcd7a28431f2f0b49e0a2
|
||||
Subproject commit f4fb99bb2a5b6dc393a4fa115d4bf25cdae12ccf
|
|
@ -27,7 +27,7 @@ module TK.SpaceTac.UI {
|
|||
this.hovered = null;
|
||||
|
||||
this.info_button = new Phaser.Button(this.game, 0, 0, "battle-shiplist-info-button");
|
||||
UITools.setHoverClick(this.info_button,
|
||||
this.battleview.inputs.setHoverClick(this.info_button,
|
||||
() => this.battleview.toggle_tactical_mode.manipulate("button")(true),
|
||||
() => this.battleview.toggle_tactical_mode.manipulate("button")(false),
|
||||
() => null);
|
||||
|
|
|
@ -43,7 +43,11 @@ module TK.SpaceTac.UI {
|
|||
this.hover_indicator.visible = false;
|
||||
this.addChild(this.hover_indicator);
|
||||
|
||||
UITools.setHoverClick(this, () => list.battleview.cursorOnShip(ship), () => list.battleview.cursorOffShip(ship), () => list.battleview.cursorClicked());
|
||||
this.view.inputs.setHoverClick(this,
|
||||
() => list.battleview.cursorOnShip(ship),
|
||||
() => list.battleview.cursorOffShip(ship),
|
||||
() => list.battleview.cursorClicked()
|
||||
);
|
||||
}
|
||||
|
||||
// Flash a damage indicator
|
||||
|
|
102
src/ui/common/InputManager.spec.ts
Normal file
102
src/ui/common/InputManager.spec.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
module TK.SpaceTac.UI.Specs {
|
||||
describe("InputManager", function () {
|
||||
let testgame = setupEmptyView();
|
||||
|
||||
it("handles hover and click on desktops and mobile targets", function (done) {
|
||||
let inputs = testgame.baseview.inputs;
|
||||
|
||||
let pointer = new Phaser.Pointer(testgame.ui, 0);
|
||||
function newButton(): [Phaser.Button, any] {
|
||||
var button = new Phaser.Button(testgame.ui);
|
||||
var funcs = {
|
||||
enter: () => null,
|
||||
leave: () => null,
|
||||
click: () => null,
|
||||
};
|
||||
spyOn(funcs, "enter");
|
||||
spyOn(funcs, "leave");
|
||||
spyOn(funcs, "click");
|
||||
inputs.setHoverClick(button, funcs.enter, funcs.leave, funcs.click, 50, 100);
|
||||
(<any>inputs).hovered = null;
|
||||
return [button, funcs];
|
||||
}
|
||||
let enter = (button: Phaser.Button) => (<any>button.input)._pointerOverHandler(pointer);
|
||||
let leave = (button: Phaser.Button) => (<any>button.input)._pointerOutHandler(pointer);
|
||||
let press = (button: Phaser.Button) => button.onInputDown.dispatch(button, pointer);
|
||||
let release = (button: Phaser.Button) => button.onInputUp.dispatch(button, pointer);
|
||||
let destroy = (button: Phaser.Button) => button.events.onDestroy.dispatch();
|
||||
|
||||
// Simple click on desktop
|
||||
let [button, funcs] = newButton();
|
||||
enter(button);
|
||||
press(button);
|
||||
release(button);
|
||||
expect(funcs.enter).toHaveBeenCalledTimes(0);
|
||||
expect(funcs.leave).toHaveBeenCalledTimes(0);
|
||||
expect(funcs.click).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Simple click on mobile
|
||||
[button, funcs] = newButton();
|
||||
press(button);
|
||||
release(button);
|
||||
expect(funcs.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.leave).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.click).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Leaves on destroy
|
||||
[button, funcs] = newButton();
|
||||
press(button);
|
||||
jasmine.clock().tick(150);
|
||||
expect(funcs.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.leave).toHaveBeenCalledTimes(0);
|
||||
expect(funcs.click).toHaveBeenCalledTimes(0);
|
||||
destroy(button);
|
||||
expect(funcs.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.leave).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.click).toHaveBeenCalledTimes(0);
|
||||
press(button);
|
||||
release(button);
|
||||
expect(funcs.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.leave).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.click).toHaveBeenCalledTimes(0);
|
||||
|
||||
// Force-leave when hovering another button without clean leaving a first one
|
||||
let [button1, funcs1] = newButton();
|
||||
let [button2, funcs2] = newButton();
|
||||
enter(button1);
|
||||
jasmine.clock().tick(150);
|
||||
expect(funcs1.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs1.leave).toHaveBeenCalledTimes(0);
|
||||
expect(funcs1.click).toHaveBeenCalledTimes(0);
|
||||
enter(button2);
|
||||
expect(funcs1.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs1.leave).toHaveBeenCalledTimes(1);
|
||||
expect(funcs1.click).toHaveBeenCalledTimes(0);
|
||||
expect(funcs2.enter).toHaveBeenCalledTimes(0);
|
||||
expect(funcs2.leave).toHaveBeenCalledTimes(0);
|
||||
expect(funcs2.click).toHaveBeenCalledTimes(0);
|
||||
jasmine.clock().tick(150);
|
||||
expect(funcs1.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs1.leave).toHaveBeenCalledTimes(1);
|
||||
expect(funcs1.click).toHaveBeenCalledTimes(0);
|
||||
expect(funcs2.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs2.leave).toHaveBeenCalledTimes(0);
|
||||
expect(funcs2.click).toHaveBeenCalledTimes(0);
|
||||
|
||||
// Hold to hover on mobile
|
||||
jasmine.clock().uninstall();
|
||||
[button, funcs] = newButton();
|
||||
button.onInputDown.dispatch(button, pointer);
|
||||
Timer.global.schedule(150, () => {
|
||||
expect(funcs.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.leave).toHaveBeenCalledTimes(0);
|
||||
expect(funcs.click).toHaveBeenCalledTimes(0);
|
||||
button.onInputUp.dispatch(button, pointer);
|
||||
expect(funcs.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.leave).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.click).toHaveBeenCalledTimes(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
module TK.SpaceTac.UI {
|
||||
type KeyPressedCallback = (key: string) => void;
|
||||
export type KeyPressedCallback = (key: string) => void
|
||||
|
||||
/**
|
||||
* Manager for keyboard/mouse/touch events.
|
||||
|
@ -13,6 +13,8 @@ module TK.SpaceTac.UI {
|
|||
private cheats_allowed: boolean
|
||||
private cheat: boolean
|
||||
|
||||
private hovered: Phaser.Button | null = null
|
||||
|
||||
private binds: { [key: string]: KeyPressedCallback } = {}
|
||||
|
||||
private keyboard_grabber: any = null
|
||||
|
@ -53,6 +55,8 @@ module TK.SpaceTac.UI {
|
|||
console.log(event);
|
||||
}
|
||||
|
||||
this.forceLeaveHovered();
|
||||
|
||||
if (!contains(["Control", "Shift", "Alt", "Meta"], event.key)) {
|
||||
this.keyPress(event.key);
|
||||
if (event.code != event.key) {
|
||||
|
@ -115,5 +119,123 @@ module TK.SpaceTac.UI {
|
|||
this.keyboard_callback = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the cursor out of currently hovered object
|
||||
*/
|
||||
private forceLeaveHovered() {
|
||||
if (this.hovered && this.hovered.data.hover_pointer) {
|
||||
(<any>this.hovered.input)._pointerOutHandler(this.hovered.data.hover_pointer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup hover/click handlers on an UI element
|
||||
*
|
||||
* This is done in a way that should be compatible with touch-enabled screen
|
||||
*
|
||||
* Returns functions that may be used to force the behavior
|
||||
*/
|
||||
setHoverClick(obj: Phaser.Button, enter = nop, leave = nop, click = nop, hovertime = 300, holdtime = 600) {
|
||||
let holdstart = new Date();
|
||||
let enternext: Function | null = null;
|
||||
let entercalled = false;
|
||||
let cursorinside = false;
|
||||
let destroyed = false;
|
||||
|
||||
obj.input.useHandCursor = true;
|
||||
|
||||
let prevententer = () => {
|
||||
if (enternext != null) {
|
||||
Timer.global.cancel(enternext);
|
||||
enternext = null;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let effectiveenter = () => {
|
||||
if (!destroyed) {
|
||||
enternext = null;
|
||||
entercalled = true;
|
||||
enter();
|
||||
}
|
||||
}
|
||||
|
||||
let effectiveleave = () => {
|
||||
prevententer();
|
||||
if (entercalled) {
|
||||
entercalled = false;
|
||||
leave();
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.events) {
|
||||
obj.events.onDestroy.addOnce(() => {
|
||||
destroyed = true;
|
||||
effectiveleave();
|
||||
});
|
||||
}
|
||||
|
||||
obj.onInputOver.add((_: any, pointer: Phaser.Pointer) => {
|
||||
if (destroyed) return;
|
||||
|
||||
if (this.hovered) {
|
||||
if (this.hovered === obj) {
|
||||
return;
|
||||
} else {
|
||||
this.forceLeaveHovered();
|
||||
}
|
||||
}
|
||||
this.hovered = obj;
|
||||
this.hovered.data.hover_pointer = pointer;
|
||||
|
||||
if (obj.visible && obj.alpha) {
|
||||
cursorinside = true;
|
||||
enternext = Timer.global.schedule(hovertime, effectiveenter);
|
||||
}
|
||||
});
|
||||
|
||||
obj.onInputOut.add(() => {
|
||||
if (destroyed) return;
|
||||
|
||||
if (this.hovered === obj) {
|
||||
this.hovered = null;
|
||||
}
|
||||
|
||||
cursorinside = false;
|
||||
effectiveleave();
|
||||
});
|
||||
|
||||
obj.onInputDown.add(() => {
|
||||
if (destroyed) return;
|
||||
|
||||
if (obj.visible && obj.alpha) {
|
||||
holdstart = new Date();
|
||||
if (!cursorinside && !enternext) {
|
||||
enternext = Timer.global.schedule(holdtime, effectiveenter);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
obj.onInputUp.add(() => {
|
||||
if (destroyed) return;
|
||||
|
||||
if (!cursorinside) {
|
||||
effectiveleave();
|
||||
}
|
||||
|
||||
if (new Date().getTime() - holdstart.getTime() < holdtime) {
|
||||
if (!cursorinside) {
|
||||
effectiveenter();
|
||||
}
|
||||
click();
|
||||
if (!cursorinside) {
|
||||
effectiveleave();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -160,7 +160,7 @@ module TK.SpaceTac.UI {
|
|||
* When the component is hovered, the function is called to allow filling the tooltip container
|
||||
*/
|
||||
bind(obj: Phaser.Button, func: (filler: TooltipFiller) => boolean): void {
|
||||
UITools.setHoverClick(obj,
|
||||
this.view.inputs.setHoverClick(obj,
|
||||
// enter
|
||||
() => {
|
||||
this.hide();
|
||||
|
|
|
@ -29,101 +29,6 @@ module TK.SpaceTac.UI.Specs {
|
|||
expect(image.x).toBe(100);
|
||||
expect(image.y).toBe(100);
|
||||
});
|
||||
|
||||
it("handles hover and click on desktops and mobile targets", function (done) {
|
||||
let pointer = new Phaser.Pointer(testgame.ui, 0);
|
||||
function newButton(): [Phaser.Button, any] {
|
||||
var button = new Phaser.Button(testgame.ui);
|
||||
var funcs = {
|
||||
enter: () => null,
|
||||
leave: () => null,
|
||||
click: () => null,
|
||||
};
|
||||
spyOn(funcs, "enter");
|
||||
spyOn(funcs, "leave");
|
||||
spyOn(funcs, "click");
|
||||
UITools.setHoverClick(button, funcs.enter, funcs.leave, funcs.click, 50, 100);
|
||||
UITools.hovered = null;
|
||||
return [button, funcs];
|
||||
}
|
||||
let enter = (button: Phaser.Button) => (<any>button.input)._pointerOverHandler(pointer);
|
||||
let leave = (button: Phaser.Button) => (<any>button.input)._pointerOutHandler(pointer);
|
||||
let press = (button: Phaser.Button) => button.onInputDown.dispatch(button, pointer);
|
||||
let release = (button: Phaser.Button) => button.onInputUp.dispatch(button, pointer);
|
||||
let destroy = (button: Phaser.Button) => button.events.onDestroy.dispatch();
|
||||
|
||||
// Simple click on desktop
|
||||
let [button, funcs] = newButton();
|
||||
enter(button);
|
||||
press(button);
|
||||
release(button);
|
||||
expect(funcs.enter).toHaveBeenCalledTimes(0);
|
||||
expect(funcs.leave).toHaveBeenCalledTimes(0);
|
||||
expect(funcs.click).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Simple click on mobile
|
||||
[button, funcs] = newButton();
|
||||
press(button);
|
||||
release(button);
|
||||
expect(funcs.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.leave).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.click).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Leaves on destroy
|
||||
[button, funcs] = newButton();
|
||||
press(button);
|
||||
jasmine.clock().tick(150);
|
||||
expect(funcs.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.leave).toHaveBeenCalledTimes(0);
|
||||
expect(funcs.click).toHaveBeenCalledTimes(0);
|
||||
destroy(button);
|
||||
expect(funcs.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.leave).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.click).toHaveBeenCalledTimes(0);
|
||||
press(button);
|
||||
release(button);
|
||||
expect(funcs.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.leave).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.click).toHaveBeenCalledTimes(0);
|
||||
|
||||
// Force-leave when hovering another button without clean leaving a first one
|
||||
let [button1, funcs1] = newButton();
|
||||
let [button2, funcs2] = newButton();
|
||||
enter(button1);
|
||||
jasmine.clock().tick(150);
|
||||
expect(funcs1.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs1.leave).toHaveBeenCalledTimes(0);
|
||||
expect(funcs1.click).toHaveBeenCalledTimes(0);
|
||||
enter(button2);
|
||||
expect(funcs1.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs1.leave).toHaveBeenCalledTimes(1);
|
||||
expect(funcs1.click).toHaveBeenCalledTimes(0);
|
||||
expect(funcs2.enter).toHaveBeenCalledTimes(0);
|
||||
expect(funcs2.leave).toHaveBeenCalledTimes(0);
|
||||
expect(funcs2.click).toHaveBeenCalledTimes(0);
|
||||
jasmine.clock().tick(150);
|
||||
expect(funcs1.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs1.leave).toHaveBeenCalledTimes(1);
|
||||
expect(funcs1.click).toHaveBeenCalledTimes(0);
|
||||
expect(funcs2.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs2.leave).toHaveBeenCalledTimes(0);
|
||||
expect(funcs2.click).toHaveBeenCalledTimes(0);
|
||||
|
||||
// Hold to hover on mobile
|
||||
jasmine.clock().uninstall();
|
||||
[button, funcs] = newButton();
|
||||
button.onInputDown.dispatch(button, pointer);
|
||||
Timer.global.schedule(150, () => {
|
||||
expect(funcs.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.leave).toHaveBeenCalledTimes(0);
|
||||
expect(funcs.click).toHaveBeenCalledTimes(0);
|
||||
button.onInputUp.dispatch(button, pointer);
|
||||
expect(funcs.enter).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.leave).toHaveBeenCalledTimes(1);
|
||||
expect(funcs.click).toHaveBeenCalledTimes(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes angles", function () {
|
||||
|
|
|
@ -17,8 +17,6 @@ module TK.SpaceTac.UI {
|
|||
|
||||
// Common UI tools functions
|
||||
export class UITools {
|
||||
static hovered: Phaser.Button | null = null;
|
||||
|
||||
/**
|
||||
* Get the position of an object, adjusted to remain inside a container
|
||||
*/
|
||||
|
@ -54,113 +52,6 @@ module TK.SpaceTac.UI {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a hover/hold/click routine on an object
|
||||
*
|
||||
* This should span the bridge between desktop and mobile targets.
|
||||
*/
|
||||
static setHoverClick(obj: Phaser.Button, enter: Function, leave: Function, click: Function, hovertime = 300, holdtime = 600) {
|
||||
let holdstart = new Date();
|
||||
let enternext: Function | null = null;
|
||||
let entercalled = false;
|
||||
let cursorinside = false;
|
||||
let destroyed = false;
|
||||
|
||||
obj.input.useHandCursor = true;
|
||||
|
||||
let prevententer = () => {
|
||||
if (enternext != null) {
|
||||
Timer.global.cancel(enternext);
|
||||
enternext = null;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let effectiveenter = () => {
|
||||
if (!destroyed) {
|
||||
enternext = null;
|
||||
entercalled = true;
|
||||
enter();
|
||||
}
|
||||
}
|
||||
|
||||
let effectiveleave = () => {
|
||||
prevententer();
|
||||
if (entercalled) {
|
||||
entercalled = false;
|
||||
leave();
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.events) {
|
||||
obj.events.onDestroy.addOnce(() => {
|
||||
destroyed = true;
|
||||
effectiveleave();
|
||||
});
|
||||
}
|
||||
|
||||
obj.onInputOver.add((_: any, pointer: Phaser.Pointer) => {
|
||||
if (destroyed) return;
|
||||
|
||||
if (UITools.hovered) {
|
||||
if (UITools.hovered === obj) {
|
||||
return;
|
||||
} else {
|
||||
// Dirty fix - Force a "pointer out" on previously hovered, if it did not go out cleanly
|
||||
(<any>UITools.hovered.input)._pointerOutHandler(pointer);
|
||||
}
|
||||
}
|
||||
UITools.hovered = obj;
|
||||
|
||||
if (obj.visible && obj.alpha) {
|
||||
cursorinside = true;
|
||||
enternext = Timer.global.schedule(hovertime, effectiveenter);
|
||||
}
|
||||
});
|
||||
|
||||
obj.onInputOut.add(() => {
|
||||
if (destroyed) return;
|
||||
|
||||
if (UITools.hovered === obj) {
|
||||
UITools.hovered = null;
|
||||
}
|
||||
|
||||
cursorinside = false;
|
||||
effectiveleave();
|
||||
});
|
||||
|
||||
obj.onInputDown.add(() => {
|
||||
if (destroyed) return;
|
||||
|
||||
if (obj.visible && obj.alpha) {
|
||||
holdstart = new Date();
|
||||
if (!cursorinside && !enternext) {
|
||||
enternext = Timer.global.schedule(holdtime, effectiveenter);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
obj.onInputUp.add(() => {
|
||||
if (destroyed) return;
|
||||
|
||||
if (!cursorinside) {
|
||||
effectiveleave();
|
||||
}
|
||||
|
||||
if (new Date().getTime() - holdstart.getTime() < holdtime) {
|
||||
if (!cursorinside) {
|
||||
effectiveenter();
|
||||
}
|
||||
click();
|
||||
if (!cursorinside) {
|
||||
effectiveleave();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Constraint an angle in radians the ]-pi;pi] range.
|
||||
static normalizeAngle(angle: number): number {
|
||||
angle = angle % (2 * Math.PI);
|
||||
|
|
Loading…
Reference in a new issue