@@ -2,6 +2,9 @@ import * as React from 'react';
2
2
import { injectable , inject , postConstruct } from 'inversify' ;
3
3
import { Widget } from '@phosphor/widgets' ;
4
4
import { 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' ;
5
8
import URI from '@theia/core/lib/common/uri' ;
6
9
import { Emitter } from '@theia/core/lib/common/event' ;
7
10
import { Deferred } from '@theia/core/lib/common/promise-util' ;
@@ -15,7 +18,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
15
18
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state' ;
16
19
import { AbstractDialog , DialogProps , PreferenceService , PreferenceScope , DialogError , ReactWidget } from '@theia/core/lib/browser' ;
17
20
import { Index } from '../common/types' ;
18
- import { ConfigService , FileSystemExt } from '../common/protocol' ;
21
+ import { ConfigService , FileSystemExt , Network , ProxySettings } from '../common/protocol' ;
19
22
20
23
export interface Settings extends Index {
21
24
editorFontSize : number ; // `editor.fontSize`
@@ -32,6 +35,7 @@ export interface Settings extends Index {
32
35
33
36
sketchbookPath : string ; // CLI
34
37
additionalUrls : string [ ] ; // CLI
38
+ network : Network ; // CLI
35
39
}
36
40
export namespace Settings {
37
41
@@ -40,7 +44,6 @@ export namespace Settings {
40
44
}
41
45
42
46
}
43
- export type SettingsKey = keyof Settings ;
44
47
45
48
@injectable ( )
46
49
export class SettingsService {
@@ -101,7 +104,7 @@ export class SettingsService {
101
104
this . preferenceService . get < boolean > ( 'arduino.language.log' , true ) ,
102
105
this . configService . getConfiguration ( )
103
106
] ) ;
104
- const { additionalUrls, sketchDirUri } = cliConfig ;
107
+ const { additionalUrls, sketchDirUri, network } = cliConfig ;
105
108
const sketchbookPath = await this . fileService . fsPath ( new URI ( sketchDirUri ) ) ;
106
109
return {
107
110
editorFontSize,
@@ -115,7 +118,8 @@ export class SettingsService {
115
118
verifyAfterUpload,
116
119
enableLsLogs,
117
120
additionalUrls,
118
- sketchbookPath
121
+ sketchbookPath,
122
+ network
119
123
} ;
120
124
}
121
125
@@ -175,14 +179,16 @@ export class SettingsService {
175
179
verifyAfterUpload,
176
180
enableLsLogs,
177
181
sketchbookPath,
178
- additionalUrls
182
+ additionalUrls,
183
+ network
179
184
} = this . _settings ;
180
185
const [ config , sketchDirUri ] = await Promise . all ( [
181
186
this . configService . getConfiguration ( ) ,
182
187
this . fileSystemExt . getUri ( sketchbookPath )
183
188
] ) ;
184
189
( config as any ) . additionalUrls = additionalUrls ;
185
190
( config as any ) . sketchDirUri = sketchDirUri ;
191
+ ( config as any ) . network = network ;
186
192
187
193
await Promise . all ( [
188
194
this . preferenceService . set ( 'editor.fontSize' , editorFontSize , PreferenceScope . User ) ,
@@ -230,6 +236,21 @@ export class SettingsComponent extends React.Component<SettingsComponent.Props,
230
236
if ( ! this . state ) {
231
237
return < div /> ;
232
238
}
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 {
233
254
return < div className = 'content noselect' >
234
255
Sketchbook location:
235
256
< div className = 'flex-line' >
@@ -343,12 +364,106 @@ export class SettingsComponent extends React.Component<SettingsComponent.Props,
343
364
</ div > ;
344
365
}
345
366
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
+
346
455
protected noopKeyDown = ( event : React . KeyboardEvent < HTMLInputElement > ) => {
456
+ if ( this . isControlKey ( event ) ) {
457
+ return ;
458
+ }
347
459
event . nativeEvent . preventDefault ( ) ;
348
460
event . nativeEvent . returnValue = false ;
349
461
}
350
462
351
463
protected numbersOnlyKeyDown = ( event : React . KeyboardEvent < HTMLInputElement > ) => {
464
+ if ( this . isControlKey ( event ) ) {
465
+ return ;
466
+ }
352
467
const key = Number ( event . key )
353
468
if ( isNaN ( key ) || event . key === null || event . key === ' ' ) {
354
469
event . nativeEvent . preventDefault ( ) ;
@@ -444,6 +559,79 @@ export class SettingsComponent extends React.Component<SettingsComponent.Props,
444
559
}
445
560
} ;
446
561
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
+
447
635
}
448
636
export namespace SettingsComponent {
449
637
export interface Props {
0 commit comments