Skip to content

Commit 359cd40

Browse files
Handle hybrid setups (mouse/touchscreen) so GDevelop can be controlled both ways
1 parent e9b9cb1 commit 359cd40

3 files changed

Lines changed: 63 additions & 64 deletions

File tree

newIDE/app/src/EventsSheet/index.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ import {
9595
hasClipboardConditions,
9696
pasteInstructionsFromClipboardInInstructionsList,
9797
} from './ClipboardKind';
98-
import { useScreenType } from '../UI/Responsive/ScreenTypeMeasurer';
98+
import {
99+
useScreenType,
100+
type ScreenType,
101+
} from '../UI/Responsive/ScreenTypeMeasurer';
99102
import {
100103
type WindowSizeType,
101104
useResponsiveWindowSize,
@@ -176,6 +179,7 @@ type Props = {|
176179
type ComponentProps = {|
177180
...Props,
178181
windowSize: WindowSizeType,
182+
screenType: ScreenType,
179183
authenticatedUser: AuthenticatedUser,
180184
preferences: Preferences,
181185
tutorials: ?Array<Tutorial>,
@@ -734,9 +738,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
734738
}
735739
);
736740

737-
// This is not a real hook.
738-
// eslint-disable-next-line react-hooks/rules-of-hooks
739-
const screenType = useScreenType();
741+
const screenType = this.props.screenType;
740742
if (
741743
screenType !== 'touch' &&
742744
(type === 'BuiltinCommonInstructions::Comment' ||
@@ -2323,13 +2325,11 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
23232325
tutorials,
23242326
hotReloadPreviewButtonProps,
23252327
windowSize,
2328+
screenType,
23262329
highlightedAiGeneratedEventIds,
23272330
} = this.props;
23282331
if (!project) return null;
23292332

2330-
// eslint-disable-next-line react-hooks/rules-of-hooks
2331-
const screenType = useScreenType();
2332-
23332333
const isFunctionOnlyCallingItself =
23342334
scope.eventsFunctionsExtension &&
23352335
scope.eventsFunction &&
@@ -2860,6 +2860,7 @@ const EventsSheet = (props, ref) => {
28602860
const leaderboardsManager = React.useContext(LeaderboardContext);
28612861
const { windowSize } = useResponsiveWindowSize();
28622862
const shortcutMap = useShortcutMap();
2863+
const screenType = useScreenType();
28632864
return (
28642865
<EventsSheetComponentWithoutHandle
28652866
ref={component}
@@ -2869,6 +2870,7 @@ const EventsSheet = (props, ref) => {
28692870
leaderboardsManager={leaderboardsManager}
28702871
shortcutMap={shortcutMap}
28712872
windowSize={windowSize}
2873+
screenType={screenType}
28722874
highlightedAiGeneratedEventIds={highlightedAiGeneratedEventIds}
28732875
{...props}
28742876
/>

newIDE/app/src/UI/Menu/MaterialUIMenuImplementation.js

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ const styles = {
4646
},
4747
};
4848

49+
// MenuItem whose `dense` prop adapts to the current screen type.
50+
// Defined as a function component so useScreenType() can be called properly.
51+
const DenseMenuItem = React.forwardRef<any, any>((props, ref) => {
52+
const screenType = useScreenType();
53+
return (
54+
<MenuItem
55+
dense={!!electron || screenType !== 'touch'}
56+
ref={ref}
57+
{...props}
58+
/>
59+
);
60+
});
61+
4962
// $FlowFixMe[missing-local-annot]
5063
const SubMenuItem = ({ item, buildFromTemplate }) => {
5164
// The invisible backdrop behind the submenu is either:
@@ -132,8 +145,6 @@ const SubMenuItem = ({ item, buildFromTemplate }) => {
132145
}, 75);
133146
}
134147

135-
// This is not a real hook.
136-
// eslint-disable-next-line react-hooks/rules-of-hooks
137148
const isTouchscreen = useScreenType() === 'touch';
138149

139150
return (
@@ -217,10 +228,6 @@ export default class MaterialUIMenuImplementation
217228
template: Array<MenuItemTemplate>,
218229
forceUpdate?: () => void
219230
): any {
220-
// This is not a real hook.
221-
// eslint-disable-next-line react-hooks/rules-of-hooks
222-
const isTouchscreen = useScreenType() === 'touch';
223-
224231
return template
225232
.map((item, id) => {
226233
if (item.visible === false) return null;
@@ -233,8 +240,7 @@ export default class MaterialUIMenuImplementation
233240
return <Divider key={'separator' + id} style={styles.divider} />;
234241
} else if (item.type === 'checkbox') {
235242
return (
236-
<MenuItem
237-
dense={!!electron || !isTouchscreen}
243+
<DenseMenuItem
238244
key={'checkbox' + item.label}
239245
checked={
240246
// $FlowFixMe[incompatible-type] - existence should be inferred by Flow.
@@ -272,7 +278,7 @@ export default class MaterialUIMenuImplementation
272278
)}
273279
</ListItemIcon>
274280
<ListItemText primary={item.label} />
275-
</MenuItem>
281+
</DenseMenuItem>
276282
);
277283
} else if (item.submenu) {
278284
return (
@@ -286,8 +292,7 @@ export default class MaterialUIMenuImplementation
286292
);
287293
} else {
288294
return (
289-
<MenuItem
290-
dense={!!electron || !isTouchscreen}
295+
<DenseMenuItem
291296
key={'item' + item.label}
292297
disabled={item.enabled === false}
293298
onClick={e => {
@@ -311,7 +316,7 @@ export default class MaterialUIMenuImplementation
311316
<span style={styles.accelerator}>{accelerator}</span>
312317
</div>
313318
)}
314-
</MenuItem>
319+
</DenseMenuItem>
315320
);
316321
}
317322
})

newIDE/app/src/UI/Responsive/ScreenTypeMeasurer.js

Lines changed: 37 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,25 @@ import * as React from 'react';
33

44
export type ScreenType = 'normal' | 'touch';
55

6-
let userHasTouchedScreen = false;
7-
let userHasMovedMouse = false;
6+
// Module-level state shared across all hook instances.
7+
// A single pointerdown listener detects the current input type and only
8+
// notifies subscribers when the type actually changes (touch ↔ mouse/pen).
9+
let _screenType: ScreenType = 'normal';
10+
const _listeners: Set<(type: ScreenType) => void> = new Set();
811

912
if (typeof window !== 'undefined') {
10-
window.addEventListener(
11-
'touchstart',
12-
function onFirstTouch() {
13-
console.info('Touch detected, considering the screen as touch enabled.');
14-
userHasTouchedScreen = true;
15-
window.removeEventListener('touchstart', onFirstTouch, false);
16-
},
17-
false
18-
);
19-
20-
// An event listener is added (and then removed at the first event triggering) and
21-
// will determine if the user is on a device that uses a mouse.
22-
// If the first pointermove event is not triggered by a mouse move, the device
23-
// will never be considered as mouse-enabled.
24-
// Note: mousemove cannot be used since browsers emulate the mouse movement when
25-
// the screen is touched.
26-
window.addEventListener(
27-
'pointermove',
28-
function onPointerMove(event: PointerEvent) {
29-
console.info('Pointer move detected.');
30-
if (event.pointerType === 'mouse') {
31-
console.info(
32-
'Pointer type is mouse, considering the device is a desktop/laptop computer.'
33-
);
34-
userHasMovedMouse = true;
35-
}
36-
window.removeEventListener('pointermove', onPointerMove, false);
37-
},
38-
false
39-
);
13+
window.addEventListener('pointerdown', (event: PointerEvent) => {
14+
const newType: ScreenType =
15+
event.pointerType === 'touch' ? 'touch' : 'normal';
16+
if (newType === _screenType) return;
17+
console.info(
18+
`Screen type changed from "${_screenType}" to "${newType}" (pointerType: "${
19+
event.pointerType
20+
}").`
21+
);
22+
_screenType = newType;
23+
_listeners.forEach(fn => fn(newType));
24+
});
4025
}
4126

4227
type Props = {|
@@ -50,21 +35,28 @@ export const ScreenTypeMeasurer = ({ children }: Props): React.Node =>
5035
children(useScreenType());
5136

5237
/**
53-
* Return if the screen is a touchscreen or not.
38+
* Returns whether the screen is currently being used as a touchscreen or not.
39+
* Dynamically switches when the user alternates between touch and mouse/pen,
40+
* so hybrid devices (e.g. Windows touchscreen laptops) are handled correctly.
5441
*/
5542
export const useScreenType = (): ScreenType => {
56-
// Note: this is not a React hook but is named as one to encourage
57-
// components to use it as such, so that it could be reworked
58-
// at some point to use a context (verify in this case all usages).
59-
if (typeof window === 'undefined') return 'normal';
43+
const [screenType, setScreenType] = React.useState<ScreenType>(_screenType);
6044

61-
return userHasTouchedScreen ? 'touch' : 'normal';
62-
};
45+
React.useEffect(() => {
46+
// setScreenType is stable across renders, safe to store in the Set.
47+
_listeners.add(setScreenType);
48+
return () => {
49+
_listeners.delete(setScreenType);
50+
};
51+
}, []);
6352

64-
export const useShouldAutofocusInput = (): boolean => {
65-
const isTouchscreen = useScreenType() === 'touch';
66-
// Whatever size the screen is, if a touch event has been detected, no autofocus should
67-
// be triggered (that would annoyingly open the keyboard) unless a mouse move has been
68-
// detected (in that case, the device should be a touch-enabled desktop/laptop computer).
69-
return !(isTouchscreen && !userHasMovedMouse);
53+
return screenType;
7054
};
55+
56+
/**
57+
* Returns true if inputs should be auto-focused.
58+
* No autofocus when the last interaction was touch, to avoid opening the
59+
* on-screen keyboard unexpectedly.
60+
*/
61+
export const useShouldAutofocusInput = (): boolean =>
62+
useScreenType() !== 'touch';

0 commit comments

Comments
 (0)