1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-27 08:21:03 +02:00

Electron: added toolbar and fixed various state issues

This commit is contained in:
Laurent Cozic 2017-11-29 23:03:10 +00:00
parent a346116d5f
commit 52cb10dd4e
10 changed files with 366 additions and 95 deletions

View File

@ -0,0 +1,25 @@
const events = require('events');
class EventManager {
constructor() {
this.emitter_ = new events.EventEmitter();
}
on(eventName, callback) {
return this.emitter_.on(eventName, callback);
}
emit(eventName, object = null) {
return this.emitter_.emit(eventName, object);
}
removeListener(eventName, callback) {
return this.emitter_.removeListener(eventName, callback);
}
}
const eventManager = new EventManager();
module.exports = eventManager;

View File

@ -41,7 +41,7 @@ class HeaderComponent extends React.Component {
}
render() {
const style = this.props.style;
const style = Object.assign({}, this.props.style);
const theme = themeStyle(this.props.theme);
const showBackButton = this.props.showBackButton === undefined || this.props.showBackButton === true;
style.height = theme.headerHeight;

View File

@ -15,6 +15,7 @@ const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const layoutUtils = require('lib/layout-utils.js');
const { bridge } = require('electron').remote.require('./bridge');
const eventManager = require('../eventManager');
class MainScreenComponent extends React.Component {
@ -205,6 +206,7 @@ class MainScreenComponent extends React.Component {
if (newNote) {
await Note.save(newNote);
eventManager.emit('alarmChange', { noteId: note.id });
}
this.setState({ promptOptions: null });
@ -223,44 +225,58 @@ class MainScreenComponent extends React.Component {
}
}
styles(themeId, width, height) {
const styleKey = themeId + '_' + width + '_' + height;
if (styleKey === this.styleKey_) return this.styles_;
const theme = themeStyle(themeId);
this.styleKey_ = styleKey;
this.styles_ = {};
const rowHeight = height - theme.headerHeight;
this.styles_.header = {
width: width,
};
this.styles_.sideBar = {
width: Math.floor(layoutUtils.size(width * .2, 150, 300)),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
this.styles_.noteList = {
width: Math.floor(layoutUtils.size(width * .2, 150, 300)),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
this.styles_.noteText = {
width: Math.floor(layoutUtils.size(width - this.styles_.sideBar.width - this.styles_.noteList.width, 0)),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
this.styles_.prompt = {
width: width,
height: height,
};
return this.styles_;
}
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);
const promptOptions = this.state.promptOptions;
const folders = this.props.folders;
const notes = this.props.notes;
const headerStyle = {
width: style.width,
};
const rowHeight = style.height - theme.headerHeight;
const sideBarStyle = {
width: Math.floor(layoutUtils.size(style.width * .2, 150, 300)),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
const noteListStyle = {
width: Math.floor(layoutUtils.size(style.width * .2, 150, 300)),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
const noteTextStyle = {
width: Math.floor(layoutUtils.size(style.width - sideBarStyle.width - noteListStyle.width, 0)),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
const promptStyle = {
width: style.width,
height: style.height,
};
const styles = this.styles(this.props.theme, style.width, style.height);
const headerButtons = [];
@ -299,23 +315,29 @@ class MainScreenComponent extends React.Component {
},
});
if (!this.promptOnClose_) {
this.promptOnClose_ = (answer, buttonType) => {
return this.state.promptOptions.onClose(answer, buttonType);
}
}
return (
<div style={style}>
<PromptDialog
autocomplete={promptOptions && ('autocomplete' in promptOptions) ? promptOptions.autocomplete : null}
value={promptOptions && promptOptions.value ? promptOptions.value : ''}
defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''}
theme={this.props.theme}
style={promptStyle}
onClose={(answer, buttonType) => promptOptions.onClose(answer, buttonType)}
style={styles.prompt}
onClose={this.promptOnClose_}
label={promptOptions ? promptOptions.label : ''}
description={promptOptions ? promptOptions.description : null}
visible={!!this.state.promptOptions}
buttons={promptOptions && ('buttons' in promptOptions) ? promptOptions.buttons : null}
inputType={promptOptions && ('inputType' in promptOptions) ? promptOptions.inputType : null} />
<Header style={headerStyle} showBackButton={false} buttons={headerButtons} />
<SideBar style={sideBarStyle} />
<NoteList style={noteListStyle} />
<NoteText style={noteTextStyle} visiblePanes={this.props.noteVisiblePanes} />
<Header style={styles.header} showBackButton={false} buttons={headerButtons} />
<SideBar style={styles.sideBar} />
<NoteList style={styles.noteList} />
<NoteText style={styles.noteText} visiblePanes={this.props.noteVisiblePanes} />
</div>
);
}

View File

@ -7,6 +7,7 @@ const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const eventManager = require('../eventManager');
class NoteListComponent extends React.Component {
@ -69,6 +70,7 @@ class NoteListComponent extends React.Component {
for (let i = 0; i < noteIds.length; i++) {
const note = await Note.load(noteIds[i]);
await Note.save(Note.toggleIsTodo(note));
eventManager.emit('noteTypeToggle', { noteId: note.id });
}
}}));
@ -119,6 +121,7 @@ class NoteListComponent extends React.Component {
todo_completed: checked ? time.unixMs() : 0,
}
await Note.save(newNote);
eventManager.emit('todoToggle', { noteId: item.id });
}
const hPadding = 10;

View File

@ -1,7 +1,9 @@
const React = require('react');
const { Note } = require('lib/models/note.js');
const { time } = require('lib/time-utils.js');
const { Setting } = require('lib/models/setting.js');
const { IconButton } = require('./IconButton.min.js');
const Toolbar = require('./Toolbar.min.js');
const { connect } = require('react-redux');
const { _ } = require('lib/locale.js');
const { reg } = require('lib/registry.js');
@ -13,6 +15,7 @@ const AceEditor = require('react-ace').default;
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const { shim } = require('lib/shim.js');
const eventManager = require('../eventManager');
require('brace/mode/markdown');
// https://ace.c9.io/build/kitchen-sink.html
@ -55,6 +58,10 @@ class NoteTextComponent extends React.Component {
this.restoreScrollTop_ = null;
}
}
this.onAlarmChange_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
this.onNoteTypeToggle_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
this.onTodoToggle_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
}
mdToHtml() {
@ -82,6 +89,10 @@ class NoteTextComponent extends React.Component {
});
this.lastLoadedNoteId_ = note ? note.id : null;
eventManager.on('alarmChange', this.onAlarmChange_);
eventManager.on('noteTypeToggle', this.onNoteTypeToggle_);
eventManager.on('todoToggle', this.onTodoToggle_);
}
componentWillUnmount() {
@ -89,6 +100,10 @@ class NoteTextComponent extends React.Component {
this.mdToHtml_ = null;
this.destroyWebview();
eventManager.removeListener('alarmChange', this.onAlarmChange_);
eventManager.removeListener('noteTypeToggle', this.onNoteTypeToggle_);
eventManager.removeListener('todoToggle', this.onTodoToggle_);
}
async saveIfNeeded() {
@ -135,8 +150,8 @@ class NoteTextComponent extends React.Component {
// and then (in the renderer callback) to the value we actually need. The first
// operation helps clear the scroll position cache. See:
// https://github.com/ajaxorg/ace/issues/2195
this.editorSetScrollTop(1);
this.restoreScrollTop_ = 0;
this.editorSetScrollTop(1);
this.restoreScrollTop_ = 0;
this.setState({
note: note,
@ -315,6 +330,42 @@ class NoteTextComponent extends React.Component {
this.scheduleSave();
}
async commandAttachFile() {
const noteId = this.props.noteId;
if (!noteId) return;
const filePaths = bridge().showOpenDialog({
properties: ['openFile', 'createDirectory'],
});
if (!filePaths || !filePaths.length) return;
await this.saveIfNeeded();
const note = await Note.load(noteId);
try {
reg.logger().info('Attaching ' + filePaths[0]);
const newNote = await shim.attachFileToNote(note, filePaths[0]);
reg.logger().info('File was attached.');
this.setState({
note: newNote,
lastSavedNote: Object.assign({}, newNote),
});
} catch (error) {
reg.logger().error(error);
}
}
commandSetAlarm() {
const noteId = this.props.noteId;
if (!noteId) return;
this.props.dispatch({
type: 'WINDOW_COMMAND',
name: 'editAlarm',
noteId: noteId,
});
}
itemContextMenu(event) {
const noteId = this.props.noteId;
if (!noteId) return;
@ -322,38 +373,26 @@ class NoteTextComponent extends React.Component {
const menu = new Menu()
menu.append(new MenuItem({label: _('Attach file'), click: async () => {
const filePaths = bridge().showOpenDialog({
properties: ['openFile', 'createDirectory'],
});
if (!filePaths || !filePaths.length) return;
await this.saveIfNeeded();
const note = await Note.load(noteId);
try {
reg.logger().info('Attaching ' + filePaths[0]);
const newNote = await shim.attachFileToNote(note, filePaths[0]);
reg.logger().info('File was attached.');
this.setState({
note: newNote,
lastSavedNote: Object.assign({}, newNote),
});
} catch (error) {
reg.logger().error(error);
}
return this.commandAttachFile();
}}));
menu.append(new MenuItem({label: _('Set or clear alarm'), click: async () => {
this.props.dispatch({
type: 'WINDOW_COMMAND',
name: 'editAlarm',
noteId: noteId,
});
menu.append(new MenuItem({label: _('Set alarm'), click: async () => {
return this.commandSetAlarm();
}}));
menu.popup(bridge().window());
}
// shouldComponentUpdate(nextProps, nextState) {
// //console.info('NEXT PROPS', JSON.stringify(nextProps));
// console.info('NEXT STATE ====================');
// for (var n in nextProps) {
// if (!nextProps.hasOwnProperty(n)) continue;
// console.info(n + ' = ' + (nextProps[n] === this.props[n]));
// }
// return true;
// }
render() {
const style = this.props.style;
const note = this.state.note;
@ -385,7 +424,7 @@ class NoteTextComponent extends React.Component {
height: 30,
boxSizing: 'border-box',
marginTop: 10,
marginBottom: 10,
marginBottom: 0,
display: 'flex',
flexDirection: 'row',
};
@ -401,7 +440,11 @@ class NoteTextComponent extends React.Component {
marginRight: rootStyle.paddingLeft,
};
const bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop;
const toolbarStyle = {
marginBottom: 10,
};
const bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - toolbarStyle.marginBottom;
const viewerStyle = {
width: Math.floor(innerWidth / 2),
@ -455,6 +498,28 @@ class NoteTextComponent extends React.Component {
this.webview_.send('setHtml', html);
}
const toolbarItems = [];
toolbarItems.push({
title: _('Attach file'),
iconName: 'fa-paperclip',
onClick: () => { return this.commandAttachFile(); },
});
if (note.is_todo) {
toolbarItems.push({
title: Note.needAlarm(note) ? time.formatMsToLocal(note.todo_due) : _('Set alarm'),
iconName: 'fa-clock-o',
enabled: !note.todo_completed,
onClick: () => { return this.commandSetAlarm(); },
});
}
const toolbar = <Toolbar
style={toolbarStyle}
items={toolbarItems}
/>
const titleEditor = <input
type="text"
style={titleEditorStyle}
@ -509,6 +574,7 @@ class NoteTextComponent extends React.Component {
{ titleEditor }
{ titleBarMenuButton }
</div>
{ toolbar }
{ editor }
{ viewer }
</div>

View File

@ -10,19 +10,19 @@ class PromptDialog extends React.Component {
componentWillMount() {
this.setState({
visible: false,
answer: this.props.value ? this.props.value : '',
answer: this.props.defaultValue ? this.props.defaultValue : '',
});
this.focusInput_ = true;
}
componentWillReceiveProps(newProps) {
if ('visible' in newProps) {
if ('visible' in newProps && newProps.visible !== this.props.visible) {
this.setState({ visible: newProps.visible });
if (newProps.visible) this.focusInput_ = true;
}
if ('value' in newProps) {
this.setState({ answer: newProps.value });
if ('defaultValue' in newProps && newProps.defaultValue !== this.props.defaultValue) {
this.setState({ answer: newProps.defaultValue });
}
}
@ -31,38 +31,43 @@ class PromptDialog extends React.Component {
this.focusInput_ = false;
}
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);
const buttonTypes = this.props.buttons ? this.props.buttons : ['ok', 'cancel'];
styles(themeId, width, height, visible) {
const styleKey = themeId + '_' + width + '_' + height + '_' + visible;
if (styleKey === this.styleKey_) return this.styles_;
const modalLayerStyle = {
const theme = themeStyle(themeId);
this.styleKey_ = styleKey;
this.styles_ = {};
this.styles_.modalLayer = {
zIndex: 9999,
position: 'absolute',
top: 0,
left: 0,
width: style.width,
height: style.height,
width: width,
height: height,
backgroundColor: 'rgba(0,0,0,0.6)',
display: this.state.visible ? 'flex' : 'none',
display: visible ? 'flex' : 'none',
alignItems: 'center',
justifyContent: 'center',
};
const promptDialogStyle = {
this.styles_.promptDialog = {
backgroundColor: 'white',
padding: 16,
display: 'inline-block',
boxShadow: '6px 6px 20px rgba(0,0,0,0.5)',
};
const buttonStyle = {
this.styles_.button = {
minWidth: theme.buttonMinWidth,
minHeight: theme.buttonMinHeight,
marginLeft: 5,
};
const labelStyle = {
this.styles_.label = {
marginRight: 5,
fontSize: theme.fontSize,
color: theme.color,
@ -70,15 +75,43 @@ class PromptDialog extends React.Component {
verticalAlign: 'top',
};
const inputStyle = {
width: 0.5 * style.width,
this.styles_.input = {
width: 0.5 * width,
maxWidth: 400,
};
const descStyle = Object.assign({}, theme.textStyle, {
this.styles_.desc = Object.assign({}, theme.textStyle, {
marginTop: 10,
});
return this.styles_;
}
// shouldComponentUpdate(nextProps, nextState) {
// console.info(JSON.stringify(nextProps)+JSON.stringify(nextState));
// console.info('NEXT PROPS ====================');
// for (var n in nextProps) {
// if (!nextProps.hasOwnProperty(n)) continue;
// console.info(n + ' = ' + (nextProps[n] === this.props[n]));
// }
// console.info('NEXT STATE ====================');
// for (var n in nextState) {
// if (!nextState.hasOwnProperty(n)) continue;
// console.info(n + ' = ' + (nextState[n] === this.state[n]));
// }
// return true;
// }
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);
const buttonTypes = this.props.buttons ? this.props.buttons : ['ok', 'cancel'];
const styles = this.styles(this.props.theme, style.width, style.height, this.state.visible);
const onClose = (accept, buttonType) => {
if (this.props.onClose) this.props.onClose(accept ? this.state.answer : null, buttonType);
this.setState({ visible: false, answer: '' });
@ -100,7 +133,7 @@ class PromptDialog extends React.Component {
}
}
const descComp = this.props.description ? <div style={descStyle}>{this.props.description}</div> : null;
const descComp = this.props.description ? <div style={styles.desc}>{this.props.description}</div> : null;
let inputComp = null;
@ -113,7 +146,7 @@ class PromptDialog extends React.Component {
/>
} else {
inputComp = <input
style={inputStyle}
style={styles.input}
ref={input => this.answerInput_ = input}
value={this.state.answer}
type="text"
@ -123,14 +156,14 @@ class PromptDialog extends React.Component {
}
const buttonComps = [];
if (buttonTypes.indexOf('ok') >= 0) buttonComps.push(<button key="ok" style={buttonStyle} onClick={() => onClose(true, 'ok')}>{_('OK')}</button>);
if (buttonTypes.indexOf('cancel') >= 0) buttonComps.push(<button key="cancel" style={buttonStyle} onClick={() => onClose(false, 'cancel')}>{_('Cancel')}</button>);
if (buttonTypes.indexOf('clear') >= 0) buttonComps.push(<button key="clear" style={buttonStyle} onClick={() => onClose(false, 'clear')}>{_('Clear')}</button>);
if (buttonTypes.indexOf('ok') >= 0) buttonComps.push(<button key="ok" style={styles.button} onClick={() => onClose(true, 'ok')}>{_('OK')}</button>);
if (buttonTypes.indexOf('cancel') >= 0) buttonComps.push(<button key="cancel" style={styles.button} onClick={() => onClose(false, 'cancel')}>{_('Cancel')}</button>);
if (buttonTypes.indexOf('clear') >= 0) buttonComps.push(<button key="clear" style={styles.button} onClick={() => onClose(false, 'clear')}>{_('Clear')}</button>);
return (
<div style={modalLayerStyle}>
<div style={promptDialogStyle}>
<label style={labelStyle}>{this.props.label ? this.props.label : ''}</label>
<div style={styles.modalLayer}>
<div style={styles.promptDialog}>
<label style={styles.label}>{this.props.label ? this.props.label : ''}</label>
<div style={{display: 'inline-block'}}>
{inputComp}
{descComp}

View File

@ -0,0 +1,58 @@
const React = require('react');
const { connect } = require('react-redux');
const { reg } = require('lib/registry.js');
const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const ToolbarButton = require('./ToolbarButton.min.js');
class ToolbarComponent extends React.Component {
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);
style.height = theme.toolbarHeight;
style.display = 'flex';
style.flexDirection = 'row';
style.borderBottom = '1px solid ' + theme.dividerColor;
style.boxSizing = 'border-box';
const itemComps = [];
if (this.props.items) {
for (let i = 0; i < this.props.items.length; i++) {
const o = this.props.items[i];
let key = o.iconName ? o.iconName : '';
key += o.title ? o.title : '';
const itemType = !('type' in o) ? 'button' : o.type;
const props = Object.assign({
key: key,
theme: this.props.theme,
}, o);
if (itemType === 'button') {
itemComps.push(<ToolbarButton
{...props}
/>);
} else if (itemType === 'text') {
}
}
}
return (
<div className="editor-toolbar" style={style}>
{ itemComps }
</div>
);
}
}
const mapStateToProps = (state) => {
return { theme: state.settings.theme };
};
const Toolbar = connect(mapStateToProps)(ToolbarComponent);
module.exports = Toolbar;

View File

@ -0,0 +1,59 @@
const React = require('react');
const { connect } = require('react-redux');
const { themeStyle } = require('../theme.js');
class ToolbarButton extends React.Component {
render() {
//const style = this.props.style;
const theme = themeStyle(this.props.theme);
const style = {
height: theme.toolbarHeight,
minWidth: theme.toolbarHeight,
display: 'flex',
alignItems: 'center',
paddingLeft: theme.headerButtonHPadding,
paddingRight: theme.headerButtonHPadding,
color: theme.color,
textDecoration: 'none',
fontFamily: theme.fontFamily,
fontSize: theme.fontSize,
boxSizing: 'border-box',
cursor: 'default',
justifyContent: 'center',
};
let icon = null;
if (this.props.iconName) {
const iconStyle = {
fontSize: Math.round(theme.fontSize * 1.4),
color: theme.color
};
if (this.props.title) iconStyle.marginRight = 5;
icon = <i style={iconStyle} className={"fa " + this.props.iconName}></i>
}
const isEnabled = (!('enabled' in this.props) || this.props.enabled === true);
let classes = ['button'];
if (!isEnabled) classes.push('disabled');
const finalStyle = Object.assign({}, style, {
opacity: isEnabled ? 1 : 0.4,
});
return (
<a
className={classes.join(' ')}
style={finalStyle}
href="#"
onClick={() => { if (isEnabled && this.props.onClick) this.props.onClick() }}
>
{icon}{this.props.title ? this.props.title : ''}
</a>
);
}
}
module.exports = ToolbarButton;

View File

@ -31,18 +31,21 @@ body, textarea {
background-color: #564B6C;
}
.editor-toolbar .button:not(.disabled):hover,
.header .button:not(.disabled):hover {
background-color: rgba(0,160,255,0.1);
border: 1px solid rgba(0,160,255,0.5);
box-sizing: 'border-box';
}
.editor-toolbar .button:not(.disabled):active,
.header .button:not(.disabled):active {
background-color: rgba(0,160,255,0.2);
border: 1px solid rgba(0,160,255,0.7);
box-sizing: 'border-box';
}
.editor-toolbar .button,
.header .button {
border: 1px solid rgba(0,160,255,0);
}

View File

@ -28,6 +28,8 @@ const globalStyle = {
headerHeight: 35,
headerButtonHPadding: 6,
toolbarHeight: 35,
raisedBackgroundColor: "#0080EF",
raisedColor: "#003363",
raisedHighlightedColor: "#ffffff",