diff --git a/client/constants.js b/client/constants.js index 57c85f4e85..ab93a646be 100644 --- a/client/constants.js +++ b/client/constants.js @@ -70,6 +70,7 @@ export const SET_GRID_OUTPUT = 'SET_GRID_OUTPUT'; export const SET_SOUND_OUTPUT = 'SET_SOUND_OUTPUT'; export const SET_AUTOCLOSE_BRACKETS_QUOTES = 'SET_AUTOCLOSE_BRACKETS_QUOTES'; export const SET_AUTOCOMPLETE_HINTER = 'SET_AUTOCOMPLETE_HINTER'; +export const SET_COORDINATES = 'SET_COORDINATES'; export const OPEN_PROJECT_OPTIONS = 'OPEN_PROJECT_OPTIONS'; export const CLOSE_PROJECT_OPTIONS = 'CLOSE_PROJECT_OPTIONS'; diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index 80a43443bc..a995fd25f9 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -260,7 +260,9 @@ export function startSketch() { files: state.files, basePath: window.location.pathname, gridOutput: state.preferences.gridOutput, - textOutput: state.preferences.textOutput + textOutput: state.preferences.textOutput, + userTheme: state.preferences.theme, + coordinates: state.preferences.coordinates } }); dispatchMessage({ diff --git a/client/modules/IDE/actions/preferences.js b/client/modules/IDE/actions/preferences.js index ebaefd1625..74b8e35b71 100644 --- a/client/modules/IDE/actions/preferences.js +++ b/client/modules/IDE/actions/preferences.js @@ -1,6 +1,7 @@ import i18next from 'i18next'; import apiClient from '../../../utils/apiClient'; import * as ActionTypes from '../../../constants'; +import { dispatchMessage, MessageTypes } from '../../../utils/dispatcher'; function updatePreferences(formParams, dispatch) { apiClient @@ -14,6 +15,30 @@ function updatePreferences(formParams, dispatch) { }); } +export function setCoordinates(value) { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.SET_COORDINATES, + value + }); + + dispatchMessage({ + type: MessageTypes.COORDINATES_VISIBILITY, + payload: value + }); + + const state = getState(); + if (state.user.authenticated) { + const formParams = { + preferences: { + coordinates: value + } + }; + updatePreferences(formParams, dispatch); + } + }; +} + export function setPreferencesTab(value) { return { type: ActionTypes.SET_PREFERENCES_TAB, diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx index 3a21cca8d0..d55aabba2e 100644 --- a/client/modules/IDE/components/Preferences/index.jsx +++ b/client/modules/IDE/components/Preferences/index.jsx @@ -19,6 +19,7 @@ import { setAutocloseBracketsQuotes, setAutocompleteHinter, setLinewrap, + setCoordinates, setPreferencesTab } from '../../actions/preferences'; import { p5SoundURL, p5URL, useP5Version } from '../../hooks/useP5Version'; @@ -39,6 +40,7 @@ export default function Preferences() { fontSize, autosave, linewrap, + coordinates, lineNumbers, lintWarning, textOutput, @@ -411,6 +413,40 @@ export default function Preferences() { +
+

+ {t('Preferences.Coordinates')} +

+
+ dispatch(setCoordinates(true))} + aria-label={t('Preferences.CoordinatesOnARIA')} + name="coordinates" + id="coordinates-on" + className="preference__radio-button" + value="On" + checked={coordinates === true} + /> + + dispatch(setCoordinates(false))} + aria-label={t('Preferences.CoordinatesOffARIA')} + name="coordinates" + id="coordinates-off" + className="preference__radio-button" + value="Off" + checked={coordinates === false} + /> + +
+
diff --git a/client/modules/IDE/reducers/preferences.js b/client/modules/IDE/reducers/preferences.js index 630fa465ef..f91697b5ee 100644 --- a/client/modules/IDE/reducers/preferences.js +++ b/client/modules/IDE/reducers/preferences.js @@ -13,7 +13,8 @@ export const initialState = { autorefresh: false, language: 'en-US', autocloseBracketsQuotes: true, - autocompleteHinter: false + autocompleteHinter: false, + coordinates: false }; const preferences = (state = initialState, action) => { @@ -52,6 +53,10 @@ const preferences = (state = initialState, action) => { return Object.assign({}, state, { autocompleteHinter: action.value }); + case ActionTypes.SET_COORDINATES: + return Object.assign({}, state, { + coordinates: action.value + }); default: return state; } diff --git a/client/modules/Preview/CoordinateTracker.jsx b/client/modules/Preview/CoordinateTracker.jsx new file mode 100644 index 0000000000..a7148f81ea --- /dev/null +++ b/client/modules/Preview/CoordinateTracker.jsx @@ -0,0 +1,79 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { remSize } from '../../theme'; + +const CoordContainer = styled.div` + z-index: 1000; + padding: ${remSize(0.1)}; + // border-bottom: ${remSize(1)} dashed #a6a6a6; + margin-bottom: ${remSize(4)}; + + p { + font-size: ${remSize(9.5)}; + padding: 0 0 ${remSize(3.5)} ${remSize(3.5)}; + margin: 0; + font-family: Inconsolata, monospace; + font-weight: light; + color: ${(props) => props.theme.Button.primary.default.foreground}; + } + + @media (max-width: 550px) { + // border-bottom: none; + margin-top: ${remSize(10)}; + } +`; + +const CoordinateTracker = ({ isPlaying, sketchReloaded }) => { + const [coordinates, setCoordinates] = useState({ x: 0, y: 0 }); + + useEffect(() => { + let canvas; + let mouseMoveHandler; + + const timeout = setTimeout(() => { + const iFrame = document.getElementById('previewIframe0'); + canvas = iFrame?.contentWindow?.document?.getElementById( + 'defaultCanvas0' + ); + + if (!canvas) { + console.warn('Canvas not found.'); + return; + } + + mouseMoveHandler = (event) => { + const rect = canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + setCoordinates({ x, y }); + }; + + canvas.addEventListener('mousemove', mouseMoveHandler); + }, 500); + + return () => { + clearTimeout(timeout); + + if (canvas && mouseMoveHandler) { + canvas.removeEventListener('mousemove', mouseMoveHandler); + } + }; + }, [isPlaying, sketchReloaded]); + + return ( + +

+ Mouse X: {isPlaying ? coordinates.x : 0} Mouse Y:{' '} + {isPlaying ? coordinates.y : 0} +

+
+ ); +}; + +CoordinateTracker.propTypes = { + isPlaying: PropTypes.bool.isRequired, + sketchReloaded: PropTypes.bool.isRequired +}; + +export default CoordinateTracker; diff --git a/client/modules/Preview/EmbedFrame.jsx b/client/modules/Preview/EmbedFrame.jsx index fa01604ab7..ca4f811629 100644 --- a/client/modules/Preview/EmbedFrame.jsx +++ b/client/modules/Preview/EmbedFrame.jsx @@ -302,6 +302,7 @@ function EmbedFrame({ files, isPlaying, basePath, gridOutput, textOutput }) { useEffect(renderSketch, [files, isPlaying]); return ( { const [basePath, setBasePath] = useState(''); const [textOutput, setTextOutput] = useState(false); const [gridOutput, setGridOutput] = useState(false); + const [userTheme, setUserTheme] = useState('light'); + const [coordinatesVisible, setCoordinatesVisible] = useState(false); + const [sketchReloaded, setSketchReloaded] = useState(0); + registerFrame(window.parent, getConfig('EDITOR_URL')); function handleMessageEvent(message) { @@ -34,12 +40,16 @@ const App = () => { setBasePath(payload.basePath); setTextOutput(payload.textOutput); setGridOutput(payload.gridOutput); + setUserTheme(payload.userTheme); + setCoordinatesVisible(payload.coordinates); + setSketchReloaded((prev) => prev + 1); break; case MessageTypes.START: setIsPlaying(true); break; case MessageTypes.STOP: setIsPlaying(false); + setCoordinatesVisible(false); break; case MessageTypes.REGISTER: dispatchMessage({ type: MessageTypes.REGISTER }); @@ -47,6 +57,9 @@ const App = () => { case MessageTypes.EXECUTE: dispatchMessage(payload); break; + case MessageTypes.COORDINATES_VISIBILITY: + if (isPlaying) setCoordinatesVisible(payload); + break; default: break; } @@ -65,23 +78,35 @@ const App = () => { }); } + const memoizedFiles = useMemo(() => addCacheBustingToAssets(state), [ + state, + addCacheBustingToAssets + ]); + useEffect(() => { const unsubscribe = listen(handleMessageEvent); return function cleanup() { unsubscribe(); }; - }); + }, []); + return ( - + + {coordinatesVisible && ( + + )} - + ); }; diff --git a/client/testData/testServerResponses.js b/client/testData/testServerResponses.js index 9675b005b6..d8b240719e 100644 --- a/client/testData/testServerResponses.js +++ b/client/testData/testServerResponses.js @@ -19,7 +19,8 @@ export const userResponse = { autorefresh: false, language: 'en-US', autocloseBracketsQuotes: true, - autocompleteHinter: false + autocompleteHinter: false, + coordinates: false }, apiKeys: [], verified: 'verified', diff --git a/client/utils/dispatcher.js b/client/utils/dispatcher.js index 49393121ac..0eb83ebef7 100644 --- a/client/utils/dispatcher.js +++ b/client/utils/dispatcher.js @@ -11,7 +11,9 @@ export const MessageTypes = { FILES: 'FILES', SKETCH: 'SKETCH', REGISTER: 'REGISTER', - EXECUTE: 'EXECUTE' + EXECUTE: 'EXECUTE', + COORDINATES: 'COORDINATES', + COORDINATES_VISIBILITY: 'COORDINATES_VISIBILITY' }; export function registerFrame(newFrame, newOrigin) { diff --git a/server/models/user.js b/server/models/user.js index b825971747..ccee359764 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -74,7 +74,8 @@ const userSchema = new Schema( autorefresh: { type: Boolean, default: false }, language: { type: String, default: 'en-US' }, autocloseBracketsQuotes: { type: Boolean, default: true }, - autocompleteHinter: { type: Boolean, default: false } + autocompleteHinter: { type: Boolean, default: false }, + coordinates: { type: Boolean, default: false } }, totalSize: { type: Number, default: 0 }, cookieConsent: { diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json index 59e6bcbaad..129ceefaa5 100644 --- a/translations/locales/en-US/translations.json +++ b/translations/locales/en-US/translations.json @@ -203,6 +203,9 @@ "WordWrap": "Word Wrap", "LineWrapOnARIA": "linewrap on", "LineWrapOffARIA": "linewrap off", + "Coordinates": "Coordinates", + "CoordinatesOnARIA": "coordinates on", + "CoordinatesOffARIA": "coordinates off", "LineNumbers": "Line numbers", "LineNumbersOnARIA": "line numbers on", "LineNumbersOffARIA": "line numbers off",