@@ -2,6 +2,9 @@ import * as React from 'react';
22import { injectable , inject , postConstruct } from 'inversify' ;
33import { Widget } from '@phosphor/widgets' ;
44import { Message } from '@phosphor/messaging' ;
5+ import { Tab , Tabs , TabList , TabPanel } from 'react-tabs' ;
6+ import 'react-tabs/style/react-tabs.css' ;
7+ import { Disable } from 'react-disable' ;
58import URI from '@theia/core/lib/common/uri' ;
69import { Emitter } from '@theia/core/lib/common/event' ;
710import { Deferred } from '@theia/core/lib/common/promise-util' ;
@@ -15,7 +18,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
1518import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state' ;
1619import { AbstractDialog , DialogProps , PreferenceService , PreferenceScope , DialogError , ReactWidget } from '@theia/core/lib/browser' ;
1720import { Index } from '../common/types' ;
18- import { ConfigService , FileSystemExt } from '../common/protocol' ;
21+ import { ConfigService , FileSystemExt , Network , ProxySettings } from '../common/protocol' ;
1922
2023export interface Settings extends Index {
2124 editorFontSize : number ; // `editor.fontSize`
@@ -32,6 +35,7 @@ export interface Settings extends Index {
3235
3336 sketchbookPath : string ; // CLI
3437 additionalUrls : string [ ] ; // CLI
38+ network : Network ; // CLI
3539}
3640export namespace Settings {
3741
@@ -40,7 +44,6 @@ export namespace Settings {
4044 }
4145
4246}
43- export type SettingsKey = keyof Settings ;
4447
4548@injectable ( )
4649export class SettingsService {
@@ -101,7 +104,7 @@ export class SettingsService {
101104 this . preferenceService . get < boolean > ( 'arduino.language.log' , true ) ,
102105 this . configService . getConfiguration ( )
103106 ] ) ;
104- const { additionalUrls, sketchDirUri } = cliConfig ;
107+ const { additionalUrls, sketchDirUri, network } = cliConfig ;
105108 const sketchbookPath = await this . fileService . fsPath ( new URI ( sketchDirUri ) ) ;
106109 return {
107110 editorFontSize,
@@ -115,7 +118,8 @@ export class SettingsService {
115118 verifyAfterUpload,
116119 enableLsLogs,
117120 additionalUrls,
118- sketchbookPath
121+ sketchbookPath,
122+ network
119123 } ;
120124 }
121125
@@ -175,14 +179,16 @@ export class SettingsService {
175179 verifyAfterUpload,
176180 enableLsLogs,
177181 sketchbookPath,
178- additionalUrls
182+ additionalUrls,
183+ network
179184 } = this . _settings ;
180185 const [ config , sketchDirUri ] = await Promise . all ( [
181186 this . configService . getConfiguration ( ) ,
182187 this . fileSystemExt . getUri ( sketchbookPath )
183188 ] ) ;
184189 ( config as any ) . additionalUrls = additionalUrls ;
185190 ( config as any ) . sketchDirUri = sketchDirUri ;
191+ ( config as any ) . network = network ;
186192
187193 await Promise . all ( [
188194 this . preferenceService . set ( 'editor.fontSize' , editorFontSize , PreferenceScope . User ) ,
@@ -230,6 +236,21 @@ export class SettingsComponent extends React.Component<SettingsComponent.Props,
230236 if ( ! this . state ) {
231237 return < div /> ;
232238 }
239+ return < Tabs >
240+ < TabList >
241+ < Tab > Settings</ Tab >
242+ < Tab > Network</ Tab >
243+ </ TabList >
244+ < TabPanel >
245+ { this . renderSettings ( ) }
246+ </ TabPanel >
247+ < TabPanel >
248+ { this . renderNetwork ( ) }
249+ </ TabPanel >
250+ </ Tabs > ;
251+ }
252+
253+ protected renderSettings ( ) : React . ReactNode {
233254 return < div className = 'content noselect' >
234255 Sketchbook location:
235256 < div className = 'flex-line' >
@@ -343,12 +364,106 @@ export class SettingsComponent extends React.Component<SettingsComponent.Props,
343364 </ div > ;
344365 }
345366
367+ protected renderNetwork ( ) : React . ReactNode {
368+ return < div className = 'content noselect' >
369+ < form >
370+ < label className = 'flex-line' >
371+ < input
372+ type = 'radio'
373+ checked = { this . state . network === 'none' }
374+ onChange = { this . noProxyDidChange } />
375+ No proxy
376+ </ label >
377+ < label className = 'flex-line' >
378+ < input
379+ type = 'radio'
380+ checked = { this . state . network !== 'none' }
381+ onChange = { this . manualProxyDidChange } />
382+ Manual proxy configuration
383+ </ label >
384+ </ form >
385+ { this . renderProxySettings ( ) }
386+ </ div > ;
387+ }
388+
389+ protected renderProxySettings ( ) : React . ReactNode {
390+ const disabled = this . state . network === 'none' ;
391+ return < Disable disabled = { disabled } >
392+ < div className = 'proxy-settings' aria-disabled = { disabled } >
393+ < form className = 'flex-line' >
394+ < input
395+ type = 'radio'
396+ checked = { this . state . network === 'none' ? true : this . state . network . protocol === 'http' }
397+ onChange = { this . httpProtocolDidChange } />
398+ HTTP
399+ < label className = 'flex-line' >
400+ < input
401+ type = 'radio'
402+ checked = { this . state . network === 'none' ? false : this . state . network . protocol !== 'http' }
403+ onChange = { this . socksProtocolDidChange } />
404+ SOCKS
405+ </ label >
406+ </ form >
407+ < div className = 'flex-line proxy-settings' >
408+ < div className = 'column' >
409+ < div className = 'flex-line' > Host name:</ div >
410+ < div className = 'flex-line' > Port number:</ div >
411+ < div className = 'flex-line' > Username:</ div >
412+ < div className = 'flex-line' > Password:</ div >
413+ </ div >
414+ < div className = 'column stretch' >
415+ < div className = 'flex-line' >
416+ < input
417+ className = 'theia-input stretch with-margin'
418+ type = 'text'
419+ value = { this . state . network === 'none' ? '' : this . state . network . hostname }
420+ onChange = { this . hostnameDidChange } />
421+ </ div >
422+ < div className = 'flex-line' >
423+ < input
424+ className = 'theia-input small with-margin'
425+ type = 'number'
426+ pattern = '[0-9]'
427+ value = { this . state . network === 'none' ? '' : this . state . network . port }
428+ onKeyDown = { this . numbersOnlyKeyDown }
429+ onChange = { this . portDidChange } />
430+ </ div >
431+ < div className = 'flex-line' >
432+ < input
433+ className = 'theia-input stretch with-margin'
434+ type = 'text'
435+ value = { this . state . network === 'none' ? '' : this . state . network . username }
436+ onChange = { this . usernameDidChange } />
437+ </ div >
438+ < div className = 'flex-line' >
439+ < input
440+ className = 'theia-input stretch with-margin'
441+ type = 'password'
442+ value = { this . state . network === 'none' ? '' : this . state . network . password }
443+ onChange = { this . passwordDidChange } />
444+ </ div >
445+ </ div >
446+ </ div >
447+ </ div >
448+ </ Disable > ;
449+ }
450+
451+ private isControlKey ( event : React . KeyboardEvent < HTMLInputElement > ) : boolean {
452+ return ! ! event . key && [ 'tab' , 'delete' , 'backspace' , 'arrowleft' , 'arrowright' ] . some ( key => event . key . toLocaleLowerCase ( ) === key ) ;
453+ }
454+
346455 protected noopKeyDown = ( event : React . KeyboardEvent < HTMLInputElement > ) => {
456+ if ( this . isControlKey ( event ) ) {
457+ return ;
458+ }
347459 event . nativeEvent . preventDefault ( ) ;
348460 event . nativeEvent . returnValue = false ;
349461 }
350462
351463 protected numbersOnlyKeyDown = ( event : React . KeyboardEvent < HTMLInputElement > ) => {
464+ if ( this . isControlKey ( event ) ) {
465+ return ;
466+ }
352467 const key = Number ( event . key )
353468 if ( isNaN ( key ) || event . key === null || event . key === ' ' ) {
354469 event . nativeEvent . preventDefault ( ) ;
@@ -444,6 +559,79 @@ export class SettingsComponent extends React.Component<SettingsComponent.Props,
444559 }
445560 } ;
446561
562+ protected noProxyDidChange = ( event : React . ChangeEvent < HTMLInputElement > ) => {
563+ if ( event . target . checked ) {
564+ this . setState ( { network : 'none' } ) ;
565+ } else {
566+ this . setState ( { network : Network . Default ( ) } ) ;
567+ }
568+ } ;
569+
570+ protected manualProxyDidChange = ( event : React . ChangeEvent < HTMLInputElement > ) => {
571+ if ( event . target . checked ) {
572+ this . setState ( { network : Network . Default ( ) } ) ;
573+ } else {
574+ this . setState ( { network : 'none' } ) ;
575+ }
576+ } ;
577+
578+ protected httpProtocolDidChange = ( event : React . ChangeEvent < HTMLInputElement > ) => {
579+ if ( this . state . network !== 'none' ) {
580+ const network = this . cloneProxySettings ;
581+ network . protocol = event . target . checked ? 'http' : 'socks' ;
582+ this . setState ( { network } ) ;
583+ }
584+ } ;
585+
586+ protected socksProtocolDidChange = ( event : React . ChangeEvent < HTMLInputElement > ) => {
587+ if ( this . state . network !== 'none' ) {
588+ const network = this . cloneProxySettings ;
589+ network . protocol = event . target . checked ? 'socks' : 'http' ;
590+ this . setState ( { network } ) ;
591+ }
592+ } ;
593+
594+ protected hostnameDidChange = ( event : React . ChangeEvent < HTMLInputElement > ) => {
595+ if ( this . state . network !== 'none' ) {
596+ const network = this . cloneProxySettings ;
597+ network . hostname = event . target . value ;
598+ this . setState ( { network } ) ;
599+ }
600+ } ;
601+
602+ protected portDidChange = ( event : React . ChangeEvent < HTMLInputElement > ) => {
603+ if ( this . state . network !== 'none' ) {
604+ const network = this . cloneProxySettings ;
605+ network . port = event . target . value ;
606+ this . setState ( { network } ) ;
607+ }
608+ } ;
609+
610+ protected usernameDidChange = ( event : React . ChangeEvent < HTMLInputElement > ) => {
611+ if ( this . state . network !== 'none' ) {
612+ const network = this . cloneProxySettings ;
613+ network . username = event . target . value ;
614+ this . setState ( { network } ) ;
615+ }
616+ } ;
617+
618+ protected passwordDidChange = ( event : React . ChangeEvent < HTMLInputElement > ) => {
619+ if ( this . state . network !== 'none' ) {
620+ const network = this . cloneProxySettings ;
621+ network . password = event . target . value ;
622+ this . setState ( { network } ) ;
623+ }
624+ } ;
625+
626+ private get cloneProxySettings ( ) : ProxySettings {
627+ const { network } = this . state ;
628+ if ( network === 'none' ) {
629+ throw new Error ( 'Must be called when proxy is enabled.' ) ;
630+ }
631+ const copyNetwork = deepClone ( network ) ;
632+ return copyNetwork ;
633+ }
634+
447635}
448636export namespace SettingsComponent {
449637 export interface Props {
0 commit comments