diff --git a/client/constants.js b/client/constants.js
index 72f8948c..ef4f9041 100644
--- a/client/constants.js
+++ b/client/constants.js
@@ -84,6 +84,12 @@ export const SET_TOAST_TEXT = 'SET_TOAST_TEXT';
export const SET_THEME = 'SET_THEME';
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
+export const SET_AUTOREFRESH = 'SET_AUTOREFRESH';
+export const START_SKETCH_REFRESH = 'START_SKETCH_REFRESH';
+export const END_SKETCH_REFRESH = 'END_SKETCH_REFRESH';
+
+export const DETECT_INFINITE_LOOPS = 'DETECT_INFINITE_LOOPS';
+export const RESET_INFINITE_LOOPS = 'RESET_INFINITE_LOOPS';
// eventually, handle errors more specifically and better
export const ERROR = 'ERROR';
diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js
index 05725fca..19cb940a 100644
--- a/client/modules/IDE/actions/ide.js
+++ b/client/modules/IDE/actions/ide.js
@@ -1,11 +1,5 @@
import * as ActionTypes from '../../../constants';
-export function toggleSketch() {
- return {
- type: ActionTypes.TOGGLE_SKETCH
- };
-}
-
export function startSketch() {
return {
type: ActionTypes.START_SKETCH
@@ -18,6 +12,25 @@ export function stopSketch() {
};
}
+export function startRefreshSketch() {
+ return {
+ type: ActionTypes.START_SKETCH_REFRESH
+ };
+}
+
+export function startSketchAndRefresh() {
+ return (dispatch) => {
+ dispatch(startSketch());
+ dispatch(startRefreshSketch());
+ };
+}
+
+export function endSketchRefresh() {
+ return {
+ type: ActionTypes.END_SKETCH_REFRESH
+ };
+}
+
export function startTextOutput() {
return {
type: ActionTypes.START_TEXT_OUTPUT
@@ -170,3 +183,14 @@ export function setUnsavedChanges(value) {
};
}
+export function detectInfiniteLoops() {
+ return {
+ type: ActionTypes.DETECT_INFINITE_LOOPS
+ };
+}
+
+export function resetInfiniteLoops() {
+ return {
+ type: ActionTypes.RESET_INFINITE_LOOPS
+ };
+}
diff --git a/client/modules/IDE/actions/preferences.js b/client/modules/IDE/actions/preferences.js
index 6b3964ef..5d8bc4b1 100644
--- a/client/modules/IDE/actions/preferences.js
+++ b/client/modules/IDE/actions/preferences.js
@@ -138,9 +138,46 @@ export function setTextOutput(value) {
}
export function setTheme(value) {
- return {
- type: ActionTypes.SET_THEME,
- value
+ // return {
+ // type: ActionTypes.SET_THEME,
+ // value
+ // };
+ return (dispatch, getState) => {
+ dispatch({
+ type: ActionTypes.SET_THEME,
+ value
+ });
+ const state = getState();
+ if (state.user.authenticated) {
+ const formParams = {
+ preferences: {
+ theme: value
+ }
+ };
+ updatePreferences(formParams, dispatch);
+ }
+ };
+}
+
+export function setAutorefresh(value) {
+ // return {
+ // type: ActionTypes.SET_AUTOREFRESH,
+ // value
+ // };
+ return (dispatch, getState) => {
+ dispatch({
+ type: ActionTypes.SET_AUTOREFRESH,
+ value
+ });
+ const state = getState();
+ if (state.user.authenticated) {
+ const formParams = {
+ preferences: {
+ autorefresh: value
+ }
+ };
+ updatePreferences(formParams, dispatch);
+ }
};
}
diff --git a/client/modules/IDE/components/About.js b/client/modules/IDE/components/About.js
index 0959ffe5..534c6642 100644
--- a/client/modules/IDE/components/About.js
+++ b/client/modules/IDE/components/About.js
@@ -4,13 +4,17 @@ const exitUrl = require('../../../images/exit.svg');
import { browserHistory } from 'react-router';
class About extends React.Component {
+ componentDidMount() {
+ this.refs.about.focus();
+ }
+
closeAboutModal() {
browserHistory.goBack();
}
render() {
return (
-
+
About
-
+
);
}
}
diff --git a/client/modules/IDE/components/Editor.js b/client/modules/IDE/components/Editor.js
index ae8c415b..f62cb4a4 100644
--- a/client/modules/IDE/components/Editor.js
+++ b/client/modules/IDE/components/Editor.js
@@ -15,6 +15,7 @@ import 'codemirror/addon/lint/html-lint';
import 'codemirror/addon/comment/comment';
import 'codemirror/keymap/sublime';
import 'codemirror/addon/search/jump-to-line';
+
import { JSHINT } from 'jshint';
window.JSHINT = JSHINT;
import { CSSLint } from 'csslint';
@@ -27,15 +28,16 @@ const downArrowUrl = require('../../../images/down-arrow.svg');
import classNames from 'classnames';
import { debounce } from 'throttle-debounce';
+import loopProtect from 'loop-protect';
class Editor extends React.Component {
constructor(props) {
super(props);
this.tidyCode = this.tidyCode.bind(this);
}
-
componentDidMount() {
this.beep = new Audio(beepUrl);
+ this.widgets = [];
this._cm = CodeMirror(this.refs.container, { // eslint-disable-line
theme: `p5-${this.props.theme}`,
value: this.props.file.content,
@@ -47,23 +49,30 @@ class Editor extends React.Component {
gutters: ['CodeMirror-lint-markers'],
keyMap: 'sublime',
lint: {
- onUpdateLinting: debounce(2000, (annotations) => {
- this.props.clearLintMessage();
- annotations.forEach((x) => {
- if (x.from.line > -1) {
- this.props.updateLintMessage(x.severity, (x.from.line + 1), x.message);
+ onUpdateLinting: () => {
+ debounce(2000, (annotations) => {
+ this.props.clearLintMessage();
+ annotations.forEach((x) => {
+ if (x.from.line > -1) {
+ this.props.updateLintMessage(x.severity, (x.from.line + 1), x.message);
+ }
+ });
+ if (this.props.lintMessages.length > 0 && this.props.lintWarning) {
+ this.beep.play();
}
});
- if (this.props.lintMessages.length > 0 && this.props.lintWarning) {
- this.beep.play();
- }
- })
+ }
}
});
- this._cm.on('change', debounce(200, () => {
+ this._cm.on('change', debounce(1000, () => {
this.props.setUnsavedChanges(true);
this.props.updateFileContent(this.props.file.name, this._cm.getValue());
+ this.checkForInfiniteLoop((infiniteLoop, prevs) => {
+ if (!infiniteLoop && prevs && this.props.autorefresh) {
+ this.props.startRefreshSketch();
+ }
+ });
}));
this._cm.on('keyup', () => {
@@ -132,6 +141,77 @@ class Editor extends React.Component {
}
}
+ checkForInfiniteLoop(callback) {
+ const prevIsplaying = this.props.isPlaying;
+ let infiniteLoop = false;
+ let prevLine;
+ this.props.resetInfiniteLoops();
+ let iframe;
+
+ for (let i = 0; i < this.widgets.length; ++i) {
+ this._cm.removeLineWidget(this.widgets[i]);
+ }
+ this.widgets.length = 0;
+
+ loopProtect.alias = 'protect';
+
+ let foundInfiniteLoop = false;
+ loopProtect.hit = (line) => {
+ foundInfiniteLoop = true;
+ if (line !== prevLine) {
+ this.props.detectInfiniteLoops();
+ this.props.stopSketch();
+ infiniteLoop = true;
+ callback(infiniteLoop, prevIsplaying);
+ const msg = document.createElement('div');
+ const loopError = `line ${line}: This loop is taking too long to run. This might be an infinite loop.`;
+ msg.appendChild(document.createTextNode(loopError));
+ msg.className = 'lint-error';
+ this.widgets.push(this._cm.addLineWidget(line - 1, msg, { coverGutter: false, noHScroll: true }));
+ prevLine = line;
+ }
+ };
+
+ const processed = loopProtect(this.props.file.content);
+
+ let iframeForLoop = document.getElementById('iframeForLoop');
+ if (iframeForLoop === null) {
+ iframe = document.createElement('iframe');
+ iframe.id = 'iframeForLoop';
+ iframe.style.display = 'none';
+ document.body.appendChild(iframe);
+ iframeForLoop = iframe;
+ } else {
+ iframeForLoop.srcdoc = '';
+ }
+ const win = iframeForLoop.contentWindow;
+ const doc = win.document;
+ doc.open();
+
+ win.protect = loopProtect;
+
+ doc.write(`
+
+
+
+
+
+
+
+
+ `);
+ win.onerror = () => true;
+ doc.close();
+
+ setTimeout(() => {
+ if (!foundInfiniteLoop) {
+ callback(infiniteLoop, prevIsplaying, prevLine);
+ }
+ }, 200);
+ }
+
_cm: CodeMirror.Editor
render() {
@@ -148,6 +228,7 @@ class Editor extends React.Component {
>
-
+
-
Tidy
@@ -196,7 +277,14 @@ Editor.propTypes = {
closeEditorOptions: PropTypes.func.isRequired,
showKeyboardShortcutModal: PropTypes.func.isRequired,
setUnsavedChanges: PropTypes.func.isRequired,
+ infiniteLoop: PropTypes.bool.isRequired,
+ detectInfiniteLoops: PropTypes.func.isRequired,
+ resetInfiniteLoops: PropTypes.func.isRequired,
+ startRefreshSketch: PropTypes.func.isRequired,
+ autorefresh: PropTypes.bool.isRequired,
+ isPlaying: PropTypes.bool.isRequired,
theme: PropTypes.string.isRequired,
+ stopSketch: PropTypes.func.isRequired
};
export default Editor;
diff --git a/client/modules/IDE/components/KeyboardShortcutModal.js b/client/modules/IDE/components/KeyboardShortcutModal.js
index b0c05553..48d2fd03 100644
--- a/client/modules/IDE/components/KeyboardShortcutModal.js
+++ b/client/modules/IDE/components/KeyboardShortcutModal.js
@@ -28,17 +28,34 @@ class KeyboardShortcutModal extends React.Component {
Save
-
- Command + [
- Indent Code Right
-
- -
- Command + ]
+
+ {this.isMac ? 'Command + [' : 'Control + ['}
+
Indent Code Left
-
- Command + /
+
+ {this.isMac ? 'Command + ]' : 'Control + ]'}
+
+ Indent Code Right
+
+ -
+
+ {this.isMac ? 'Command + /' : 'Control + /'}
+
Comment Line
+ -
+
+ {this.isMac ? 'Command + Enter' : 'Control + Enter'}
+ Start Sketch
+
+ -
+
+ {this.isMac ? 'Command + Shift + Enter' : 'Control + Shift + Enter'}
+
+ Stop Sketch
+
);
diff --git a/client/modules/IDE/components/PreviewFrame.js b/client/modules/IDE/components/PreviewFrame.js
index 16545376..5dd60e0e 100644
--- a/client/modules/IDE/components/PreviewFrame.js
+++ b/client/modules/IDE/components/PreviewFrame.js
@@ -40,8 +40,29 @@ function hijackConsoleLogsScript() {
'debug', 'clear', 'error', 'info', 'log', 'warn'
];
+
+ function throttle(fn, threshhold, scope) {
+ var last, deferTimer;
+ return function() {
+ var context = scope || this;
+ var now = +new Date,
+ args = arguments;
+ if (last && now < last + threshhold) {
+ // hold on to it
+ clearTimeout(deferTimer);
+ deferTimer = setTimeout(function() {
+ last = now;
+ fn.apply(context, args);
+ }, threshhold);
+ } else {
+ last = now;
+ fn.apply(context, args);
+ }
+ };
+ }
+
methods.forEach( function(method) {
- iframeWindow.console[method] = function() {
+ iframeWindow.console[method] = throttle(function() {
originalConsole[method].apply(originalConsole, arguments);
var args = Array.from(arguments);
@@ -56,11 +77,12 @@ function hijackConsoleLogsScript() {
arguments: args,
source: 'sketch'
}, '*');
- };
+ }, 250);
});
`;
return s;
}
+
function hijackConsoleErrorsScript(offs) {
const s = `