import * as React from '@theia/core/shared/react'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import 'react-tabs/style/react-tabs.css'; import { Disable } from 'react-disable'; import { deepClone } from '@theia/core/lib/common/objects'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { ThemeService } from '@theia/core/lib/browser/theming'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/file-dialog-service'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { AdditionalUrls, CompilerWarningLiterals, Network, ProxySettings, } from '../../../common/protocol'; import { nls } from '@theia/core/lib/common'; import { Settings, SettingsService } from './settings'; import { AdditionalUrlsDialog } from './settings-dialog'; import { AsyncLocalizationProvider, LanguageInfo, } from '@theia/core/lib/common/i18n/localization'; import SettingsStepInput from './settings-step-input'; const maxScale = 200; const minScale = -100; const scaleStep = 20; const maxFontSize = 72; const minFontSize = 0; const fontSizeStep = 2; export class SettingsComponent extends React.Component< SettingsComponent.Props, SettingsComponent.State > { readonly toDispose = new DisposableCollection(); constructor(props: SettingsComponent.Props) { super(props); } override componentDidUpdate( _: SettingsComponent.Props, prevState: SettingsComponent.State ): void { if ( this.state && prevState && JSON.stringify(SettingsComponent.State.toSettings(this.state)) !== JSON.stringify(SettingsComponent.State.toSettings(prevState)) ) { this.props.settingsService.update( SettingsComponent.State.toSettings(this.state), true ); } } override componentDidMount(): void { this.props.settingsService .settings() .then((settings) => this.setState(SettingsComponent.State.fromSettings(settings)) ); this.toDispose.pushAll([ this.props.settingsService.onDidChange((settings) => this.setState((prevState) => ({ ...SettingsComponent.State.merge(prevState, settings), })) ), this.props.settingsService.onDidReset((settings) => this.setState(SettingsComponent.State.fromSettings(settings)) ), ]); } override componentWillUnmount(): void { this.toDispose.dispose(); } override render(): React.ReactNode { if (!this.state) { return <div />; } return ( <Tabs> <TabList> <Tab>{nls.localize('vscode/settingsTree/settings', 'Settings')}</Tab> <Tab>{nls.localize('arduino/preferences/network', 'Network')}</Tab> </TabList> <TabPanel>{this.renderSettings()}</TabPanel> <TabPanel>{this.renderNetwork()}</TabPanel> </Tabs> ); } protected renderSettings(): React.ReactNode { const scalePercentage = 100 + this.state.interfaceScale * 20; return ( <div className="content noselect"> {nls.localize( 'arduino/preferences/sketchbook.location', 'Sketchbook location' ) + ':'} <div className="flex-line"> <input className="theia-input stretch" type="text" value={this.state.sketchbookPath} onChange={this.sketchpathDidChange} /> <button className="theia-button shrink" onClick={this.browseSketchbookDidClick} > {nls.localize('arduino/preferences/browse', 'Browse')} </button> </div> <label className="flex-line"> <input type="checkbox" checked={this.state.sketchbookShowAllFiles === true} onChange={this.sketchbookShowAllFilesDidChange} /> {nls.localize( 'arduino/preferences/files.inside.sketches', 'Show files inside Sketches' )} </label> <div className="column-container"> <div className="column"> <div className="flex-line"> {nls.localize( 'arduino/preferences/editorFontSize', 'Editor font size' ) + ':'} </div> <div className="flex-line"> {nls.localize( 'arduino/preferences/interfaceScale', 'Interface scale' ) + ':'} </div> <div className="flex-line"> {nls.localize( 'vscode/themes.contribution/selectTheme.label', 'Theme' ) + ':'} </div> <div className="flex-line"> {nls.localize( 'vscode/editorStatus/status.editor.mode', 'Language' ) + ':'} </div> <div className="flex-line"> {nls.localize( 'arduino/preferences/showVerbose', 'Show verbose output during' )} </div> <div className="flex-line"> {nls.localize( 'arduino/preferences/compilerWarnings', 'Compiler warnings' )} </div> </div> <div className="column"> <div className="flex-line"> <SettingsStepInput value={this.state.editorFontSize} setSettingsStateValue={this.setFontSize} step={fontSizeStep} maxValue={maxFontSize} minValue={minFontSize} classNames={{ input: 'theia-input small' }} /> </div> <div className="flex-line"> <label className="flex-line"> <input type="checkbox" checked={this.state.autoScaleInterface} onChange={this.autoScaleInterfaceDidChange} /> {nls.localize('arduino/preferences/automatic', 'Automatic')} </label> <SettingsStepInput value={scalePercentage} setSettingsStateValue={this.setInterfaceScale} step={scaleStep} maxValue={maxScale} minValue={minScale} classNames={{ input: 'theia-input small with-margin' }} /> % </div> <div className="flex-line"> <select className="theia-select" value={ThemeService.get().getCurrentTheme().label} onChange={this.themeDidChange} > {ThemeService.get() .getThemes() .map(({ id, label }) => ( <option key={id} value={label}> {label} </option> ))} </select> </div> <div className="flex-line"> <select className="theia-select" value={this.state.currentLanguage} onChange={this.languageDidChange} > {this.state.languages.map((label) => this.toSelectOptions(label) )} </select> <span style={{ marginLeft: '5px' }}> ( {nls.localize( 'vscode/extensionsActions/reloadRequired', 'Reload required' )} ) </span> </div> <div className="flex-line"> <label className="flex-line"> <input type="checkbox" checked={this.state.verboseOnCompile} onChange={this.verboseOnCompileDidChange} /> {nls.localize('arduino/preferences/compile', 'compile')} </label> <label className="flex-line"> <input type="checkbox" checked={this.state.verboseOnUpload} onChange={this.verboseOnUploadDidChange} /> {nls.localize('arduino/preferences/upload', 'upload')} </label> </div> <div className="flex-line"> <select className="theia-select" value={this.state.compilerWarnings} onChange={this.compilerWarningsDidChange} > {CompilerWarningLiterals.map((value) => ( <option key={value} value={value}> {value} </option> ))} </select> </div> </div> </div> <label className="flex-line"> <input type="checkbox" checked={this.state.verifyAfterUpload} onChange={this.verifyAfterUploadDidChange} /> {nls.localize( 'arduino/preferences/verifyAfterUpload', 'Verify code after upload' )} </label> <label className="flex-line"> <input type="checkbox" checked={this.state.autoSave !== 'off'} onChange={this.autoSaveDidChange} /> {nls.localize( 'vscode/fileActions.contribution/miAutoSave', 'Auto save' )} </label> <label className="flex-line"> <input type="checkbox" checked={this.state.quickSuggestions.other === true} onChange={this.quickSuggestionsOtherDidChange} /> {nls.localize( 'arduino/preferences/editorQuickSuggestions', 'Editor Quick Suggestions' )} </label> <div className="flex-line"> {nls.localize( 'arduino/preferences/additionalManagerURLs', 'Additional boards manager URLs' ) + ':'} <input className="theia-input stretch with-margin" type="text" value={this.state.rawAdditionalUrlsValue} onChange={this.rawAdditionalUrlsValueDidChange} /> <i className="fa fa-window-restore theia-button shrink" onClick={this.editAdditionalUrlDidClick} /> </div> </div> ); } private toSelectOptions(language: string | LanguageInfo): JSX.Element { const plain = typeof language === 'string'; const key = plain ? language : language.languageId; const value = plain ? language : language.languageId; const label = plain ? language === 'en' ? 'English' : language : language.localizedLanguageName || language.languageName || language.languageId; return ( <option key={key} value={value}> {label} </option> ); } protected renderNetwork(): React.ReactNode { return ( <div className="content noselect"> <form> <label className="flex-line"> <input type="radio" checked={this.state.network === 'none'} onChange={this.noProxyDidChange} /> {nls.localize('arduino/preferences/noProxy', 'No proxy')} </label> <label className="flex-line"> <input type="radio" checked={this.state.network !== 'none'} onChange={this.manualProxyDidChange} /> {nls.localize( 'arduino/preferences/manualProxy', 'Manual proxy configuration' )} </label> </form> {this.renderProxySettings()} </div> ); } protected renderProxySettings(): React.ReactNode { const disabled = this.state.network === 'none'; return ( <Disable disabled={disabled}> <div className="proxy-settings" aria-disabled={disabled}> <form className="flex-line"> <input type="radio" checked={ this.state.network === 'none' ? true : this.state.network.protocol === 'http' } onChange={this.httpProtocolDidChange} /> HTTP <label className="flex-line"> <input type="radio" checked={ this.state.network === 'none' ? false : this.state.network.protocol !== 'http' } onChange={this.socksProtocolDidChange} /> SOCKS </label> </form> <div className="flex-line proxy-settings"> <div className="column"> <div className="flex-line">Host name:</div> <div className="flex-line">Port number:</div> <div className="flex-line">Username:</div> <div className="flex-line">Password:</div> </div> <div className="column stretch"> <div className="flex-line"> <input className="theia-input stretch with-margin" type="text" value={ this.state.network === 'none' ? '' : this.state.network.hostname } onChange={this.hostnameDidChange} /> </div> <div className="flex-line"> <input className="theia-input small with-margin" type="number" pattern="[0-9]" value={ this.state.network === 'none' ? '' : this.state.network.port } onKeyDown={this.numbersOnlyKeyDown} onChange={this.portDidChange} /> </div> <div className="flex-line"> <input className="theia-input stretch with-margin" type="text" value={ this.state.network === 'none' ? '' : this.state.network.username } onChange={this.usernameDidChange} /> </div> <div className="flex-line"> <input className="theia-input stretch with-margin" type="password" value={ this.state.network === 'none' ? '' : this.state.network.password } onChange={this.passwordDidChange} /> </div> </div> </div> </div> </Disable> ); } private isControlKey(event: React.KeyboardEvent<HTMLInputElement>): boolean { return ( !!event.key && ['tab', 'delete', 'backspace', 'arrowleft', 'arrowright'].some( (key) => event.key.toLocaleLowerCase() === key ) ); } protected noopKeyDown = ( event: React.KeyboardEvent<HTMLInputElement> ): void => { if (this.isControlKey(event)) { return; } event.nativeEvent.preventDefault(); event.nativeEvent.returnValue = false; }; protected numbersOnlyKeyDown = ( event: React.KeyboardEvent<HTMLInputElement> ): void => { if (this.isControlKey(event)) { return; } const key = Number(event.key); if (isNaN(key) || event.key === null || event.key === ' ') { event.nativeEvent.preventDefault(); event.nativeEvent.returnValue = false; return; } }; protected browseSketchbookDidClick = async (): Promise<void> => { const uri = await this.props.fileDialogService.showOpenDialog({ title: nls.localize( 'arduino/preferences/newSketchbookLocation', 'Select new sketchbook location' ), openLabel: nls.localize('arduino/preferences/choose', 'Choose'), canSelectFiles: false, canSelectMany: false, canSelectFolders: true, }); if (uri) { const sketchbookPath = await this.props.fileService.fsPath(uri); this.setState({ sketchbookPath }); } }; protected editAdditionalUrlDidClick = async (): Promise<void> => { const additionalUrls = await new AdditionalUrlsDialog( AdditionalUrls.parse(this.state.rawAdditionalUrlsValue, ','), this.props.windowService ).open(); if (additionalUrls) { this.setState({ rawAdditionalUrlsValue: AdditionalUrls.stringify(additionalUrls), }); } }; private setFontSize = (editorFontSize: number) => { this.setState({ editorFontSize }); }; protected rawAdditionalUrlsValueDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { this.setState({ rawAdditionalUrlsValue: event.target.value, }); }; protected autoScaleInterfaceDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { this.setState({ autoScaleInterface: event.target.checked }); }; private setInterfaceScale = (percentage: number) => { const interfaceScale = (percentage - 100) / 20; this.setState({ interfaceScale }); }; protected verifyAfterUploadDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { this.setState({ verifyAfterUpload: event.target.checked }); }; protected sketchbookShowAllFilesDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { this.setState({ sketchbookShowAllFiles: event.target.checked }); }; protected autoSaveDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { this.setState({ autoSave: event.target.checked ? Settings.AutoSave.DEFAULT_ON : 'off', }); }; protected quickSuggestionsOtherDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { // need to persist react events through lifecycle https://reactjs.org/docs/events.html#event-pooling const newVal = event.target.checked ? true : false; this.setState((prevState) => { return { quickSuggestions: { ...prevState.quickSuggestions, other: newVal, }, }; }); }; protected themeDidChange = ( event: React.ChangeEvent<HTMLSelectElement> ): void => { const { selectedIndex } = event.target.options; const theme = ThemeService.get().getThemes()[selectedIndex]; if (theme) { this.setState({ themeId: theme.id }); if (ThemeService.get().getCurrentTheme().id !== theme.id) { ThemeService.get().setCurrentTheme(theme.id); } } }; protected languageDidChange = ( event: React.ChangeEvent<HTMLSelectElement> ): void => { const selectedLanguage = event.target.value; this.setState({ currentLanguage: selectedLanguage }); }; protected compilerWarningsDidChange = ( event: React.ChangeEvent<HTMLSelectElement> ): void => { const { selectedIndex } = event.target.options; const compilerWarnings = CompilerWarningLiterals[selectedIndex]; if (compilerWarnings) { this.setState({ compilerWarnings }); } }; protected verboseOnCompileDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { this.setState({ verboseOnCompile: event.target.checked }); }; protected verboseOnUploadDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { this.setState({ verboseOnUpload: event.target.checked }); }; protected sketchpathDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { const sketchbookPath = event.target.value; if (sketchbookPath) { this.setState({ sketchbookPath }); } }; protected noProxyDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { if (event.target.checked) { this.setState({ network: 'none' }); } else { this.setState({ network: Network.Default() }); } }; protected manualProxyDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { if (event.target.checked) { this.setState({ network: Network.Default() }); } else { this.setState({ network: 'none' }); } }; protected httpProtocolDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { if (this.state.network !== 'none') { const network = this.cloneProxySettings; network.protocol = event.target.checked ? 'http' : 'socks'; this.setState({ network }); } }; protected socksProtocolDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { if (this.state.network !== 'none') { const network = this.cloneProxySettings; network.protocol = event.target.checked ? 'socks' : 'http'; this.setState({ network }); } }; protected hostnameDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { if (this.state.network !== 'none') { const network = this.cloneProxySettings; network.hostname = event.target.value; this.setState({ network }); } }; protected portDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { if (this.state.network !== 'none') { const network = this.cloneProxySettings; network.port = event.target.value; this.setState({ network }); } }; protected usernameDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { if (this.state.network !== 'none') { const network = this.cloneProxySettings; network.username = event.target.value; this.setState({ network }); } }; protected passwordDidChange = ( event: React.ChangeEvent<HTMLInputElement> ): void => { if (this.state.network !== 'none') { const network = this.cloneProxySettings; network.password = event.target.value; this.setState({ network }); } }; private get cloneProxySettings(): ProxySettings { const { network } = this.state; if (network === 'none') { throw new Error('Must be called when proxy is enabled.'); } const copyNetwork = deepClone(network); return copyNetwork; } } export namespace SettingsComponent { export interface Props { readonly settingsService: SettingsService; readonly fileService: FileService; readonly fileDialogService: FileDialogService; readonly windowService: WindowService; readonly localizationProvider: AsyncLocalizationProvider; } export type State = Settings & { rawAdditionalUrlsValue: string; }; export namespace State { export function fromSettings(settings: Settings): State { return { ...settings, rawAdditionalUrlsValue: AdditionalUrls.stringify( settings.additionalUrls ), }; } export function toSettings(state: State): Settings { const parsedAdditionalUrls = AdditionalUrls.parse( state.rawAdditionalUrlsValue, ',' ); return { ...state, additionalUrls: AdditionalUrls.sameAs( state.additionalUrls, parsedAdditionalUrls ) ? state.additionalUrls : parsedAdditionalUrls, }; } export function merge(prevState: State, settings: Settings): State { const prevAdditionalUrls = AdditionalUrls.parse( prevState.rawAdditionalUrlsValue, ',' ); return { ...settings, rawAdditionalUrlsValue: prevState.rawAdditionalUrlsValue, additionalUrls: AdditionalUrls.sameAs( prevAdditionalUrls, settings.additionalUrls ) ? prevAdditionalUrls : settings.additionalUrls, }; } } }