Skip to content

Commit

Permalink
remove mousepad element since mousepad action is now part of touchpad…
Browse files Browse the repository at this point in the history
…, move initial/current/delta X/Y logic into base functions called in base onStart/Move and called in super by child elements
  • Loading branch information
Nerwyn committed Dec 9, 2024
1 parent 1eef549 commit b20efa1
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 236 deletions.
31 changes: 13 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ A super customizable universal remote card iterating on the work of several othe
- Roku (with keyboard)
- LG webOS (with keyboard)
- Kodi (with keyboard)
- Unified Remote for computers (with keyboard and mousepad)
- Unified Remote for computers (with keyboard)
- Apple TV
- Samsung TV
- Jellyfin
- Support for multiple buttons, touchpads, mousepads, and sliders using default or user defined custom actions.
- Support for multiple buttons, touchpads, and sliders using default or user defined custom actions.
- Complete [Home Assistant actions](https://www.home-assistant.io/dashboards/actions/) support.
- Keyboard and search dialog actions for most platforms.
- [Template](#a-note-on-templating) support for almost all fields using nunjucks.
- Toggleable haptics.
- Remappable touchpad and mousepad with [multi-touch](#touchpad-actions) gesture support.
- Remappable touchpad with [multi-touch and mouse](#touchpad-actions) gesture support.
- Remappable slider with vertical orientation support.
- User configurable remote [layout](#layout).
- Icons and labels for all elements.
Expand Down Expand Up @@ -177,12 +177,12 @@ The remote layout is defined using a series of nested arrays. The lowest level o
- search
```
The default keys and sources lists for your selected platform are displayed below the layout code editor. If you have configured any custom actions, they will be displayed above this. You can use this as reference as you create your remote, or drag and drop entries from these lists to the editor. The default keys list also includes the default touchpad, mousepad, and slider, along with some special elements for button pads and layouts. Not all special elements are available for all platforms.
The default keys and sources lists for your selected platform are displayed below the layout code editor. If you have configured any custom actions, they will be displayed above this. You can use this as reference as you create your remote, or drag and drop entries from these lists to the editor. The default keys list also includes the default touchpad and slider, along with some special elements for button pads and layouts. Not all special elements are available for all platforms.
| Name | Type | Description |
| ------------------ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| touchpad | touchpad | A touchpad for navigation. |
| mousepad | mousepad | A mousepad for mouse movement and scrolling. **NOTE**: Mousepad support is dependent on the platform support mouse control via its Home Assistant integration. |
| touchpad | touchpad | A touchpad for swipe navigation. |
| mousepad | touchpad | A touchpad for mouse navigation. |
| slider | slider | A slider that controls the volume of the entity defined by `media_player_id`. **NOTE**: Volume slider support is dependent on the media player supporting the `media_player.volume_set` action. |
| volume_buttons | button rows | Shorthand to generate a set of volume down, volume mute, and volume up buttons in a row or column. |
| navigation_buttons | button rows | Shorthand to generate a set of up, down, left, right, and center buttons across three rows within a column. |
Expand Down Expand Up @@ -230,11 +230,11 @@ Sliders have some additional general options. They have a range `Min` and `Max`

Sliders will wait one second before updating their internal values from Home Assistant to prevent it from bouncing between the old and new values. This time can be changed by setting `Update after action delay`, which defaults to 1000ms

### Touchpad and Mousepad Tabs
### Touchpad Tabs

<img src="https://raw.githubusercontent.com/Nerwyn/android-tv-card/main/assets/editor_actions_general_options_touchpad.png" alt="editor actions general options touchpad" width="600"/>

Touchpads and mousepads have five tabs at the top of their actions page for each direction and it's center. Only the center tab has general options as these apply to the entire touchpad remote element. Each direction and center have their own options for appearance and interactions (touchpad only) as described below.
Touchpads have five tabs at the top of their actions page for each direction and it's center. Only the center tab has general options as these apply to the entire touchpad remote element. Each direction and center have their own options for appearance and interactions as described below.

## Appearance

Expand Down Expand Up @@ -277,9 +277,9 @@ Sliders have an additional `Vertical` toggle which rotates it 90 degrees to make
}
```

### Multiple Icons and Labels for Touchpads and Mousepads
### Multiple Icons and Labels for Touchpads

Touchpads and mousepads can have a separate icon and label for the center and each direction. You can also style each of these icons and labels independently using their own `CSS Styles` fields. General touchpad and mousepad styles such as those for `toucharea` (mousepad also uses this element name) like height should go in the center tab styles.
Touchpads can have a separate icon and label for the center and each direction. You can also style each of these icons and labels independently using their own `CSS Styles` fields. General touchpad styles such as those for `toucharea` like height should go in the center tab styles.

### A Note on Templating

Expand All @@ -289,7 +289,7 @@ You can include the current value of a remote element and it's units by using th

## Interactions

There are three traditional ways to trigger an action - tap, double tap, and hold. Buttons, touch/mousepad center support all three, touchpad (not mousepad) swipes only support tap and hold actions, and sliders only support tap actions. Defining a double tap action that is not `none` introduces a 200ms delay to single tap actions.
There are three traditional ways to trigger an action - tap, double tap, and hold. Buttons, touchpad center support all three, touchpad swipes only support tap and hold actions, and sliders only support tap actions. Defining a double tap action that is not `none` introduces a 200ms delay to single tap actions.

<img src="https://raw.githubusercontent.com/Nerwyn/android-tv-card/main/assets/editor_actions_interactions.png" alt="editor actions interactions" width="600"/>

Expand Down Expand Up @@ -342,19 +342,14 @@ For momentary end actions you can include the number of seconds a button has bee

### Touchpad Actions

TODO Update config UI and make new image with mouse tab
<img src="https://raw.githubusercontent.com/Nerwyn/android-tv-card/main/assets/editor_actions_interactions_touchpad.png" alt="editor actions interactions touchpad" width="600"/>

The touchpad's center acts like a button, with support for the same actions. The touchpad's direction actions are activated when the user swipes in a direction, and do not support double tap actions or momentary mode.

Touchpads also support multi-touch mode, which fires alternate actions when more than one finger is used with it. This mode is disabled by default but can be enabled by setting a touchpad's multi-touch actions to something other than `Nothing`. Multi-touch mode supports center tap, double tap, and hold actions, and direction swipe and hold actions.

### Mousepad Actions

TODO config UI and image

Similar to the touchpad, the mousepad's center acts like a button with support for tap, double tap, and hold actions, but it does not support momentary mode. The center also supports multi-touch mode. This mode is enabled by default.

Instead of seprate direction actions, mousepads support a mouse action. This action is called whenever movement is detected on the mousepad, and is meant to be used with mouse movement actions like Unified Remote's `Core.Input MoveBy`. The mouse X and Y movement can be added to actions using templates using `deltaX` and `deltaY`. The mouse action can also be used in multi-touch mode.
Touchpads also support an alternate mouse mode. This action is called whenever movement is detected on the mousepad, and works best with mouse movement actions like Unified Remote's `Core.Input MoveBy`. The mouse X and Y movement can be added to actions using templates using `deltaX` and `deltaY`. The mouse action can also be used in multi-touch mode. Enabling this action disables directional actions.

### Keyboard, Textbox, and Search

Expand Down
6 changes: 3 additions & 3 deletions dist/universal-remote-card.min.js

Large diffs are not rendered by default.

70 changes: 63 additions & 7 deletions src/classes/base-remote-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class BaseRemoteElement extends LitElement {
fireMouseEvent?: boolean = true;

swiping?: boolean = false;
targetTouches?: Touch[];
initialX?: number;
initialY?: number;
currentX?: number;
Expand All @@ -68,6 +69,7 @@ export class BaseRemoteElement extends LitElement {
this.momentaryEnd = undefined;

this.swiping = false;
this.targetTouches = undefined;
this.initialX = undefined;
this.initialY = undefined;
this.currentX = undefined;
Expand Down Expand Up @@ -775,25 +777,79 @@ export class BaseRemoteElement extends LitElement {
: '';
}

// Skeletons for overridden event handlers
onStart(_e: MouseEvent | TouchEvent) {}
onEnd(_e: MouseEvent | TouchEvent) {}
onMove(_e: MouseEvent | TouchEvent) {}
setTargetTouches(e: TouchEvent) {
if (!this.targetTouches) {
this.targetTouches = Array.from(e.targetTouches ?? []);
} else {
for (const touch of e.targetTouches) {
const i = this.targetTouches.findIndex(
(t) => t.identifier == touch.identifier,
);
if (i >= 0) {
this.targetTouches[i] = touch;
} else {
this.targetTouches.push(touch);
}
}
}
}

onStart(e: TouchEvent | MouseEvent) {
if ('targetTouches' in e) {
let totalX = 0;
let totalY = 0;
this.setTargetTouches(e);
for (const touch of this.targetTouches ?? []) {
totalX += touch.clientX;
totalY += touch.clientY;
}
this.initialX = totalX / (this.targetTouches?.length ?? 1);
this.initialY = totalY / (this.targetTouches?.length ?? 1);
} else {
this.initialX = e.clientX;
this.initialY = e.clientY;
}
this.currentX = this.initialX;
this.currentY = this.initialY;
}

onEnd(e: TouchEvent | MouseEvent) {}

onMove(e: TouchEvent | MouseEvent) {
let currentX: number = 0;
let currentY: number = 0;
if ('targetTouches' in e) {
this.setTargetTouches(e);
for (const touch of this.targetTouches ?? []) {
currentX += touch.clientX;
currentY += touch.clientY;
}
currentX = currentX / (this.targetTouches?.length ?? 1);
currentY = currentY / (this.targetTouches?.length ?? 1);
} else {
currentX = e.clientX ?? 0;
currentY = e.clientY ?? 0;
}
this.deltaX = currentX - (this.currentX ?? currentX);
this.deltaY = currentY - (this.currentY ?? currentY);
this.currentX = currentX;
this.currentY = currentY;
}

@eventOptions({ passive: true })
onMouseDown(e: MouseEvent | TouchEvent) {
onMouseDown(e: TouchEvent | MouseEvent) {
if (this.fireMouseEvent) {
this.onStart(e);
}
}
onMouseUp(e: MouseEvent | TouchEvent) {
onMouseUp(e: TouchEvent | MouseEvent) {
if (this.fireMouseEvent) {
this.onEnd(e);
}
this.fireMouseEvent = true;
}
@eventOptions({ passive: true })
onMouseMove(e: MouseEvent | TouchEvent) {
onMouseMove(e: TouchEvent | MouseEvent) {
if (this.fireMouseEvent) {
this.onMove(e);
}
Expand Down
19 changes: 8 additions & 11 deletions src/classes/remote-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,9 @@ export class RemoteButton extends BaseRemoteElement {
}

onStart(e: TouchEvent | MouseEvent) {
super.onStart(e);
this.cancelRippleToggle();
this.swiping = false;
if ('targetTouches' in e) {
this.initialX = e.targetTouches[0].clientX;
this.initialY = e.targetTouches[0].clientY;
} else {
this.initialX = e.clientX;
this.initialY = e.clientY;
}

if (
this.renderTemplate(
Expand Down Expand Up @@ -159,14 +153,17 @@ export class RemoteButton extends BaseRemoteElement {
currentX = e.clientX;
currentY = e.clientY;
}

this.deltaX = currentX - (this.initialX ?? currentX);
this.deltaY = currentY - (this.initialY ?? currentY);
this.deltaX = currentX - (this.currentX ?? currentX);
this.deltaY = currentY - (this.currentY ?? currentY);
this.currentX = currentX;
this.currentY = currentY;

// Only consider significant enough movement
const sensitivity = 24;
const totalDeltaX = this.currentX - (this.initialX ?? 0);
const totalDeltaY = this.currentY - (this.initialY ?? 0);
if (
Math.abs(Math.abs(this.deltaX) - Math.abs(this.deltaY)) >
Math.abs(Math.abs(totalDeltaX) - Math.abs(totalDeltaY)) >
sensitivity
) {
this.endAction();
Expand Down
72 changes: 0 additions & 72 deletions src/classes/remote-mousepad.ts

This file was deleted.

33 changes: 10 additions & 23 deletions src/classes/remote-slider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ export class RemoteSlider extends BaseRemoteElement {
}
}

onStart(e: MouseEvent | TouchEvent) {
onStart(e: TouchEvent | MouseEvent) {
super.onStart(e);
const slider = e.currentTarget as HTMLInputElement;

if (!this.swiping) {
Expand All @@ -129,7 +130,7 @@ export class RemoteSlider extends BaseRemoteElement {
}
}

onEnd(_e: MouseEvent | TouchEvent) {
onEnd(_e: TouchEvent | MouseEvent) {
this.setThumbOffset();
this.showTooltip = false;
this.setValue();
Expand Down Expand Up @@ -157,29 +158,15 @@ export class RemoteSlider extends BaseRemoteElement {
this.resetGetValueFromHass();
}

onMove(e: MouseEvent | TouchEvent) {
onMove(e: TouchEvent | MouseEvent) {
if (!this.vertical) {
let currentX: number;
if ('clientX' in e) {
currentX = e.clientX;
} else {
currentX = e.touches[0].clientX;
}
let currentY: number;
if ('clientY' in e) {
currentY = e.clientY;
} else {
currentY = e.touches[0].clientY;
}
super.onMove(e);

if (this.initialY == undefined) {
this.initialY = currentY;
}
if (this.initialX == undefined) {
this.initialX = currentX;
} else if (
Math.abs(currentX - this.initialX) <
Math.abs(currentY - this.initialY) - 50
const sensitivity = 50;
if (
Math.abs((this.currentX ?? 0) - (this.initialX ?? 0)) <
Math.abs((this.currentY ?? 0) - (this.initialY ?? 0)) -
sensitivity
) {
this.swiping = true;
this.getValueFromHass = true;
Expand Down
Loading

0 comments on commit b20efa1

Please sign in to comment.