@@ -5,19 +5,37 @@ import {
5
5
BoardsService ,
6
6
BoardsPackage ,
7
7
Board ,
8
+ Port ,
8
9
} from '../../common/protocol/boards-service' ;
9
10
import { BoardsServiceProvider } from './boards-service-provider' ;
10
- import { BoardsConfig } from './boards-config' ;
11
11
import { Installable , ResponseServiceArduino } from '../../common/protocol' ;
12
12
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution' ;
13
13
import { nls } from '@theia/core/lib/common' ;
14
+ import { NotificationCenter } from '../notification-center' ;
15
+
16
+ interface AutoInstallPromptAction {
17
+ // isAcceptance, whether or not the action indicates acceptance of auto-install proposal
18
+ isAcceptance ?: boolean ;
19
+ key : string ;
20
+ handler : ( ...args : unknown [ ] ) => unknown ;
21
+ }
22
+
23
+ type AutoInstallPromptActions = AutoInstallPromptAction [ ] ;
14
24
15
25
/**
16
26
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
17
27
* have the corresponding core installed, it proposes the user to install the core.
18
28
*/
29
+
30
+ // * Cases in which we do not show the auto-install prompt:
31
+ // 1. When a related platform is already installed
32
+ // 2. When a prompt is already showing in the UI
33
+ // 3. When a board is unplugged
19
34
@injectable ( )
20
35
export class BoardsAutoInstaller implements FrontendApplicationContribution {
36
+ @inject ( NotificationCenter )
37
+ private readonly notificationCenter : NotificationCenter ;
38
+
21
39
@inject ( MessageService )
22
40
protected readonly messageService : MessageService ;
23
41
@@ -36,97 +54,228 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
36
54
// Workaround for https://github.com/eclipse-theia/theia/issues/9349
37
55
protected notifications : Board [ ] = [ ] ;
38
56
57
+ // * "refusal" meaning a "prompt action" not accepting the auto-install offer ("X" or "install manually")
58
+ // we can use "portSelectedOnLastRefusal" to deduce when a board is unplugged after a user has "refused"
59
+ // an auto-install prompt. Important to know as we do not want "an unplug" to trigger a "refused" prompt
60
+ // showing again
61
+ private portSelectedOnLastRefusal : Port | undefined ;
62
+ private lastRefusedPackageId : string | undefined ;
63
+
39
64
onStart ( ) : void {
40
- this . boardsServiceClient . onBoardsConfigChanged (
41
- this . ensureCoreExists . bind ( this )
42
- ) ;
43
- this . ensureCoreExists ( this . boardsServiceClient . boardsConfig ) ;
44
- }
65
+ const setEventListeners = ( ) => {
66
+ this . boardsServiceClient . onBoardsConfigChanged ( ( config ) => {
67
+ const { selectedBoard, selectedPort } = config ;
68
+
69
+ const boardWasUnplugged =
70
+ ! selectedPort && this . portSelectedOnLastRefusal ;
71
+
72
+ this . clearLastRefusedPromptInfo ( ) ;
45
73
46
- protected ensureCoreExists ( config : BoardsConfig . Config ) : void {
47
- const { selectedBoard, selectedPort } = config ;
48
- if (
49
- selectedBoard &&
50
- selectedPort &&
51
- ! this . notifications . find ( ( board ) => Board . sameAs ( board , selectedBoard ) )
52
- ) {
53
- this . notifications . push ( selectedBoard ) ;
54
- this . boardsService . search ( { } ) . then ( ( packages ) => {
55
- // filter packagesForBoard selecting matches from the cli (installed packages)
56
- // and matches based on the board name
57
- // NOTE: this ensures the Deprecated & new packages are all in the array
58
- // so that we can check if any of the valid packages is already installed
59
- const packagesForBoard = packages . filter (
60
- ( pkg ) =>
61
- BoardsPackage . contains ( selectedBoard , pkg ) ||
62
- pkg . boards . some ( ( board ) => board . name === selectedBoard . name )
63
- ) ;
64
-
65
- // check if one of the packages for the board is already installed. if so, no hint
66
74
if (
67
- packagesForBoard . some ( ( { installedVersion } ) => ! ! installedVersion )
75
+ boardWasUnplugged ||
76
+ ! selectedBoard ||
77
+ this . promptAlreadyShowingForBoard ( selectedBoard )
68
78
) {
69
79
return ;
70
80
}
71
81
72
- // filter the installable (not installed) packages,
73
- // CLI returns the packages already sorted with the deprecated ones at the end of the list
74
- // in order to ensure the new ones are preferred
75
- const candidates = packagesForBoard . filter (
76
- ( { installable, installedVersion } ) =>
77
- installable && ! installedVersion
78
- ) ;
79
-
80
- const candidate = candidates [ 0 ] ;
81
- if ( candidate ) {
82
- const version = candidate . availableVersions [ 0 ]
83
- ? `[v ${ candidate . availableVersions [ 0 ] } ]`
84
- : '' ;
85
- const yes = nls . localize ( 'vscode/extensionsUtils/yes' , 'Yes' ) ;
86
- const manualInstall = nls . localize (
87
- 'arduino/board/installManually' ,
88
- 'Install Manually'
89
- ) ;
90
- // tslint:disable-next-line:max-line-length
91
- this . messageService
92
- . info (
93
- nls . localize (
94
- 'arduino/board/installNow' ,
95
- 'The "{0} {1}" core has to be installed for the currently selected "{2}" board. Do you want to install it now?' ,
96
- candidate . name ,
97
- version ,
98
- selectedBoard . name
99
- ) ,
100
- manualInstall ,
101
- yes
102
- )
103
- . then ( async ( answer ) => {
104
- const index = this . notifications . findIndex ( ( board ) =>
105
- Board . sameAs ( board , selectedBoard )
106
- ) ;
107
- if ( index !== - 1 ) {
108
- this . notifications . splice ( index , 1 ) ;
109
- }
110
- if ( answer === yes ) {
111
- await Installable . installWithProgress ( {
112
- installable : this . boardsService ,
113
- item : candidate ,
114
- messageService : this . messageService ,
115
- responseService : this . responseService ,
116
- version : candidate . availableVersions [ 0 ] ,
117
- } ) ;
118
- return ;
119
- }
120
- if ( answer === manualInstall ) {
121
- this . boardsManagerFrontendContribution
122
- . openView ( { reveal : true } )
123
- . then ( ( widget ) =>
124
- widget . refresh ( candidate . name . toLocaleLowerCase ( ) )
125
- ) ;
126
- }
127
- } ) ;
82
+ this . ensureCoreExists ( selectedBoard , selectedPort ) ;
83
+ } ) ;
84
+
85
+ // we "clearRefusedPackageInfo" if a "refused" package is eventually
86
+ // installed, though this is not strictly necessary. It's more of a
87
+ // cleanup, to ensure the related variables are representative of
88
+ // current state.
89
+ this . notificationCenter . onPlatformInstalled ( ( installed ) => {
90
+ if ( this . lastRefusedPackageId === installed . item . id ) {
91
+ this . clearLastRefusedPromptInfo ( ) ;
128
92
}
129
93
} ) ;
94
+ } ;
95
+
96
+ // we should invoke this.ensureCoreExists only once we're sure
97
+ // everything has been reconciled
98
+ this . boardsServiceClient . reconciled . then ( ( ) => {
99
+ const { selectedBoard, selectedPort } =
100
+ this . boardsServiceClient . boardsConfig ;
101
+
102
+ if ( selectedBoard ) {
103
+ this . ensureCoreExists ( selectedBoard , selectedPort ) ;
104
+ }
105
+
106
+ setEventListeners ( ) ;
107
+ } ) ;
108
+ }
109
+
110
+ private removeNotificationByBoard ( selectedBoard : Board ) : void {
111
+ const index = this . notifications . findIndex ( ( notification ) =>
112
+ Board . sameAs ( notification , selectedBoard )
113
+ ) ;
114
+ if ( index !== - 1 ) {
115
+ this . notifications . splice ( index , 1 ) ;
116
+ }
117
+ }
118
+
119
+ private clearLastRefusedPromptInfo ( ) : void {
120
+ this . lastRefusedPackageId = undefined ;
121
+ this . portSelectedOnLastRefusal = undefined ;
122
+ }
123
+
124
+ private setLastRefusedPromptInfo (
125
+ packageId : string ,
126
+ selectedPort ?: Port
127
+ ) : void {
128
+ this . lastRefusedPackageId = packageId ;
129
+ this . portSelectedOnLastRefusal = selectedPort ;
130
+ }
131
+
132
+ private promptAlreadyShowingForBoard ( board : Board ) : boolean {
133
+ return Boolean (
134
+ this . notifications . find ( ( notification ) =>
135
+ Board . sameAs ( notification , board )
136
+ )
137
+ ) ;
138
+ }
139
+
140
+ protected ensureCoreExists ( selectedBoard : Board , selectedPort ?: Port ) : void {
141
+ this . notifications . push ( selectedBoard ) ;
142
+ this . boardsService . search ( { } ) . then ( ( packages ) => {
143
+ const candidate = this . getInstallCandidate ( packages , selectedBoard ) ;
144
+
145
+ if ( candidate ) {
146
+ this . showAutoInstallPrompt ( candidate , selectedBoard , selectedPort ) ;
147
+ } else {
148
+ this . removeNotificationByBoard ( selectedBoard ) ;
149
+ }
150
+ } ) ;
151
+ }
152
+
153
+ private getInstallCandidate (
154
+ packages : BoardsPackage [ ] ,
155
+ selectedBoard : Board
156
+ ) : BoardsPackage | undefined {
157
+ // filter packagesForBoard selecting matches from the cli (installed packages)
158
+ // and matches based on the board name
159
+ // NOTE: this ensures the Deprecated & new packages are all in the array
160
+ // so that we can check if any of the valid packages is already installed
161
+ const packagesForBoard = packages . filter (
162
+ ( pkg ) =>
163
+ BoardsPackage . contains ( selectedBoard , pkg ) ||
164
+ pkg . boards . some ( ( board ) => board . name === selectedBoard . name )
165
+ ) ;
166
+
167
+ // check if one of the packages for the board is already installed. if so, no hint
168
+ if ( packagesForBoard . some ( ( { installedVersion } ) => ! ! installedVersion ) ) {
169
+ return ;
130
170
}
171
+
172
+ // filter the installable (not installed) packages,
173
+ // CLI returns the packages already sorted with the deprecated ones at the end of the list
174
+ // in order to ensure the new ones are preferred
175
+ const candidates = packagesForBoard . filter (
176
+ ( { installable, installedVersion } ) => installable && ! installedVersion
177
+ ) ;
178
+
179
+ return candidates [ 0 ] ;
180
+ }
181
+
182
+ private showAutoInstallPrompt (
183
+ candidate : BoardsPackage ,
184
+ selectedBoard : Board ,
185
+ selectedPort ?: Port
186
+ ) : void {
187
+ const candidateName = candidate . name ;
188
+ const version = candidate . availableVersions [ 0 ]
189
+ ? `[v ${ candidate . availableVersions [ 0 ] } ]`
190
+ : '' ;
191
+
192
+ const info = this . generatePromptInfoText (
193
+ candidateName ,
194
+ version ,
195
+ selectedBoard . name
196
+ ) ;
197
+
198
+ const actions = this . createPromptActions ( candidate ) ;
199
+
200
+ const onRefuse = ( ) => {
201
+ this . setLastRefusedPromptInfo ( candidate . id , selectedPort ) ;
202
+ } ;
203
+ const handleAction = this . createOnAnswerHandler ( actions , onRefuse ) ;
204
+
205
+ const onAnswer = ( answer : string ) => {
206
+ this . removeNotificationByBoard ( selectedBoard ) ;
207
+
208
+ handleAction ( answer ) ;
209
+ } ;
210
+
211
+ this . messageService
212
+ . info ( info , ...actions . map ( ( action ) => action . key ) )
213
+ . then ( onAnswer ) ;
214
+ }
215
+
216
+ private generatePromptInfoText (
217
+ candidateName : string ,
218
+ version : string ,
219
+ boardName : string
220
+ ) : string {
221
+ return nls . localize (
222
+ 'arduino/board/installNow' ,
223
+ 'The "{0} {1}" core has to be installed for the currently selected "{2}" board. Do you want to install it now?' ,
224
+ candidateName ,
225
+ version ,
226
+ boardName
227
+ ) ;
228
+ }
229
+
230
+ private createPromptActions (
231
+ candidate : BoardsPackage
232
+ ) : AutoInstallPromptActions {
233
+ const yes = nls . localize ( 'vscode/extensionsUtils/yes' , 'Yes' ) ;
234
+ const manualInstall = nls . localize (
235
+ 'arduino/board/installManually' ,
236
+ 'Install Manually'
237
+ ) ;
238
+
239
+ const actions : AutoInstallPromptActions = [
240
+ {
241
+ isAcceptance : true ,
242
+ key : yes ,
243
+ handler : ( ) => {
244
+ return Installable . installWithProgress ( {
245
+ installable : this . boardsService ,
246
+ item : candidate ,
247
+ messageService : this . messageService ,
248
+ responseService : this . responseService ,
249
+ version : candidate . availableVersions [ 0 ] ,
250
+ } ) ;
251
+ } ,
252
+ } ,
253
+ {
254
+ key : manualInstall ,
255
+ handler : ( ) => {
256
+ this . boardsManagerFrontendContribution
257
+ . openView ( { reveal : true } )
258
+ . then ( ( widget ) =>
259
+ widget . refresh ( candidate . name . toLocaleLowerCase ( ) )
260
+ ) ;
261
+ } ,
262
+ } ,
263
+ ] ;
264
+
265
+ return actions ;
266
+ }
267
+
268
+ private createOnAnswerHandler (
269
+ actions : AutoInstallPromptActions ,
270
+ onRefuse ?: ( ) => void
271
+ ) : ( answer : string ) => void {
272
+ return ( answer ) => {
273
+ const actionToHandle = actions . find ( ( action ) => action . key === answer ) ;
274
+ actionToHandle ?. handler ( ) ;
275
+
276
+ if ( ! actionToHandle ?. isAcceptance && onRefuse ) {
277
+ onRefuse ( ) ;
278
+ }
279
+ } ;
131
280
}
132
281
}
0 commit comments