Skip to content

Commit a0396f6

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

3 files changed

Lines changed: 63 additions & 63 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' ||
@@ -2347,13 +2349,11 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
23472349
tutorials,
23482350
hotReloadPreviewButtonProps,
23492351
windowSize,
2352+
screenType,
23502353
highlightedAiGeneratedEventIds,
23512354
} = this.props;
23522355
if (!project) return null;
23532356

2354-
// eslint-disable-next-line react-hooks/rules-of-hooks
2355-
const screenType = useScreenType();
2356-
23572357
const isFunctionOnlyCallingItself =
23582358
scope.eventsFunctionsExtension &&
23592359
scope.eventsFunction &&
@@ -2884,6 +2884,7 @@ const EventsSheet = (props, ref) => {
28842884
const leaderboardsManager = React.useContext(LeaderboardContext);
28852885
const { windowSize } = useResponsiveWindowSize();
28862886
const shortcutMap = useShortcutMap();
2887+
const screenType = useScreenType();
28872888
return (
28882889
<EventsSheetComponentWithoutHandle
28892890
ref={component}
@@ -2893,6 +2894,7 @@ const EventsSheet = (props, ref) => {
28932894
leaderboardsManager={leaderboardsManager}
28942895
shortcutMap={shortcutMap}
28952896
windowSize={windowSize}
2897+
screenType={screenType}
28962898
highlightedAiGeneratedEventIds={highlightedAiGeneratedEventIds}
28972899
{...props}
28982900
/>

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

Lines changed: 17 additions & 11 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, portalContainer }) => {
5164
// The invisible backdrop behind the submenu is either:
@@ -132,8 +145,6 @@ const SubMenuItem = ({ item, buildFromTemplate, portalContainer }) => {
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 (
@@ -226,9 +237,6 @@ export default class MaterialUIMenuImplementation
226237
template: Array<MenuItemTemplate>,
227238
forceUpdate?: () => void
228239
): any {
229-
// This is not a real hook.
230-
// eslint-disable-next-line react-hooks/rules-of-hooks
231-
const isTouchscreen = useScreenType() === 'touch';
232240
const portalContainer = this._portalContainer;
233241

234242
return template
@@ -243,8 +251,7 @@ export default class MaterialUIMenuImplementation
243251
return <Divider key={'separator' + id} style={styles.divider} />;
244252
} else if (item.type === 'checkbox') {
245253
return (
246-
<MenuItem
247-
dense={!!electron || !isTouchscreen}
254+
<DenseMenuItem
248255
key={'checkbox' + item.label}
249256
checked={
250257
// $FlowFixMe[incompatible-type] - existence should be inferred by Flow.
@@ -282,7 +289,7 @@ export default class MaterialUIMenuImplementation
282289
)}
283290
</ListItemIcon>
284291
<ListItemText primary={item.label} />
285-
</MenuItem>
292+
</DenseMenuItem>
286293
);
287294
} else if (item.submenu) {
288295
return (
@@ -297,8 +304,7 @@ export default class MaterialUIMenuImplementation
297304
);
298305
} else {
299306
return (
300-
<MenuItem
301-
dense={!!electron || !isTouchscreen}
307+
<DenseMenuItem
302308
key={'item' + item.label}
303309
disabled={item.enabled === false}
304310
onClick={e => {
@@ -322,7 +328,7 @@ export default class MaterialUIMenuImplementation
322328
<span style={styles.accelerator}>{accelerator}</span>
323329
</div>
324330
)}
325-
</MenuItem>
331+
</DenseMenuItem>
326332
);
327333
}
328334
})

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)