Make newIDE context menus dynamic (greyed-out items if not applicable, checkbox for grid)

This commit is contained in:
Florian Rival
2017-12-23 13:51:36 +01:00
parent 8c9214ce5e
commit 2e17b16516
18 changed files with 137 additions and 80 deletions

View File

@@ -2,11 +2,16 @@
//TODO: These types could be generated from GDevelop.js instead of being
//manually written here.
declare type gdProject = Object;
declare type gdLayout = Object;
declare type gdExternalLayout = Object;
declare type gdExternalEvents = Object;
declare type gdSerializerElement = Object;
type EmscriptenObject = {
ptr: Number
};
declare type gdProject = EmscriptenObject;
declare type gdLayout = EmscriptenObject;
declare type gdExternalLayout = EmscriptenObject;
declare type gdExternalEvents = EmscriptenObject;
declare type gdSerializerElement = EmscriptenObject;
declare type gdInitialInstance = EmscriptenObject;
//Represents all objects that have serializeTo and unserializeFrom methods.
declare type gdSerializable = Object;
declare type gdSerializable = EmscriptenObject;

View File

@@ -70,12 +70,13 @@ export class Toolbar extends PureComponent {
tooltip={t('Choose and add an event')}
/>
}
menuTemplate={this.allEventsMetadata.map(metadata => {
return {
label: metadata.fullName,
click: () => this.props.onAddEvent(metadata.type),
};
})}
buildMenuTemplate={() =>
this.allEventsMetadata.map(metadata => {
return {
label: metadata.fullName,
click: () => this.props.onAddEvent(metadata.type),
};
})}
/>
<ToolbarSeparator />
<ToolbarIcon

View File

@@ -504,7 +504,7 @@ export default class EventsSheet extends Component {
/>
<ContextMenu
ref={eventContextMenu => (this.eventContextMenu = eventContextMenu)}
menuTemplate={[
buildMenuTemplate={() => [
{
label: 'Copy',
click: () => this.copySelection(),
@@ -518,6 +518,7 @@ export default class EventsSheet extends Component {
{
label: 'Paste',
click: () => this.pasteEvents(),
enabled: Clipboard.has(CLIPBOARD_KIND),
accelerator: 'CmdOrCtrl+V',
},
{ type: 'separator' },
@@ -530,11 +531,13 @@ export default class EventsSheet extends Component {
{
label: 'Undo',
click: this.undo,
enabled: canUndo(this.state.history),
accelerator: 'CmdOrCtrl+Z',
},
{
label: 'Redo',
click: this.redo,
enabled: canRedo(this.state.history),
accelerator: 'CmdOrCtrl+Shift+Z',
},
]}
@@ -542,7 +545,7 @@ export default class EventsSheet extends Component {
<ContextMenu
ref={instructionContextMenu =>
(this.instructionContextMenu = instructionContextMenu)}
menuTemplate={[
buildMenuTemplate={() => [
{
label: 'Copy',
click: () => this.copySelection(),
@@ -556,6 +559,7 @@ export default class EventsSheet extends Component {
{
label: 'Paste',
click: () => this.pasteInstructions(),
enabled: Clipboard.has(CLIPBOARD_KIND),
accelerator: 'CmdOrCtrl+V',
},
{ type: 'separator' },
@@ -568,11 +572,13 @@ export default class EventsSheet extends Component {
{
label: 'Undo',
click: this.undo,
enabled: canUndo(this.state.history),
accelerator: 'CmdOrCtrl+Z',
},
{
label: 'Redo',
click: this.redo,
enabled: canRedo(this.state.history),
accelerator: 'CmdOrCtrl+Shift+Z',
},
]}
@@ -580,21 +586,24 @@ export default class EventsSheet extends Component {
<ContextMenu
ref={instructionsListContextMenu =>
(this.instructionsListContextMenu = instructionsListContextMenu)}
menuTemplate={[
buildMenuTemplate={() => [
{
label: 'Paste',
click: () => this.pasteInstructions(),
enabled: Clipboard.has(CLIPBOARD_KIND),
accelerator: 'CmdOrCtrl+V',
},
{ type: 'separator' },
{
label: 'Undo',
click: this.undo,
enabled: canUndo(this.state.history),
accelerator: 'CmdOrCtrl+Z',
},
{
label: 'Redo',
click: this.redo,
enabled: canRedo(this.state.history),
accelerator: 'CmdOrCtrl+Shift+Z',
},
]}

View File

@@ -57,7 +57,7 @@ export class MainFrameToolbar extends Component {
iconButtonElement={
<ToolbarIcon src="res/ribbon_default/bug32.png" />
}
menuTemplate={[
buildMenuTemplate={() => [
{
label: 'Request update from external editor',
click: () => this.props.requestUpdate(),

View File

@@ -266,7 +266,7 @@ class AnimationsListContainer extends Component {
<ContextMenu
ref={spriteContextMenu =>
(this.spriteContextMenu = spriteContextMenu)}
menuTemplate={[
buildMenuTemplate={() => [
{
label: 'Delete',
click: () => this.deleteSelection(),

View File

@@ -35,7 +35,7 @@ export default class GroupRow extends React.Component {
<MoreVertIcon />
</IconButton>
}
menuTemplate={[
buildMenuTemplate={()=> [
{
label: 'Edit group',
click: () => this.props.onEdit(group),

View File

@@ -0,0 +1 @@
export const CLIPBOARD_KIND = 'Object';

View File

@@ -5,6 +5,8 @@ import ListIcon from '../UI/ListIcon';
import IconButton from 'material-ui/IconButton';
import TextField from 'material-ui/TextField';
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert';
import Clipboard from '../Utils/Clipboard';
import { CLIPBOARD_KIND } from './ClipboardKind';
const styles = {
container: {
@@ -35,7 +37,7 @@ export default class ObjectRow extends React.Component {
<MoreVertIcon />
</IconButton>
}
menuTemplate={[
buildMenuTemplate={() => [
{
label: 'Edit object',
enabled: !!this.props.onEdit,
@@ -73,6 +75,7 @@ export default class ObjectRow extends React.Component {
},
{
label: 'Paste',
enabled: Clipboard.has(CLIPBOARD_KIND),
click: () => this.props.onPaste(),
},
]}

View File

@@ -21,8 +21,7 @@ import type {
ObjectWithContextList,
ObjectWithContext,
} from '../ObjectsList/EnumerateObjects';
const CLIPBOARD_KIND = 'Object';
import { CLIPBOARD_KIND } from './ClipboardKind';
const listItemHeight = 48;
const styles = {

View File

@@ -91,7 +91,7 @@ class Item extends Component {
<MoreVertIcon />
</IconButton>
}
menuTemplate={[
buildMenuTemplate={() => [
{
label: 'Edit',
click: () => this.props.onEdit(),
@@ -115,6 +115,7 @@ class Item extends Component {
},
{
label: 'Paste',
enabled: this.props.canPaste(),
click: () => this.props.onPaste(),
},
]}
@@ -376,6 +377,7 @@ export default class ProjectManager extends React.Component {
onCopy={() => this._copyLayout(layout)}
onCut={() => this._cutLayout(layout)}
onPaste={() => this._pasteLayout(i)}
canPaste={() => Clipboard.has(LAYOUT_CLIPBOARD_KIND)}
/>
);
})
@@ -421,6 +423,7 @@ export default class ProjectManager extends React.Component {
onCopy={() => this._copyExternalEvents(externalEvents)}
onCut={() => this._cutExternalEvents(externalEvents)}
onPaste={() => this._pasteExternalEvents(i)}
canPaste={() => Clipboard.has(EXTERNAL_EVENTS_CLIPBOARD_KIND)}
/>
);
})
@@ -466,6 +469,7 @@ export default class ProjectManager extends React.Component {
onCopy={() => this._copyExternalLayout(externalLayout)}
onCut={() => this._cutExternalLayout(externalLayout)}
onPaste={() => this._pasteExternalLayout(i)}
canPaste={() => Clipboard.has(EXTERNAL_LAYOUT_CLIPBOARD_KIND)}
/>
);
})

View File

@@ -1,15 +1,18 @@
// @flow
const gd = global.gd;
export default class InstancesSelection {
constructor() {
this.selection = [];
selection: Array<gdInitialInstance> = [];
hasSelectedInstances() {
return !!this.getSelectedInstances().length;
}
getSelectedInstances() {
return this.selection;
}
isInstanceSelected(instance) {
isInstanceSelected(instance: gdInitialInstance) {
for (var i = 0; i < this.selection.length; i++) {
if (gd.compare(this.selection[i], instance)) return true;
}
@@ -21,7 +24,7 @@ export default class InstancesSelection {
this.selection.length = 0;
}
selectInstance(instance, multiselect) {
selectInstance(instance: gdInitialInstance, multiselect: boolean) {
if (this.isInstanceSelected(instance)) {
if (multiselect) this.unselectInstance(instance);
@@ -32,13 +35,13 @@ export default class InstancesSelection {
this.selection.push(instance);
}
selectInstances(instances, multiselect) {
selectInstances(instances: [gdInitialInstance], multiselect: boolean) {
if (!multiselect) this.clearSelection();
instances.forEach(instance => this.selectInstance(instance, true));
}
unselectInstance(instance) {
unselectInstance(instance: gdInitialInstance) {
if (this.isInstanceSelected(instance)) {
var i = this.selection.length - 1;
while (i >= -1 && this.selection[i].ptr !== instance.ptr) {

View File

@@ -22,7 +22,6 @@ export default class SetupGridDialog extends Component {
const actions = [
<FlatButton
label="Cancel"
primary={true}
onTouchTap={this.props.onCancel}
/>,
<FlatButton

View File

@@ -83,9 +83,11 @@ export class Toolbar extends PureComponent {
tooltip={t('Toggle/edit grid')}
/>
}
menuTemplate={[
buildMenuTemplate={() => [
{
label: 'Toggle grid',
type: 'checkbox',
label: 'Show grid',
checked: this.props.isGridShown(),
click: () => this.props.toggleGrid(),
},
{ type: 'separator' },
@@ -102,7 +104,7 @@ export class Toolbar extends PureComponent {
tooltip={t('Change editor zoom')}
/>
}
menuTemplate={[
buildMenuTemplate={() => [
{
label: 'Zoom in',
click: this.props.zoomIn,

View File

@@ -40,6 +40,8 @@ import {
} from '../../Utils/History';
const gd = global.gd;
const INSTANCES_CLIPBOARD_KIND = 'Instances';
const FullSizeInstancesEditor = passFullSize(addScrollbars(InstancesEditor), {
useFlex: true,
});
@@ -107,6 +109,7 @@ export default class InstancesFullEditor extends Component {
toggleLayersList={this.toggleLayersList}
toggleWindowMask={this.toggleWindowMask}
toggleGrid={this.toggleGrid}
isGridShown={() => !!this.state.uiSettings.grid}
openSetupGrid={this.openSetupGrid}
setZoomFactor={this.setZoomFactor}
canUndo={canUndo(this.state.history)}
@@ -436,7 +439,7 @@ export default class InstancesFullEditor extends Component {
const position = useLastCursorPosition
? this.editor.getLastCursorPosition()
: this.editor.getLastContextMenuPosition();
Clipboard.set('instances', {
Clipboard.set(INSTANCES_CLIPBOARD_KIND, {
x: position[0],
y: position[1],
instances: serializedSelection,
@@ -449,7 +452,7 @@ export default class InstancesFullEditor extends Component {
};
paste = ({ useLastCursorPosition } = {}) => {
const clipboardContent = Clipboard.get('instances');
const clipboardContent = Clipboard.get(INSTANCES_CLIPBOARD_KIND);
if (!clipboardContent) return;
const position = useLastCursorPosition
@@ -682,7 +685,7 @@ export default class InstancesFullEditor extends Component {
/>
<ContextMenu
ref={contextMenu => (this.contextMenu = contextMenu)}
menuTemplate={[
buildMenuTemplate={() => [
{
label: 'Scene properties',
click: () => this.openSceneProperties(true),
@@ -691,27 +694,32 @@ export default class InstancesFullEditor extends Component {
{
label: 'Copy',
click: () => this.copySelection(),
enabled: this.instancesSelection.hasSelectedInstances(),
accelerator: 'CmdOrCtrl+C',
},
{
label: 'Cut',
click: () => this.cutSelection(),
enabled: this.instancesSelection.hasSelectedInstances(),
accelerator: 'CmdOrCtrl+X',
},
{
label: 'Paste',
click: () => this.paste(),
enabled: Clipboard.has(INSTANCES_CLIPBOARD_KIND),
accelerator: 'CmdOrCtrl+V',
},
{ type: 'separator' },
{
label: 'Undo',
click: this.undo,
enabled: canUndo(this.state.history),
accelerator: 'CmdOrCtrl+Z',
},
{
label: 'Redo',
click: this.redo,
enabled: canRedo(this.state.history),
accelerator: 'CmdOrCtrl+Shift+Z',
},
]}

View File

@@ -56,7 +56,9 @@ class MaterialUIContextMenu extends React.Component {
{...this.menuImplementation.getMenuProps()}
>
<Menu desktop width={256}>
{this.menuImplementation.buildFromTemplate(this.props.menuTemplate)}
{this.menuImplementation.buildFromTemplate(
this.props.buildMenuTemplate()
)}
</Menu>
</Popover>
</div>
@@ -68,16 +70,10 @@ class ElectronContextMenu extends React.Component {
constructor(props) {
super(props);
this.menuImplementation = new ElectronMenuImplementation();
this.menuImplementation.buildFromTemplate(props.menuTemplate);
}
componentWillReceiveProps(nextProps) {
if (this.props.menuTemplate !== nextProps.menuTemplate) {
this.menuImplementation.buildFromTemplate(nextProps.menuTemplate);
}
}
open = (x, y) => {
this.menuImplementation.buildFromTemplate(this.props.buildMenuTemplate());
this.menuImplementation.showMenu({
left: x || 0,
top: y || 0,

View File

@@ -1,6 +1,9 @@
import optionalRequire from '../../Utils/OptionalRequire.js';
const electron = optionalRequire('electron');
/**
* Wraps an Electron Menu
*/
export default class ElectronMenuImplementation {
buildFromTemplate(template) {
this.menuTemplate = template;

View File

@@ -17,32 +17,20 @@ export default class GDIconMenu extends Component {
: new MaterialUIMenuImplementation({ onClose: () => {} });
}
componentWillMount() {
this.setState({
children: this.menuImplementation.buildFromTemplate(
this.props.menuTemplate
),
});
}
componentWillReceiveProps(nextProps) {
if (this.props.menuTemplate !== nextProps.menuTemplate) {
this.setState({
children: this.menuImplementation.buildFromTemplate(
nextProps.menuTemplate
),
});
}
}
open = event => {
if (!this.iconMenu) return;
const node = ReactDOM.findDOMNode(this.iconMenu);
if (!node) return;
this.setState({
children: this.menuImplementation.buildFromTemplate(
this.props.buildMenuTemplate()
),
});
this.menuImplementation.showMenu(node.getBoundingClientRect());
this.iconMenu.open('unkown', event);
this.iconMenu.open('unknown', event);
};
_onTouchTap = event => {
@@ -53,11 +41,16 @@ export default class GDIconMenu extends Component {
const node = ReactDOM.findDOMNode(this.iconMenu);
if (!node) return;
this.setState({
children: this.menuImplementation.buildFromTemplate(
this.props.buildMenuTemplate()
),
});
this.menuImplementation.showMenu(node.getBoundingClientRect());
};
render() {
const { menuTemplate, ...iconMenuProps } = this.props; //eslint-disable-line
const { buildMenuTemplate, ...iconMenuProps } = this.props; //eslint-disable-line
// Use disableAutoFocus to avoid making TextField lose focus.
// See material-ui bug: https://github.com/callemall/material-ui/issues/4387

View File

@@ -26,6 +26,18 @@ const adaptAcceleratorString = (accelerator: string): string => {
}
};
/**
* Construct items for material-ui's Menu, using a template which
* is partially supporting the Electron Menu API (https://github.com/electron/electron/blob/master/docs/api/menu-item.md).
*
* Supported options are:
* - click
* - type ('separator' and 'checkbox')
* - label
* - accelerator
* - enabled
* - checked (when `type` is 'checkbox')
*/
export default class MaterialUIMenuImplementation {
constructor({ onClose }) {
this._onClose = onClose;
@@ -35,24 +47,43 @@ export default class MaterialUIMenuImplementation {
return template.map((item, id) => {
if (item.type === 'separator') {
return <Divider key={'separator' + id} />;
} else if (item.type === 'checkbox') {
return (
<MenuItem
key={item.label}
primaryText={item.label}
secondaryText={
item.accelerator
? adaptAcceleratorString(item.accelerator)
: undefined
}
checked={item.checked}
insetChildren={!item.checked}
disabled={item.enabled === false}
onTouchTap={() => {
item.click();
this._onClose();
}}
/>
);
} else {
return (
<MenuItem
key={item.label}
primaryText={item.label}
secondaryText={
item.accelerator
? adaptAcceleratorString(item.accelerator)
: undefined
}
disabled={item.enabled === false}
onTouchTap={() => {
item.click();
this._onClose();
}}
/>
);
}
return (
<MenuItem
key={item.label}
primaryText={item.label}
secondaryText={
item.accelerator
? adaptAcceleratorString(item.accelerator)
: undefined
}
disabled={item.enabled === false}
onTouchTap={() => {
item.click();
this._onClose();
}}
/>
);
});
}