diff --git a/app/components/CodeMirrorEditor/index.jsx b/app/components/CodeMirrorEditor/index.jsx index 1d850ff9..d5edca01 100644 --- a/app/components/CodeMirrorEditor/index.jsx +++ b/app/components/CodeMirrorEditor/index.jsx @@ -1,7 +1,7 @@ -/*@flow weak*/ import React, {Component} from 'react'; import CodeMirror from 'codemirror'; -import {connect} from 'react-redux'; +import { inject, observer } from 'mobx-react' +import { dispatch } from 'store' import './addons'; import dispatchCommand from '../../commands/dispatchCommand' import _ from 'lodash' @@ -39,10 +39,10 @@ function getMode (tab) { const debounced = _.debounce(func => func(), 1000) -@connect(state => ({ - setting: state.SettingState.views.tabs.EDITOR, - themeSetting: state.SettingState.views.tabs.THEME, +@inject(state => ({ + themeName: state.SettingState.settings.theme.syntax_theme.value, })) +@observer class CodeMirrorEditor extends Component { static defaultProps = { theme: 'default', @@ -56,15 +56,14 @@ class CodeMirrorEditor extends Component { } componentDidMount () { - const { themeSetting, tab } = this.props; - const themeConfig = themeSetting.items[1].value + const { themeName, tab } = this.props; let editorInitialized = false // todo add other setting item from config if (tab.editor) { this.editor = tab.editor this.editorContainer.appendChild(this.editor.getWrapperElement()) } else { - this.editor = tab.editor = initializeEditor(this.editorContainer, themeConfig) + this.editor = tab.editor = initializeEditor(this.editorContainer, themeName) editorInitialized = true } const editor = this.editor @@ -100,7 +99,7 @@ class CodeMirrorEditor extends Component { onChange = (e) => { if (!this.isChanging) this.isChanging = true - const {tab, dispatch} = this.props; + const { tab } = this.props; dispatch(TabActions.updateTab({ id: tab.id, flags: { modified: true }, @@ -113,17 +112,17 @@ class CodeMirrorEditor extends Component { } onFocus = () => { - this.props.dispatch(TabActions.activateTab(this.props.tab.id)) + dispatch(TabActions.activateTab(this.props.tab.id)) } - componentWillReceiveProps ({ tab, themeSetting }) { + componentWillReceiveProps ({ tab, themeName }) { if (tab.flags.modified || !this.editor || !tab.content) return if (tab.content.body !== this.editor.getValue()) { this.editor.setValue(tab.content.body) } - const nextTheme = themeSetting.items[1].value - const theme = this.props.themeSetting.items[1].value + const nextTheme = themeName + const theme = this.props.themeName if (theme !== nextTheme) this.editor.setOption('theme', nextTheme) } @@ -142,10 +141,10 @@ class CodeMirrorEditor extends Component { } -@connect(state => ({ - setting: state.SettingState.views.tabs.EDITOR, - themeSetting: state.SettingState.views.tabs.THEME, +@inject(state => ({ + themeName: state.SettingState.settings.theme.syntax_theme.value, })) +@observer class TablessCodeMirrorEditor extends Component { constructor (props) { super(props) @@ -153,24 +152,23 @@ class TablessCodeMirrorEditor extends Component { } componentDidMount() { - const { themeSetting, width, height } = this.props - const theme = themeSetting.items[1].value + const { themeName, width, height } = this.props - this.editor = initializeEditor(this.editorContainer, theme) + this.editor = initializeEditor(this.editorContainer, themeName) this.editor.focus() this.editor.on('change', this.onChange) } onChange = (e) => { - this.props.dispatch(TabActions.createTabInGroup(this.props.tabGroupId, { + dispatch(TabActions.createTabInGroup(this.props.tabGroupId, { flags: { modified: true }, content: this.editor.getValue() })) } - componentWillReceiveProps ({ themeSetting }) { - const nextTheme = themeSetting.items[1].value - const theme = this.props.themeSetting.items[1].value + componentWillReceiveProps ({ themeName }) { + const nextTheme = themeName + const theme = this.props.themeName if (theme !== nextTheme) this.editor.setOption('theme', nextTheme) } diff --git a/app/components/Setting/SettingForm.jsx b/app/components/Setting/SettingForm.jsx new file mode 100644 index 00000000..fbb7b524 --- /dev/null +++ b/app/components/Setting/SettingForm.jsx @@ -0,0 +1,94 @@ +import React, { Component } from 'react' +import { inject, observer } from 'mobx-react' +import { runInAction } from 'mobx' +import cx from 'classnames' +import _ from 'lodash' + +@observer +class SettingForm extends Component { + constructor (props) { + super(props) + } + + updateSettingItemBind = settingItem => { + let update + if (this.props.setting.requireConfirm) { + update = value => settingItem.tempValue = value + } else { + update = value => settingItem.value = value + } + return (e) => { + const value = (() => { + switch (e.target.type) { + case 'checkbox': + return e.target.checked + case 'number': + return Number(e.target.value) + case 'text': + case 'select-one': + default: + return e.target.value + } + })() + update(value) + } + } + + render () { + const { setting } = this.props + return
+ {setting.items.map(settingItem => + + )} +
+ } +} + +const FormInputGroup = observer(({ settingItem, updateSettingItem }) => { + if (settingItem.options && _.isArray(settingItem.options)) { + return ( +
+ + +
) + } else if (_.isBoolean(settingItem.value)) { + return ( +
+
+ + +
+
) + } else { + return ( +
+ + +
) + } +}) + +export default SettingForm diff --git a/app/components/Setting/actions.js b/app/components/Setting/actions.js deleted file mode 100644 index b28f2d95..00000000 --- a/app/components/Setting/actions.js +++ /dev/null @@ -1,24 +0,0 @@ -import { createAction } from 'redux-actions' - -export const SETTING_ACTIVATE_TAB = 'SETTING_ACTIVATE_TAB' -export const activateSettingTab = createAction(SETTING_ACTIVATE_TAB) - -export const SETTING_UPDATE_FIELD = 'SETTING_UPDATE_FIELD' -export const CONFIRM_UPDATE_FIELD = 'CONFIRM_UPDATE_FIELD' -export const CANCEL_UPDATE_FIELD = 'CANCEL_UPDATE_FIELD' - - -export const updateSettingItem = createAction(SETTING_UPDATE_FIELD, - (domain, fieldName, value) => ({ domain, fieldName, value }) -) -// export const confirmSettingItem = createAction(CONFIRM_UPDATE_FIELD) - -export const confirmSettingItem = () => dispatch => { - dispatch({ type: 'MODAL_DISMISS' }) - dispatch({ type: CONFIRM_UPDATE_FIELD }) -} -export const cancelSettingItem = () => dispatch => { - dispatch({ type: 'MODAL_DISMISS' }) - dispatch({ type: CANCEL_UPDATE_FIELD }) -} - diff --git a/app/components/Setting/index.jsx b/app/components/Setting/index.jsx index 5ba9abb3..c97e5415 100644 --- a/app/components/Setting/index.jsx +++ b/app/components/Setting/index.jsx @@ -1,142 +1,62 @@ /* @flow weak */ import React from 'react' -import { bindActionCreators } from 'redux' -import { connect } from 'react-redux' +import { inject, observer } from 'mobx-react' import cx from 'classnames' -import _ from 'lodash' -import ExtensionList from '../Package/extensionList.js'; +import ExtensionList from '../Package/extensionList' +import SettingForm from './SettingForm' -import * as SettingActions from './actions' - -let FormGroupFactory = ({ domain, settingItem, dispatch }) => { - let formComponent - const updateSettingItem = e => { - const value = (() => { - switch (e.target.type) { - case 'checkbox': - return e.target.checked - case 'number': - return Number(e.target.value) - case 'text': - return e.target.value - case 'select-one': - return e.target.value - } - })() - - return dispatch( - SettingActions.updateSettingItem(domain, settingItem.name, value) - ) - } - - if (settingItem.options && _.isArray(settingItem.options)) { - return ( -
- - -
) - } else if (_.isBoolean(settingItem.value)) { - return ( -
-
- - -
-
) - } else { - return ( -
- - -
) - } -} -FormGroupFactory = connect(null)(FormGroupFactory) - -const GeneralSettings = (props) => ( +const GeneralSetting = ({ content }) => { + return (
-

General Settings

- {props.items.map(settingItem => - - )} +

General Setting

+
) +} -const EditorSettings = (props) => ( +const EditorSetting = ({ content }) => (
-

Editor Settings

- {props.items.map(settingItem => - - )} +

Editor Setting

+
) -const ThemeSettings = (props) => ( +const ThemeSetting = ({ content }) => (
-

Theme Settings

- {props.items.map(settingItem => - - )} +

Theme Setting

+
) -const ExtensionSettings = () => ( +const ExtensionSetting = () => (
-

Extension Settings

+

Extension Setting

) -const SettingsContent = ({ content }) => { - switch (content.id) { +const DomainSetting = ({ content, domainKey }) => { + switch (domainKey) { case 'GENERAL': default: - return + return case 'EDITOR': - return + return case 'THEME': - return + return case 'EXTENSIONS': - return + return } } -let SettingsView = (props) => { +let SettingsView = observer(props => { const { - views: { activeTabId, tabIds, tabs }, - activateSettingTab, - confirmSettingItem, - cancelSettingItem -} = props + activeTabId, tabIds, activeTab, activateTab, + } = props + + const onConfirm = () => activeTab.onConfirm && activeTab.onConfirm() + const onCancel = () => activeTab.onCancel && activeTab.onCancel() return (
@@ -147,38 +67,30 @@ let SettingsView = (props) => { {tabIds.map(tabId =>
  • activateSettingTab(tabId)} + onClick={e => activateTab(tabId)} >{tabId}
  • )}
    -
    +
    - +
    + {activeTab.requireConfirm &&
    + + +
    }
    -
    - - -
    - ) -} -SettingsView = connect( - state => (state.SettingState), - dispatch => bindActionCreators(SettingActions, dispatch) -)(SettingsView) +}) +SettingsView = inject(state => { + const { activeTabId, tabIds, activeTab, activateTab } = state.SettingState + return { activeTabId, tabIds, activeTab, activateTab } +})(SettingsView) export default SettingsView diff --git a/app/components/Setting/reducer.js b/app/components/Setting/reducer.js deleted file mode 100644 index 08bdc412..00000000 --- a/app/components/Setting/reducer.js +++ /dev/null @@ -1,152 +0,0 @@ -/* @flow weak */ -import { handleActions } from 'redux-actions' -import { OrderedMap } from 'immutable' -import { changeTheme } from '../../utils/themeManager' -import { - SETTING_ACTIVATE_TAB, - SETTING_UPDATE_FIELD, - CONFIRM_UPDATE_FIELD, - CANCEL_UPDATE_FIELD -} from './actions' - -const langCodes = { - en_US: 'English', - zh_CN: 'Chinese' -} - -const getDefaultLangCode = () => { - return [ - 'languages', - 'language', - 'browserLanguage', - 'systemLanguage', - 'userLanguage', - ].reduce((defaultLangCode, attr) => { - if (defaultLangCode) return defaultLangCode - let languages = window.navigator[attr] - if (!Array.isArray(languages)) languages = [languages] - return languages.reduce((defaultLangCode, lang) => { - if (!lang) return defaultLangCode - lang = lang.replace(/-/g, '_') - if (Object.keys(langCodes).includes(lang)) return lang - return defaultLangCode - }, '') - }, '') -} - -export const UIThemeOptions = ['base-theme', 'dark'] -export const SyntaxThemeOptions = ['default', 'neo', 'eclipse', 'monokai', 'material'] - -const SettingState = { - activeTabId: 'GENERAL', - tabIds: ['GENERAL', 'THEME', 'EDITOR', 'EXTENSIONS'], - tabs: { - THEME: { - id: 'THEME', - items: [{ - name: 'UI Theme', - value: 'Light', - options: UIThemeOptions - }, { - name: 'Syntax Theme', - value: 'default', - options: SyntaxThemeOptions - }] - }, - EXTENSIONS: { - id: 'EXTENSIONS' - }, - GENERAL: { - id: 'GENERAL', - items: [{ - name: 'Language', - value: langCodes[getDefaultLangCode()], - options: ['English', 'Chinese'] - }, { - name: 'Hide Files', - value: '/.git,/.coding-ide' - }] - }, - EDITOR: { - id: 'EDITOR', - items: [{ - name: 'Keyboard Mode', - value: 'Default', - options: ['Default', 'Vim', 'Emacs'] - }, { - name: 'Font Size', - value: 14 - }, { - name: 'Font Family', - value: 'Consolas', - options: ['Consolas', 'Courier', 'Courier New', 'Menlo'] - }, { - name: 'Charset', - value: 'utf8', - options: [ - {name: 'Unicode (UTF-8)', value: 'utf8'}, - {name: '中文简体 (GB18030)', value: 'gb18030'}, - {name: '中文繁体 (Big5-HKSCS)', value: 'big5'} - ] - }, { - name: 'Soft Tab', - value: true - }, { - name: 'Tab Size', - value: 4, - options: [1,2,3,4,5,6,7,8] - }, { - name: 'Auto Save', - value: true - }, { - name: 'Auto Wrap', - value: false - }, { - name: 'Live Auto Completion', - value: true - }, { - name: 'Snippets', - value: false - }] - } - } -} - -export default handleActions({ - [SETTING_ACTIVATE_TAB]: (state, action) => { - return ({ - ...state, - views: { ...state.views, activeTabId: action.payload } - }) - }, - - [SETTING_UPDATE_FIELD]: (state, action) => { - const { domain, fieldName, value } = action.payload - if (fieldName === 'UI Theme') { changeTheme(value); } - return { - ...state, - views: { ...state.views, - tabs: { - ...state.views.tabs, - [domain]: { - ...state.views.tabs[domain], - items: state.views.tabs[domain].items.map(settingItem => { - if (settingItem.name === fieldName) { - return { ...settingItem, value } - } - return settingItem - }) - } - } - } - } - }, - [CONFIRM_UPDATE_FIELD]: (state) => ({ - ...state, - data: { ...state.data, ...state.views } - }), - [CANCEL_UPDATE_FIELD]: (state) => ({ - ...state, - views: { ...state.views, ...state.data } - }), -}, { views: SettingState, data: SettingState }) diff --git a/app/components/Setting/state.js b/app/components/Setting/state.js new file mode 100644 index 00000000..e1a57624 --- /dev/null +++ b/app/components/Setting/state.js @@ -0,0 +1,16 @@ +import { observable, action } from 'mobx' +import settings from 'settings' + +const state = observable({ + activeTabId: 'GENERAL', + tabIds: ['GENERAL', 'THEME', 'EDITOR', 'EXTENSIONS'], + get activeTab () { + return settings[this.activeTabId.toLowerCase()] + }, + settings, + activateTab: action((tabId) => { + state.activeTabId = tabId + }) +}) + +export default state diff --git a/app/mobxStore.js b/app/mobxStore.js index a875b473..565407a4 100644 --- a/app/mobxStore.js +++ b/app/mobxStore.js @@ -2,10 +2,12 @@ import { autorun, createTransformer, toJS } from 'mobx' import PaneState from './components/Pane/state' import EditorTabState from './components/Editor/state' import FileTreeState from './components/FileTree/state' +import SettingState from './components/Setting/state' const store = { PaneState, EditorTabState, FileTreeState, + SettingState, } const transform = createTransformer(store => { diff --git a/app/settings.js b/app/settings.js new file mode 100644 index 00000000..78b45bdc --- /dev/null +++ b/app/settings.js @@ -0,0 +1,211 @@ +import isObject from 'lodash/isObject' +import emitter, { THEME_CHANGED } from 'utils/emitter' +import { observable, extendObservable, computed, action, autorunAsync } from 'mobx' + +export const UIThemeOptions = ['base-theme', 'dark'] +export const SyntaxThemeOptions = ['default', 'neo', 'eclipse', 'monokai', 'material'] + +const changeTheme = (nextThemeId, force) => { + if (!window.themes) window.themes = {} + if (UIThemeOptions.includes(nextThemeId)) { + import(`!!style-loader/useable!css-loader!stylus-loader!./styles/${nextThemeId}/index.styl`).then(module => { + const currentTheme = window.themes['@current'] + if (currentTheme && currentTheme.unuse) currentTheme.unuse() + window.themes['@current'] = window.themes[nextThemeId] = module + module.use() + }) + } + emitter.emit(THEME_CHANGED, nextThemeId) +} + +const localeToLangs = { + en_US: 'English', + zh_CN: 'Chinese' +} + +const getDefaultLangCode = () => { + const langProps = [ + 'languages', + 'language', + 'browserLanguage', + 'systemLanguage', + 'userLanguage', + ] + return langProps.reduce((defaultLangCode, attr) => { + if (defaultLangCode) return defaultLangCode + let languages = window.navigator[attr] + if (!Array.isArray(languages)) languages = [languages] + return languages.reduce((defaultLangCode, lang) => { + if (!lang) return defaultLangCode + lang = lang.replace(/-/g, '_') + if (Object.keys(localeToLangs).includes(lang)) return lang + return defaultLangCode + }, '') + }, '') +} + +const titleCase = (snake_case_str) => // eslint-disable-line + snake_case_str.split('_') + .map(str => str.charAt(0).toUpperCase() + str.substr(1)) + .join(' ') + + +class DomainSetting { + constructor (config) { + Object.entries(config).forEach(([key, settingItem]) => { + if (!isObject(settingItem)) return + settingItem.key = key + settingItem.name = settingItem.name || titleCase(key) + if (settingItem.options) { + // don't auto-convert 'options' to observable + settingItem.options = observable.ref(settingItem.options) + } + + if (config.requireConfirm) { + settingItem.tempValue = undefined + } + }) + extendObservable(this, config) + } + + @observable _keys = []; + @computed + get items () { + return this._keys.map(key => this[key]) + } + + @action.bound + onConfirm () { + this.items.forEach(item => { + if (item.tempValue !== undefined) { + item.value = item.tempValue + item.tempValue = undefined + } + }) + } + + @action.bound + onCancel () { + this.items.forEach(item => { + if (item.tempValue !== undefined) { + item.tempValue = undefined + } + }) + } + + @computed + get unsaved () { + if (!this.requireConfirm) return false + return this.items.reduce((bool, item) => { + if (bool) return bool + return item.tempValue !== undefined + }, false) + } +} + + +const settings = observable({ + _keys: ['theme', 'extensions', 'general', 'editor'], + get items () { + return this._keys.map(key => this[key]) + }, + theme: new DomainSetting({ + _keys: ['ui_theme', 'syntax_theme'], + ui_theme: { + name: 'UI Theme', + value: 'Light', + options: UIThemeOptions + }, + syntax_theme: { + name: 'Syntax Theme', + value: 'default', + options: SyntaxThemeOptions + } + }), + + extensions: new DomainSetting({}), + + general: new DomainSetting({ + _keys: ['language', 'hide_files'], + requireConfirm: true, + language: { + name: 'Language', + value: localeToLangs[getDefaultLangCode()], + options: ['English', 'Chinese'] + }, + hide_files: { + name: 'Hide Files', + value: '/.git,/.coding-ide' + } + }), + + editor: new DomainSetting({ + _keys: [ + 'keyboard_mode', + 'font_size', + 'font_family', + 'charset', + 'soft_tab', + 'tab_size', + 'auto_save', + 'auto_wrap', + 'live_auto_completion', + 'snippets', + ], + + keyboard_mode: { + name: 'Keyboard Mode', + value: 'Default', + options: ['Default', 'Vim', 'Emacs'] + }, + font_size: { + name: 'Font Size', + value: 14 + }, + font_family: { + name: 'Font Family', + value: 'Consolas', + options: ['Consolas', 'Courier', 'Courier New', 'Menlo'] + }, + charset: { + name: 'Charset', + value: 'utf8', + options: [ + {name: 'Unicode (UTF-8)', value: 'utf8'}, + {name: '中文简体 (GB18030)', value: 'gb18030'}, + {name: '中文繁体 (Big5-HKSCS)', value: 'big5'}, + ] + }, + soft_tab: { + name: 'Soft Tab', + value: true + }, + tab_size: { + name: 'Tab Size', + value: 4, + options: [1,2,3,4,5,6,7,8], + }, + auto_save: { + name: 'Auto Save', + value: true + }, + auto_wrap: { + name: 'Auto Wrap', + value: false + }, + live_auto_completion: { + name: 'Live Auto Completion', + value: true + }, + snippets: { + name: 'Snippets', + value: false + } + }) +}) + +export default settings + +autorunAsync('changeTheme', () => { + changeTheme(settings.theme.ui_theme.value) +}) diff --git a/app/store.js b/app/store.js index 5a60903f..ebeaea72 100644 --- a/app/store.js +++ b/app/store.js @@ -14,7 +14,6 @@ import ModalsReducer from './components/Modal/reducer' import NotificationReducer from './components/Notification/reducer' import TerminalReducer from './components/Terminal/reducer' import GitReducer from './components/Git/reducer' -import SettingReducer from './components/Setting/reducer' import RootReducer from './containers/Root/reducer' import PackageReducer, { PackageCrossReducer } from './components/Package/reducer' import StatusBarReducer from './components/StatusBar/reducer' @@ -33,7 +32,6 @@ const combinedReducers = combineReducers({ TerminalState: TerminalReducer, GitState: GitReducer, NotificationState: NotificationReducer, - SettingState: SettingReducer, StatusBarState: StatusBarReducer, }) diff --git a/app/styles/core-ui/Settings.styl b/app/styles/core-ui/Settings.styl index aa1f82ee..2089faaa 100644 --- a/app/styles/core-ui/Settings.styl +++ b/app/styles/core-ui/Settings.styl @@ -9,18 +9,6 @@ } } -.settings-view { - .modal-ops { - position: relative; - margin-top: 10px; - padding-top: 10px; - padding-right: 8px; - border-top: 1px solid #eee; - margin-left: -10px; - margin-right: -10px; - } -} - .settings-header { border-right: 1px solid #ccc; width: 25%; @@ -62,8 +50,19 @@ .settings-content { width: 75%; + position: relative; + display: flex; + flex-direction: column; + $padding-lr= 18px .settings-content-container { - padding: 0 24px; + padding: 0 $padding-lr 10px; + flex-grow: 1; + overflow: scroll; + } + .settings-content-controls { + flex-grow: 0; + padding: 10px $padding-lr - 2px; + border-top: 1px solid #eee; } } @@ -114,4 +113,4 @@ } } } -} \ No newline at end of file +} diff --git a/app/utils/createI18n.js b/app/utils/createI18n.js index 677f7d64..7b53ba7f 100644 --- a/app/utils/createI18n.js +++ b/app/utils/createI18n.js @@ -1,5 +1,6 @@ import React, { PropTypes } from 'react' -import { connect } from 'react-redux' +import settings from 'settings' +import { inject } from 'mobx-react' import * as languageDicPool from '../i18n' const separator = ':=' @@ -32,8 +33,7 @@ const mapStateToProps = (state) => { English: 'en_US', Chinese: 'zh_CN' } - const languageSettings = state.SettingState.data.tabs.GENERAL.items - const languageSetting = languageSettings.find(e => e.name === 'Language') + const languageSetting = settings.general.language const language = languageToCode[languageSetting.value] return ({ language }) } @@ -46,5 +46,5 @@ export default (template = [], ...values) => { translateComponent.propTypes = { language: PropTypes.string } - return React.createElement(connect(mapStateToProps)(translateComponent)) + return React.createElement(inject(mapStateToProps)(translateComponent)) } diff --git a/app/utils/themeManager.js b/app/utils/themeManager.js deleted file mode 100644 index d1b686e4..00000000 --- a/app/utils/themeManager.js +++ /dev/null @@ -1,31 +0,0 @@ -import { UIThemeOptions } from '../components/Setting/reducer' -import { getState } from '../store' -import { emitter, E } from 'utils' - -export const changeTheme = (nextThemeId, force) => { - const currentThemeId = getState().SettingState.views.tabs.THEME.items[0].value - if (!window.themes) window.themes = {} - - if (nextThemeId !== currentThemeId || force) { - if (UIThemeOptions.includes(nextThemeId)) { - import(`!!style-loader/useable!css-loader!stylus-loader!../styles/${nextThemeId}/index.styl`).then(module => { - const currentTheme = window.themes['@current'] - if (currentTheme && currentTheme.unuse) currentTheme.unuse() - window.themes['@current'] = window.themes[nextThemeId] = module - module.use() - }) - } - } - emitter.emit(E.THEME_CHANGED, nextThemeId) -} - -export const changeCodeTheme = (next) => { - const nextTheme = next.split('/').pop() - const currentThemeValue = getState().SettingState.views.tabs.THEME.items[1].value - const editors = window.ide.editors - if (Object.keys(editors).length && nextTheme !== currentThemeValue) { - Object.keys(editors).forEach(editor => { - editors[editor].setOption('theme', nextTheme) - }); - } -}