@@ -5,19 +5,37 @@ import {
55 BoardsService ,
66 BoardsPackage ,
77 Board ,
8+ Port ,
89} from '../../common/protocol/boards-service' ;
910import { BoardsServiceProvider } from './boards-service-provider' ;
10- import { BoardsConfig } from './boards-config' ;
1111import { Installable , ResponseServiceArduino } from '../../common/protocol' ;
1212import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution' ;
1313import { 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 [ ] ;
1424
1525/**
1626 * Listens on `BoardsConfig.Config` changes, if a board is selected which does not
1727 * have the corresponding core installed, it proposes the user to install the core.
1828 */
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
1934@injectable ( )
2035export class BoardsAutoInstaller implements FrontendApplicationContribution {
36+ @inject ( NotificationCenter )
37+ private readonly notificationCenter : NotificationCenter ;
38+ 2139 @inject ( MessageService )
2240 protected readonly messageService : MessageService ;
2341
@@ -36,97 +54,228 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
3654 // Workaround for https://github.com/eclipse-theia/theia/issues/9349
3755 protected notifications : Board [ ] = [ ] ;
3856
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+ 3964 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 ( ) ;
4573
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
6674 if (
67- packagesForBoard . some ( ( { installedVersion } ) => ! ! installedVersion )
75+ boardWasUnplugged ||
76+ ! selectedBoard ||
77+ this . promptAlreadyShowingForBoard ( selectedBoard )
6878 ) {
6979 return ;
7080 }
7181
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 ( ) ;
12892 }
12993 } ) ;
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 ;
130170 }
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+ } ;
131280 }
132281}
0 commit comments