1
0
Fork 0

ui: Force leave hovered button when pressing keyboard

This commit is contained in:
Michaël Lemaire 2017-10-01 22:52:50 +02:00
parent 3b4c5f0d7c
commit 156a2ae4bd
9 changed files with 233 additions and 210 deletions

View file

@ -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

View file

@ -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);

View file

@ -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

View 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();
});
});
});
}

View file

@ -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();
}
}
});
}
}
}

View file

@ -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();

View file

@ -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 () {

View file

@ -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);