mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
[WIP] Add ResourcesEditor based on a generic SortableVirtualizedItemList
TODO: * Use SortableVirtualizedItemList for ObjectsList and ObjectsGroupsList * Add missing features (sorting, delete, thumbnail for images, properties edition) to ResourcesEditor
This commit is contained in:
1
newIDE/app/flow-typed/libGD.js
vendored
1
newIDE/app/flow-typed/libGD.js
vendored
@@ -13,6 +13,7 @@ declare type gdExternalEvents = EmscriptenObject;
|
||||
declare type gdSerializerElement = EmscriptenObject;
|
||||
declare type gdInitialInstance = EmscriptenObject;
|
||||
declare type gdBaseEvent = EmscriptenObject;
|
||||
declare type gdResource = EmscriptenObject;
|
||||
|
||||
//Represents all objects that have serializeTo and unserializeFrom methods.
|
||||
declare type gdSerializable = EmscriptenObject;
|
||||
|
21
newIDE/app/src/MainFrame/Editors/ResourcesEditor.js
Normal file
21
newIDE/app/src/MainFrame/Editors/ResourcesEditor.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import BaseEditor from './BaseEditor';
|
||||
import ResourcesList from '../../ResourcesList';
|
||||
|
||||
export default class ResourcesEditor extends BaseEditor {
|
||||
updateToolbar() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
render() {
|
||||
const { project, layoutName } = this.props;
|
||||
|
||||
return (
|
||||
<ResourcesList
|
||||
{...this.props}
|
||||
ref={editor => (this.editor = editor)}
|
||||
project={project}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@@ -47,6 +47,7 @@ import ExternalEventsEditor from './Editors/ExternalEventsEditor';
|
||||
import SceneEditor from './Editors/SceneEditor';
|
||||
import ExternalLayoutEditor from './Editors/ExternalLayoutEditor';
|
||||
import StartPage from './Editors/StartPage';
|
||||
import ResourcesEditor from './Editors/ResourcesEditor';
|
||||
import {
|
||||
type PreferencesState,
|
||||
getThemeName,
|
||||
@@ -545,6 +546,36 @@ export default class MainFrame extends Component<*, State> {
|
||||
);
|
||||
};
|
||||
|
||||
openResources = () => {
|
||||
this.setState(
|
||||
{
|
||||
editorTabs: openEditorTab(this.state.editorTabs, {
|
||||
name: 'Resources',
|
||||
editorCreator: () => (
|
||||
<ResourcesEditor
|
||||
project={this.state.currentProject}
|
||||
setToolbar={this.setEditorToolbar}
|
||||
onDeleteResource={(resource: gdResource, cb: boolean => void) => {
|
||||
// TODO: Project wide refactoring of objects/events using the resource
|
||||
cb(true);
|
||||
}}
|
||||
onRenameResource={(
|
||||
resource: gdResource,
|
||||
newName: string,
|
||||
cb: boolean => void
|
||||
) => {
|
||||
// TODO: Project wide refactoring of objects/events using the resource
|
||||
cb(true);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
key: 'resources',
|
||||
}),
|
||||
},
|
||||
() => this.updateToolbar()
|
||||
);
|
||||
};
|
||||
|
||||
openStartPage = () => {
|
||||
this.setState(
|
||||
{
|
||||
@@ -814,6 +845,7 @@ export default class MainFrame extends Component<*, State> {
|
||||
onCloseProject={this.askToCloseProject}
|
||||
onExportProject={this.openExportDialog}
|
||||
onOpenPreferences={() => this.openPreferences(true)}
|
||||
onOpenResources={() => this.openResources(true)}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import ResourcesLoader from '../ObjectsRendering/ResourcesLoader';
|
||||
import ResourceSelector from '../ResourcesEditor/ResourceSelector';
|
||||
import ResourceSelector from '../ResourcesList/ResourceSelector';
|
||||
import ImageThumbnail from './ImageThumbnail';
|
||||
|
||||
export default ({
|
||||
|
@@ -1,21 +1,6 @@
|
||||
// @flow
|
||||
import { mapFor } from '../Utils/MapFor';
|
||||
|
||||
//TODO: Layout, ExternalEvents and ExternalLayout should be moved to a common type definition file
|
||||
//for all GDevelop.js
|
||||
type Layout = {
|
||||
getName: Function,
|
||||
setName: Function,
|
||||
};
|
||||
type ExternalLayout = {
|
||||
getName: Function,
|
||||
setName: Function,
|
||||
};
|
||||
type ExternalEvents = {
|
||||
getName: Function,
|
||||
setName: Function,
|
||||
};
|
||||
|
||||
export const enumerateLayouts = (project: any) =>
|
||||
mapFor(0, project.getLayoutsCount(), i => project.getLayoutAt(i));
|
||||
|
||||
@@ -30,7 +15,7 @@ export const enumerateExternalLayouts = (project: any) =>
|
||||
);
|
||||
|
||||
export const filterProjectItemsList = (
|
||||
list: Array<Layout> | Array<ExternalLayout> | Array<ExternalEvents>,
|
||||
list: Array<gdLayout> | Array<gdExternalLayout> | Array<gdExternalEvents>,
|
||||
searchText: string
|
||||
) => {
|
||||
if (!searchText) return list;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { List, ListItem } from 'material-ui/List';
|
||||
import TextField from 'material-ui/TextField';
|
||||
import SearchBar from 'material-ui-search-bar';
|
||||
@@ -65,7 +66,10 @@ const ThemableProjectStructureItem = ({ muiTheme, ...otherProps }) => (
|
||||
|
||||
const ProjectStructureItem = muiThemeable()(ThemableProjectStructureItem);
|
||||
|
||||
class ThemableItem extends Component {
|
||||
class ThemableItem extends React.Component<*, *> {
|
||||
textField: ?Object;
|
||||
_iconMenu: ?Object;
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.editingName && this.props.editingName) {
|
||||
setTimeout(() => {
|
||||
@@ -126,7 +130,7 @@ class ThemableItem extends Component {
|
||||
onKeyPress={event => {
|
||||
if (event.charCode === 13) {
|
||||
// enter key pressed
|
||||
this.textField.blur();
|
||||
if (this.textField) this.textField.blur();
|
||||
this.props.onRename(event.target.value);
|
||||
}
|
||||
}}
|
||||
@@ -157,33 +161,64 @@ const Item = muiThemeable()(ThemableItem);
|
||||
|
||||
const AddItem = makeAddItem(ListItem);
|
||||
|
||||
export default class ProjectManager extends React.Component {
|
||||
type Props = {
|
||||
project: gdProject,
|
||||
onDeleteLayout: (gdLayout) => void,
|
||||
onDeleteExternalEvents: (gdExternalEvents) => void,
|
||||
onDeleteExternalLayout: (gdExternalLayout) => void,
|
||||
onRenameLayout: (string, string) => void,
|
||||
onRenameExternalEvents: (string, string) => void,
|
||||
onRenameExternalLayout: (string, string) => void,
|
||||
onOpenLayout: (string) => void,
|
||||
onOpenExternalEvents: (string) => void,
|
||||
onOpenExternalLayout: (string) => void,
|
||||
onSaveProject: () => void,
|
||||
onCloseProject: () => void,
|
||||
onExportProject: () => void,
|
||||
onOpenPreferences: () => void,
|
||||
onOpenResources: () => void,
|
||||
onAddLayout: () => void,
|
||||
onAddExternalEvents: () => void,
|
||||
onAddExternalLayout: () => void,
|
||||
};
|
||||
|
||||
type State = {|
|
||||
renamedItemKind: ?string,
|
||||
renamedItemName: string,
|
||||
searchText: string,
|
||||
projectPropertiesDialogOpen: boolean,
|
||||
variablesEditorOpen: boolean,
|
||||
|};
|
||||
|
||||
export default class ProjectManager extends React.Component<Props, State> {
|
||||
state = {
|
||||
renamedItemKind: null,
|
||||
renamedItemName: '',
|
||||
searchText: '',
|
||||
projectPropertiesDialogOpen: false,
|
||||
variablesEditorOpen: false,
|
||||
};
|
||||
|
||||
_onEditName = (kind, name) => {
|
||||
_onEditName = (kind: ?string, name: string) => {
|
||||
this.setState({
|
||||
renamedItemKind: kind,
|
||||
renamedItemName: name,
|
||||
});
|
||||
};
|
||||
|
||||
_copyLayout = layout => {
|
||||
_copyLayout = (layout: gdLayout) => {
|
||||
Clipboard.set(LAYOUT_CLIPBOARD_KIND, {
|
||||
layout: serializeToJSObject(layout),
|
||||
name: layout.getName(),
|
||||
});
|
||||
};
|
||||
|
||||
_cutLayout = layout => {
|
||||
_cutLayout = (layout: gdLayout) => {
|
||||
this._copyLayout(layout);
|
||||
this.props.onDeleteLayout(layout);
|
||||
};
|
||||
|
||||
_pasteLayout = index => {
|
||||
_pasteLayout = (index: number) => {
|
||||
if (!Clipboard.has(LAYOUT_CLIPBOARD_KIND)) return;
|
||||
|
||||
const { layout: copiedLayout, name } = Clipboard.get(LAYOUT_CLIPBOARD_KIND);
|
||||
@@ -207,19 +242,19 @@ export default class ProjectManager extends React.Component {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
_copyExternalEvents = externalEvents => {
|
||||
_copyExternalEvents = (externalEvents: gdExternalEvents) => {
|
||||
Clipboard.set(EXTERNAL_EVENTS_CLIPBOARD_KIND, {
|
||||
externalEvents: serializeToJSObject(externalEvents),
|
||||
name: externalEvents.getName(),
|
||||
});
|
||||
};
|
||||
|
||||
_cutExternalEvents = externalEvents => {
|
||||
_cutExternalEvents = (externalEvents: gdExternalEvents) => {
|
||||
this._copyExternalEvents(externalEvents);
|
||||
this.props.onDeleteExternalEvents(externalEvents);
|
||||
};
|
||||
|
||||
_pasteExternalEvents = index => {
|
||||
_pasteExternalEvents = (index: number) => {
|
||||
if (!Clipboard.has(EXTERNAL_EVENTS_CLIPBOARD_KIND)) return;
|
||||
|
||||
const { externalEvents: copiedExternalEvents, name } = Clipboard.get(
|
||||
@@ -244,19 +279,19 @@ export default class ProjectManager extends React.Component {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
_copyExternalLayout = externalLayout => {
|
||||
_copyExternalLayout = (externalLayout: gdExternalLayout) => {
|
||||
Clipboard.set(EXTERNAL_LAYOUT_CLIPBOARD_KIND, {
|
||||
externalLayout: serializeToJSObject(externalLayout),
|
||||
name: externalLayout.getName(),
|
||||
});
|
||||
};
|
||||
|
||||
_cutExternalLayout = externalLayout => {
|
||||
_cutExternalLayout = (externalLayout: gdExternalLayout) => {
|
||||
this._copyExternalLayout(externalLayout);
|
||||
this.props.onDeleteExternalLayout(externalLayout);
|
||||
};
|
||||
|
||||
_pasteExternalLayout = index => {
|
||||
_pasteExternalLayout = (index: number) => {
|
||||
if (!Clipboard.has(EXTERNAL_LAYOUT_CLIPBOARD_KIND)) return;
|
||||
|
||||
const { externalLayout: copiedExternalLayout, name } = Clipboard.get(
|
||||
@@ -328,10 +363,6 @@ export default class ProjectManager extends React.Component {
|
||||
<div style={styles.container}>
|
||||
<List style={styles.list}>
|
||||
{this._renderMenu()}
|
||||
{/* <ProjectStructureItem
|
||||
primaryText="Resources"
|
||||
leftIcon={<ListIcon src="res/ribbon_default/image32.png" />}
|
||||
/> */}
|
||||
<ProjectStructureItem
|
||||
primaryText="Game settings"
|
||||
leftIcon={
|
||||
@@ -354,6 +385,12 @@ export default class ProjectManager extends React.Component {
|
||||
leftIcon={<ListIcon src="res/ribbon_default/editname32.png" />}
|
||||
onClick={() => this.setState({ variablesEditorOpen: true })}
|
||||
/>,
|
||||
<ListItem
|
||||
key="resources"
|
||||
primaryText="Resources"
|
||||
leftIcon={<ListIcon src="res/ribbon_default/image32.png" />}
|
||||
onClick={() => this.props.onOpenResources()}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<ProjectStructureItem
|
||||
@@ -367,7 +404,7 @@ export default class ProjectManager extends React.Component {
|
||||
enumerateLayouts(project),
|
||||
searchText
|
||||
)
|
||||
.map((layout, i) => {
|
||||
.map((layout: gdLayout, i: number) => {
|
||||
const name = layout.getName();
|
||||
return (
|
||||
<Item
|
||||
|
19
newIDE/app/src/ResourcesList/EnumerateResources.js
Normal file
19
newIDE/app/src/ResourcesList/EnumerateResources.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// @flow
|
||||
|
||||
export const filterResourcesList = (
|
||||
list: Array<gdResource>,
|
||||
searchText: string
|
||||
): Array<gdResource> => {
|
||||
if (!searchText) return list;
|
||||
|
||||
const lowercaseSearchText = searchText.toLowerCase();
|
||||
|
||||
return list.filter((resource: gdResource) => {
|
||||
return (
|
||||
resource
|
||||
.getName()
|
||||
.toLowerCase()
|
||||
.indexOf(lowercaseSearchText) !== -1
|
||||
);
|
||||
});
|
||||
};
|
325
newIDE/app/src/ResourcesList/index.js
Normal file
325
newIDE/app/src/ResourcesList/index.js
Normal file
@@ -0,0 +1,325 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { AutoSizer } from 'react-virtualized';
|
||||
import SortableVirtualizedItemList from '../UI/SortableVirtualizedItemList';
|
||||
import Paper from 'material-ui/Paper';
|
||||
import SearchBar from 'material-ui-search-bar';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import Clipboard from '../Utils/Clipboard';
|
||||
import {
|
||||
serializeToJSObject,
|
||||
unserializeFromJSObject,
|
||||
} from '../Utils/Serializer';
|
||||
import { showWarningBox } from '../UI/Messages/MessageBox';
|
||||
import { filterResourcesList } from './EnumerateResources';
|
||||
const CLIPBOARD_KIND = 'Resource';
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
listContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
type State = {|
|
||||
renamedResource: ?gdResource,
|
||||
searchText: string,
|
||||
|};
|
||||
|
||||
type Props = {
|
||||
project: gdProject,
|
||||
onDeleteResource: (resource: gdResource, cb: (boolean) => void) => void,
|
||||
onRenameResource: (
|
||||
resource: gdResource,
|
||||
newName: string,
|
||||
cb: (boolean) => void
|
||||
) => void,
|
||||
};
|
||||
|
||||
export default class ResourcesList extends React.Component<Props, State> {
|
||||
static defaultProps = {
|
||||
onDeleteResource: (resource: gdResource, cb: boolean => void) => cb(true),
|
||||
onRenameResource: (
|
||||
resource: gdResource,
|
||||
newName: string,
|
||||
cb: boolean => void
|
||||
) => cb(true),
|
||||
};
|
||||
|
||||
sortableList: any;
|
||||
state: State = {
|
||||
renamedResource: null,
|
||||
searchText: '',
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps: Props, nextState: State) {
|
||||
// The component is costly to render, so avoid any re-rendering as much
|
||||
// as possible.
|
||||
// We make the assumption that no changes to resources list is made outside
|
||||
// from the component.
|
||||
// If a change is made, the component won't notice it: you have to manually
|
||||
// call forceUpdate.
|
||||
|
||||
if (
|
||||
this.state.renamedResource !== nextState.renamedResource ||
|
||||
this.state.searchText !== nextState.searchText
|
||||
)
|
||||
return true;
|
||||
|
||||
if (this.props.project !== nextProps.project) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
addResource = (objectType: string) => {
|
||||
// const { project, objectsContainer } = this.props;
|
||||
// const name = newNameGenerator(
|
||||
// 'NewObject',
|
||||
// name =>
|
||||
// objectsContainer.hasObjectNamed(name) || project.hasObjectNamed(name)
|
||||
// );
|
||||
// const object = objectsContainer.insertNewObject(
|
||||
// project,
|
||||
// objectType,
|
||||
// name,
|
||||
// objectsContainer.getObjectsCount()
|
||||
// );
|
||||
// this.setState(
|
||||
// {
|
||||
// newObjectDialogOpen: false,
|
||||
// },
|
||||
// () => {
|
||||
// if (this.props.onEditResource) {
|
||||
// this.props.onEditResource(object);
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
};
|
||||
|
||||
_deleteResource = (resource: gdResource) => {
|
||||
// const { object, global } = resource;
|
||||
// const { project, objectsContainer } = this.props;
|
||||
// //eslint-disable-next-line
|
||||
// const answer = confirm(
|
||||
// "Are you sure you want to remove this object? This can't be undone."
|
||||
// );
|
||||
// if (!answer) return;
|
||||
// this.props.onDeleteResource(resource, doRemove => {
|
||||
// if (!doRemove) return;
|
||||
// if (global) {
|
||||
// project.removeObject(object.getName());
|
||||
// } else {
|
||||
// objectsContainer.removeObject(object.getName());
|
||||
// }
|
||||
// this.forceUpdate();
|
||||
// });
|
||||
};
|
||||
|
||||
_copyResource = (resource: gdResource) => {
|
||||
// const { object } = resource;
|
||||
// Clipboard.set(CLIPBOARD_KIND, {
|
||||
// type: object.getType(),
|
||||
// name: object.getName(),
|
||||
// object: serializeToJSObject(object),
|
||||
// });
|
||||
};
|
||||
|
||||
_cutResource = (resource: gdResource) => {
|
||||
// this._copyResource(resource);
|
||||
// this._deleteResource(resource);
|
||||
};
|
||||
|
||||
_pasteResource = (resource: gdResource) => {
|
||||
// if (!Clipboard.has(CLIPBOARD_KIND)) return;
|
||||
// const { object: pasteObject, global } = resource;
|
||||
// const { object: copiedObject, type, name } = Clipboard.get(CLIPBOARD_KIND);
|
||||
// const { project, objectsContainer, onObjectPasted } = this.props;
|
||||
// const newName = newNameGenerator(
|
||||
// 'CopyOf' + name,
|
||||
// name =>
|
||||
// objectsContainer.hasObjectNamed(name) || project.hasObjectNamed(name)
|
||||
// );
|
||||
// const newObject = global
|
||||
// ? project.insertNewObject(
|
||||
// project,
|
||||
// type,
|
||||
// newName,
|
||||
// project.getObjectPosition(pasteObject.getName())
|
||||
// )
|
||||
// : objectsContainer.insertNewObject(
|
||||
// project,
|
||||
// type,
|
||||
// newName,
|
||||
// objectsContainer.getObjectPosition(pasteObject.getName())
|
||||
// );
|
||||
// unserializeFromJSObject(
|
||||
// newObject,
|
||||
// copiedObject,
|
||||
// 'unserializeFrom',
|
||||
// project
|
||||
// );
|
||||
// this.forceUpdate();
|
||||
// if (onObjectPasted) onObjectPasted(newObject);
|
||||
};
|
||||
|
||||
_editName = (resource: ?gdResource) => {
|
||||
this.setState(
|
||||
{
|
||||
renamedResource: resource,
|
||||
},
|
||||
() => this.sortableList.getWrappedInstance().forceUpdateGrid()
|
||||
);
|
||||
};
|
||||
|
||||
_rename = (resource: gdResource, newName: string) => {
|
||||
const { project } = this.props;
|
||||
this.setState({
|
||||
renamedResource: null,
|
||||
});
|
||||
|
||||
if (resource.getName() === newName) return;
|
||||
|
||||
if (project.getResourcesManager().hasResource(newName)) {
|
||||
showWarningBox('Another resource with this name already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onRenameResource(resource, newName, doRename => {
|
||||
if (!doRename) return;
|
||||
resource.setName(newName);
|
||||
this.forceUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
_move = (oldIndex: number, newIndex: number) => {
|
||||
// const { project, objectsContainer } = this.props;
|
||||
|
||||
// const isInContainerResourcesList =
|
||||
// oldIndex < this.containerResourcesList.length;
|
||||
// if (isInContainerResourcesList) {
|
||||
// objectsContainer.moveObject(
|
||||
// oldIndex,
|
||||
// Math.min(newIndex, this.containerResourcesList.length - 1)
|
||||
// );
|
||||
// } else {
|
||||
// const projectOldIndex = oldIndex - this.containerResourcesList.length;
|
||||
// const projectNewIndex = newIndex - this.containerResourcesList.length;
|
||||
|
||||
// project.moveObject(
|
||||
// projectOldIndex,
|
||||
// Math.min(projectNewIndex, this.projectResourcesList.length - 1)
|
||||
// );
|
||||
// }
|
||||
|
||||
this.forceUpdateList();
|
||||
};
|
||||
|
||||
forceUpdateList = () => {
|
||||
this.forceUpdate();
|
||||
this.sortableList.getWrappedInstance().forceUpdateGrid();
|
||||
};
|
||||
|
||||
_renderResourceMenuTemplate = (resource: gdResource) => {
|
||||
return [
|
||||
{
|
||||
label: 'Edit resource',
|
||||
enabled: false,
|
||||
click: () => {
|
||||
/*TODO*/
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Rename',
|
||||
click: () => this._editName(resource),
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
click: () => this._deleteResource(resource),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Add a new resource...',
|
||||
enabled: false,
|
||||
click: () => {
|
||||
/*TODO*/
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Copy',
|
||||
enabled: false,
|
||||
click: () => this._copyResource(resource),
|
||||
},
|
||||
{
|
||||
label: 'Cut',
|
||||
enabled: false,
|
||||
click: () => this._cutResource(resource),
|
||||
},
|
||||
{
|
||||
label: 'Paste',
|
||||
enabled: Clipboard.has(CLIPBOARD_KIND),
|
||||
click: () => this._pasteResource(resource),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
render() {
|
||||
const { project } = this.props;
|
||||
const { searchText } = this.state;
|
||||
|
||||
const resourcesManager = project.getResourcesManager();
|
||||
const allResourcesList = resourcesManager
|
||||
.getAllResourcesList() // TODO: This should be renamed to getAllResourcesNames
|
||||
.toJSArray()
|
||||
.map(resourceName => resourcesManager.getResource(resourceName));
|
||||
const filteredList = filterResourcesList(allResourcesList, searchText);
|
||||
const fullList = filteredList.concat({
|
||||
key: 'add-item-row',
|
||||
});
|
||||
|
||||
// Force List component to be mounted again if project or objectsContainer
|
||||
// has been changed. Avoid accessing to invalid objects that could
|
||||
// crash the app.
|
||||
const listKey = project.ptr;
|
||||
|
||||
return (
|
||||
<Paper style={styles.container}>
|
||||
<div style={styles.listContainer}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<SortableVirtualizedItemList
|
||||
key={listKey}
|
||||
ref={sortableList => (this.sortableList = sortableList)}
|
||||
fullList={fullList}
|
||||
width={width}
|
||||
height={height}
|
||||
renamedItem={this.state.renamedResource}
|
||||
onAddNewItem={() => {}}
|
||||
onRename={this._rename}
|
||||
onSortEnd={({ oldIndex, newIndex }) =>
|
||||
this._move(oldIndex, newIndex)}
|
||||
helperClass="sortable-helper"
|
||||
distance={30}
|
||||
buildMenuTemplate={this._renderResourceMenuTemplate}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
<SearchBar
|
||||
value={searchText}
|
||||
onRequestSearch={() => {}}
|
||||
onChange={text =>
|
||||
this.setState({
|
||||
searchText: text,
|
||||
})}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
}
|
116
newIDE/app/src/UI/SortableVirtualizedItemList/ItemRow.js
Normal file
116
newIDE/app/src/UI/SortableVirtualizedItemList/ItemRow.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import React from 'react';
|
||||
import { ListItem } from 'material-ui/List';
|
||||
import IconMenu from '../Menu/IconMenu';
|
||||
import ListIcon from '../ListIcon';
|
||||
import IconButton from 'material-ui/IconButton';
|
||||
import TextField from 'material-ui/TextField';
|
||||
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert';
|
||||
import muiThemeable from 'material-ui/styles/muiThemeable';
|
||||
import { type Item } from '.';
|
||||
|
||||
const styles = {
|
||||
itemName: {
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
textField: {
|
||||
top: -16,
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
index: number,
|
||||
item: Item,
|
||||
onRename: (string) => void,
|
||||
editingName: boolean,
|
||||
getThumbnail?: () => string,
|
||||
selected: true,
|
||||
onItemSelected: () => void,
|
||||
buildMenuTemplate: Item => any,
|
||||
};
|
||||
|
||||
class ThemableItemRow extends React.Component<Props, *> {
|
||||
_renderItemMenu(item) {
|
||||
return (
|
||||
<IconMenu
|
||||
ref={iconMenu => (this._iconMenu = iconMenu)}
|
||||
iconButtonElement={
|
||||
<IconButton>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
}
|
||||
buildMenuTemplate={() => this.props.buildMenuTemplate(item)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.editingName && this.props.editingName) {
|
||||
setTimeout(() => {
|
||||
if (this.textField) this.textField.focus();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
_onContextMenu = event => {
|
||||
if (this._iconMenu) this._iconMenu.open(event);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { item, selected, style, getThumbnail, muiTheme } = this.props;
|
||||
|
||||
const itemName = item.getName();
|
||||
const label = this.props.editingName ? (
|
||||
<TextField
|
||||
id="rename-item-field"
|
||||
ref={textField => (this.textField = textField)}
|
||||
defaultValue={itemName}
|
||||
onBlur={e => this.props.onRename(e.target.value)}
|
||||
onKeyPress={event => {
|
||||
if (event.charCode === 13) {
|
||||
// enter key pressed
|
||||
this.textField.blur();
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
style={styles.textField}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
...styles.itemName,
|
||||
color: selected ? muiTheme.listItem.selectedTextColor : undefined,
|
||||
}}
|
||||
>
|
||||
{itemName}
|
||||
</div>
|
||||
);
|
||||
|
||||
const itemStyle = {
|
||||
borderBottom: `1px solid ${muiTheme.listItem.separatorColor}`,
|
||||
backgroundColor: selected
|
||||
? muiTheme.listItem.selectedBackgroundColor
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
style={{ ...itemStyle, ...style }}
|
||||
onContextMenu={this._onContextMenu}
|
||||
primaryText={label}
|
||||
leftIcon={getThumbnail && <ListIcon src={getThumbnail()} />}
|
||||
rightIconButton={this._renderItemMenu(item)}
|
||||
onClick={() => {
|
||||
if (!this.props.onItemSelected) return;
|
||||
if (this.props.editingName) return;
|
||||
|
||||
this.props.onItemSelected(selected ? '' : itemName);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ItemRow = muiThemeable()(ThemableItemRow);
|
||||
export default ItemRow;
|
108
newIDE/app/src/UI/SortableVirtualizedItemList/index.js
Normal file
108
newIDE/app/src/UI/SortableVirtualizedItemList/index.js
Normal file
@@ -0,0 +1,108 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { List } from 'react-virtualized';
|
||||
import { ListItem } from 'material-ui/List';
|
||||
import ItemRow from './ItemRow';
|
||||
import { makeAddItem } from '../ListAddItem';
|
||||
import { SortableContainer, SortableElement } from 'react-sortable-hoc';
|
||||
|
||||
const listItemHeight = 48; // TODO: Move this into theme?
|
||||
|
||||
const AddItemRow = makeAddItem(ListItem);
|
||||
|
||||
const SortableItemRow = SortableElement(props => {
|
||||
const { style, ...otherProps } = props;
|
||||
return (
|
||||
<div style={style}>
|
||||
<ItemRow {...otherProps} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const SortableAddItemRow = SortableElement(props => {
|
||||
return <AddItemRow {...props} />;
|
||||
});
|
||||
|
||||
export type Item = {
|
||||
key: string | number,
|
||||
getName: () => string,
|
||||
};
|
||||
|
||||
type ItemsListProps = {
|
||||
height: number,
|
||||
width: number,
|
||||
fullList: Array<Item>,
|
||||
selectedItem: ?Item,
|
||||
onAddNewItem: () => void,
|
||||
onRename: (Item, string) => void,
|
||||
getThumbnail?: Item => string,
|
||||
onItemSelected: Item => void,
|
||||
renamedItem: ?Item,
|
||||
addNewItemLabel: React.Node | string,
|
||||
buildMenuTemplate: Item => any,
|
||||
};
|
||||
|
||||
class ItemsList extends React.Component<ItemsListProps, *> {
|
||||
list: any;
|
||||
|
||||
forceUpdateGrid() {
|
||||
if (this.list) this.list.forceUpdateGrid();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
height,
|
||||
width,
|
||||
fullList,
|
||||
selectedItem,
|
||||
addNewItemLabel,
|
||||
renamedItem,
|
||||
getThumbnail,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<List
|
||||
ref={list => (this.list = list)}
|
||||
height={height}
|
||||
rowCount={fullList.length}
|
||||
rowHeight={listItemHeight}
|
||||
rowRenderer={({ index, key, style }) => {
|
||||
const item = fullList[index];
|
||||
if (item.key === 'add-item-row') {
|
||||
return (
|
||||
<SortableAddItemRow
|
||||
index={fullList.length}
|
||||
key={key}
|
||||
style={style}
|
||||
disabled
|
||||
onClick={this.props.onAddNewItem}
|
||||
primaryText={addNewItemLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const nameBeingEdited = renamedItem === item;
|
||||
|
||||
return (
|
||||
<SortableItemRow
|
||||
index={index}
|
||||
key={item.key}
|
||||
item={item}
|
||||
style={style}
|
||||
onRename={newName => this.props.onRename(item, newName)}
|
||||
editingName={nameBeingEdited}
|
||||
getThumbnail={getThumbnail ? () => getThumbnail(item) : undefined}
|
||||
selected={item === selectedItem}
|
||||
onItemSelected={this.props.onItemSelected}
|
||||
buildMenuTemplate={this.props.buildMenuTemplate}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SortableItemsList = SortableContainer(ItemsList, { withRef: true });
|
||||
export default SortableItemsList;
|
@@ -18,7 +18,7 @@ import BrowserExamples from './ProjectCreation/BrowserExamples';
|
||||
import BrowserProjectOpener from './ProjectsStorage/BrowserProjectOpener';
|
||||
import BrowserSaveDialog from './ProjectsStorage/BrowserSaveDialog';
|
||||
import BrowserIntroDialog from './MainFrame/BrowserIntroDialog';
|
||||
import browserResourceSources from './ResourcesEditor/BrowserResourceSources';
|
||||
import browserResourceSources from './ResourcesList/BrowserResourceSources';
|
||||
import BrowserS3PreviewLauncher from './Export/BrowserExporters/BrowserS3PreviewLauncher';
|
||||
import { getBrowserExporters } from './Export/BrowserExporters';
|
||||
|
||||
@@ -26,7 +26,7 @@ import { getBrowserExporters } from './Export/BrowserExporters';
|
||||
import ExternalEditor from './ExternalEditor';
|
||||
import optionalRequire from './Utils/OptionalRequire.js';
|
||||
import LocalExamples from './ProjectCreation/LocalExamples';
|
||||
import localResourceSources from './ResourcesEditor/LocalResourceSources';
|
||||
import localResourceSources from './ResourcesList/LocalResourceSources';
|
||||
import LocalProjectWriter from './ProjectsStorage/LocalProjectWriter';
|
||||
import LocalProjectOpener from './ProjectsStorage/LocalProjectOpener';
|
||||
import LocalPreviewLauncher from './Export/LocalExporters/LocalPreviewLauncher';
|
||||
|
Reference in New Issue
Block a user