PageRenderTime 26ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/wizard/src/main/java/com/metsci/glimpse/wizard/Wizard.java

http://github.com/metsci/glimpse
Java | 575 lines | 396 code | 108 blank | 71 comment | 22 complexity | 92706c932226c9e7b0f6d3f23632ff5a MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, CC-BY-SA-3.0
  1. /*
  2. * Copyright (c) 2019, Metron, Inc.
  3. * All rights reserved.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. * * Redistributions of source code must retain the above copyright
  8. * notice, this list of conditions and the following disclaimer.
  9. * * Redistributions in binary form must reproduce the above copyright
  10. * notice, this list of conditions and the following disclaimer in the
  11. * documentation and/or other materials provided with the distribution.
  12. * * Neither the name of Metron, Inc. nor the
  13. * names of its contributors may be used to endorse or promote products
  14. * derived from this software without specific prior written permission.
  15. *
  16. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  17. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. * DISCLAIMED. IN NO EVENT SHALL METRON, INC. BE LIABLE FOR ANY
  20. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. */
  27. package com.metsci.glimpse.wizard;
  28. import java.util.ArrayList;
  29. import java.util.Collection;
  30. import java.util.Collections;
  31. import java.util.HashSet;
  32. import java.util.LinkedList;
  33. import java.util.List;
  34. import java.util.Set;
  35. import java.util.concurrent.CopyOnWriteArrayList;
  36. import java.util.stream.Collectors;
  37. import java.util.stream.Stream;
  38. import javax.swing.SwingUtilities;
  39. import com.google.common.base.Objects;
  40. import com.google.common.collect.HashMultimap;
  41. import com.google.common.collect.Multimap;
  42. import com.metsci.glimpse.wizard.listener.DataUpdatedListener;
  43. import com.metsci.glimpse.wizard.listener.ErrorsUpdatedListener;
  44. import com.metsci.glimpse.wizard.listener.PageEnteredListener;
  45. import com.metsci.glimpse.wizard.listener.PageExitedListener;
  46. import com.metsci.glimpse.wizard.listener.PageModelListener;
  47. import com.metsci.glimpse.wizard.listener.WizardCancelledListener;
  48. import com.metsci.glimpse.wizard.listener.WizardFinishedListener;
  49. import com.metsci.glimpse.wizard.tree.WizardPageModelTree;
  50. import com.metsci.glimpse.wizard.tree.WizardUITree;
  51. public class Wizard<D>
  52. {
  53. // the data object containing the state of the Wizard
  54. // this is the value being modified by the Wizard
  55. protected D data;
  56. // set containing the page ids of pages which have been viewed at least once by the user
  57. protected final Set<Object> isVisited = new HashSet<>( );
  58. // a history of the pages which have been visited by the user
  59. protected LinkedList<Object> pageHistory;
  60. // errors returned by the wizard pages
  61. protected Multimap<Object, WizardError> pageErrors;
  62. // errors added programmatically by the user
  63. protected Multimap<Object, WizardError> userErrors;
  64. protected WizardUI<D> ui;
  65. protected WizardPageModel<D> model;
  66. protected List<ErrorsUpdatedListener> errorListeners;
  67. protected List<PageEnteredListener<D>> pageEnterListeners;
  68. protected List<PageExitedListener<D>> pageExitListeners;
  69. protected List<DataUpdatedListener<D>> dataUpdatedListeners;
  70. protected List<WizardCancelledListener> cancelledListeners;
  71. protected List<WizardFinishedListener> finishedListeners;
  72. public Wizard( D data, WizardPageModel<D> model, WizardUI<D> ui )
  73. {
  74. this.data = data;
  75. this.pageErrors = HashMultimap.create( );
  76. this.userErrors = HashMultimap.create( );
  77. this.errorListeners = new CopyOnWriteArrayList<>( );
  78. this.pageEnterListeners = new CopyOnWriteArrayList<>( );
  79. this.pageExitListeners = new CopyOnWriteArrayList<>( );
  80. this.dataUpdatedListeners = new CopyOnWriteArrayList<>( );
  81. this.cancelledListeners = new CopyOnWriteArrayList<>( );
  82. this.finishedListeners = new CopyOnWriteArrayList<>( );
  83. this.pageHistory = new LinkedList<>( );
  84. this.model = model;
  85. this.ui = ui;
  86. this.ui.setWizard( this );
  87. this.model.setWizard( this );
  88. this.model.addListener( new PageModelListener<D>( )
  89. {
  90. @Override
  91. public void onPagesAdded( Collection<WizardPage<D>> addedPages )
  92. {
  93. }
  94. @Override
  95. public void onPagesRemoved( Collection<WizardPage<D>> removedPages )
  96. {
  97. // clear errors for deleted pages
  98. for ( WizardPage<D> page : removedPages )
  99. {
  100. pageErrors.get( page.getId( ) ).clear( );
  101. userErrors.get( page.getId( ) ).clear( );
  102. }
  103. fireErrorsUpdated( );
  104. }
  105. } );
  106. }
  107. public Wizard( boolean displayErrorButton )
  108. {
  109. this( null, new WizardPageModelTree<>( ), new WizardUITree<>( displayErrorButton ) );
  110. }
  111. public Wizard( )
  112. {
  113. this( true );
  114. }
  115. /**
  116. * Reinitialized the Wizard to its default state. This is mainly used to
  117. * allow reuse of the Wizard dialog since it takes a few seconds to construct initially.
  118. *
  119. * @param settings the new initial settings to edit
  120. */
  121. public void reset( D data )
  122. {
  123. assert ( SwingUtilities.isEventDispatchThread( ) );
  124. this.isVisited.clear( );
  125. this.pageHistory.clear( );
  126. this.clearErrors( );
  127. this.setData( data );
  128. this.visitNextPage( );
  129. }
  130. public void finish( )
  131. {
  132. assert ( SwingUtilities.isEventDispatchThread( ) );
  133. this.doLeavePage( this.getCurrentPage( ) );
  134. this.fireFinished( );
  135. }
  136. public void cancel( )
  137. {
  138. assert ( SwingUtilities.isEventDispatchThread( ) );
  139. this.fireCancelled( );
  140. }
  141. public D getData( )
  142. {
  143. return this.data;
  144. }
  145. public void setData( D data )
  146. {
  147. assert ( SwingUtilities.isEventDispatchThread( ) );
  148. this.data = data;
  149. // apply the new settings to each page
  150. this.getPageModel( ).getPages( ).forEach( ( page ) ->
  151. {
  152. page.setData( data, true );
  153. } );
  154. this.fireDataUpdated( data );
  155. }
  156. public void visitAll( )
  157. {
  158. assert ( SwingUtilities.isEventDispatchThread( ) );
  159. this.getPageModel( ).getPages( ).stream( ).forEach( p ->
  160. {
  161. this.isVisited.add( p.getId( ) );
  162. //set the fields on the page using the data object
  163. p.setData( this.data, false );
  164. //pull any default values from the page into the data object
  165. this.data = p.updateData( this.data );
  166. Collection<WizardError> pageErrors = p.getErrors( );
  167. this.pageErrors.replaceValues( p.getId( ), pageErrors );
  168. } );
  169. this.fireErrorsUpdated( );
  170. }
  171. public WizardPage<D> visitPreviousPage( )
  172. {
  173. assert ( SwingUtilities.isEventDispatchThread( ) );
  174. this.doLeavePage( this.getCurrentPage( ) );
  175. this.pageHistory.removeLast( );
  176. WizardPage<D> nextPage = this.model.getPageById( this.pageHistory.getLast( ) );
  177. this.doEnterPage( nextPage );
  178. return nextPage;
  179. }
  180. public WizardPage<D> visitNextPage( )
  181. {
  182. assert ( SwingUtilities.isEventDispatchThread( ) );
  183. this.doLeavePage( this.getCurrentPage( ) );
  184. WizardPage<D> nextPage = this.model.getNextPage( this.getPageHistory( ), this.data );
  185. this.pageHistory.add( nextPage.getId( ) );
  186. this.doEnterPage( nextPage );
  187. return nextPage;
  188. }
  189. /**
  190. * Make the provide page the current Wizard page.
  191. */
  192. public void visitPage( WizardPage<D> page )
  193. {
  194. assert ( SwingUtilities.isEventDispatchThread( ) );
  195. // do nothing if we are re-visiting the current page
  196. if ( this.getCurrentPage( ) != null && Objects.equal( this.getCurrentPage( ).getId( ), page.getId( ) ) ) return;
  197. this.doLeavePage( this.getCurrentPage( ) );
  198. // Only add new pages to history, not when 'Previous' button is hit
  199. if ( this.pageHistory.isEmpty( ) || !page.getId( ).equals( this.pageHistory.getLast( ) ) )
  200. {
  201. this.pageHistory.add( page.getId( ) );
  202. }
  203. this.doEnterPage( page );
  204. }
  205. /**
  206. * Update the wizard data based on the state of the current page.
  207. */
  208. public void updateData( WizardPage<D> page )
  209. {
  210. assert ( SwingUtilities.isEventDispatchThread( ) );
  211. this.data = page.updateData( this.data );
  212. this.fireDataUpdated( data );
  213. }
  214. /**
  215. * Recalculates errors for the provided page.
  216. */
  217. public void setErrors( WizardPage<D> page )
  218. {
  219. assert ( SwingUtilities.isEventDispatchThread( ) );
  220. if ( this.isVisited( page.getId( ) ) )
  221. {
  222. Collection<WizardError> pageErrors = page.getErrors( );
  223. this.pageErrors.replaceValues( page.getId( ), pageErrors );
  224. this.fireErrorsUpdated( );
  225. }
  226. }
  227. /**
  228. * @return the currently displayed page
  229. */
  230. public WizardPage<D> getCurrentPage( )
  231. {
  232. assert ( SwingUtilities.isEventDispatchThread( ) );
  233. if ( this.pageHistory.isEmpty( ) )
  234. {
  235. return null;
  236. }
  237. else
  238. {
  239. return this.model.getPageById( this.pageHistory.getLast( ) );
  240. }
  241. }
  242. /**
  243. * Add a listener which is notified when the wizard errors change.
  244. */
  245. public void addErrorsUpdatedListener( ErrorsUpdatedListener listener )
  246. {
  247. this.errorListeners.add( listener );
  248. }
  249. public void removeErrorsUpdatedListener( ErrorsUpdatedListener listener )
  250. {
  251. this.errorListeners.remove( listener );
  252. }
  253. public void addPageEnteredListener( PageEnteredListener<D> listener )
  254. {
  255. this.pageEnterListeners.add( listener );
  256. }
  257. public void removePageEnteredListener( PageEnteredListener<D> listener )
  258. {
  259. this.pageEnterListeners.remove( listener );
  260. }
  261. public void addPageExitedListener( PageExitedListener<D> listener )
  262. {
  263. this.pageExitListeners.add( listener );
  264. }
  265. public void removePageExitedListener( PageExitedListener<D> listener )
  266. {
  267. this.pageExitListeners.remove( listener );
  268. }
  269. public void addDataUpdatedListener( DataUpdatedListener<D> listener )
  270. {
  271. this.dataUpdatedListeners.add( listener );
  272. }
  273. public void removeDataUpdatedListener( DataUpdatedListener<D> listener )
  274. {
  275. this.dataUpdatedListeners.remove( listener );
  276. }
  277. public void addCancelledListener( WizardCancelledListener listener )
  278. {
  279. this.cancelledListeners.add( listener );
  280. }
  281. public void removeCancelledListener( WizardCancelledListener listener )
  282. {
  283. this.cancelledListeners.remove( listener );
  284. }
  285. public void addFinishedListener( WizardFinishedListener listener )
  286. {
  287. this.finishedListeners.add( listener );
  288. }
  289. public void removeFinishedListener( WizardFinishedListener listener )
  290. {
  291. this.finishedListeners.remove( listener );
  292. }
  293. public WizardPageModel<D> getPageModel( )
  294. {
  295. assert ( SwingUtilities.isEventDispatchThread( ) );
  296. return this.model;
  297. }
  298. public WizardUI<D> getUI( )
  299. {
  300. assert ( SwingUtilities.isEventDispatchThread( ) );
  301. return this.ui;
  302. }
  303. // only return errors pertaining to visited pages
  304. public Collection<WizardError> getErrors( )
  305. {
  306. assert ( SwingUtilities.isEventDispatchThread( ) );
  307. Collection<WizardError> errors = Stream.concat( this.pageErrors.values( ).stream( ), this.userErrors.values( ).stream( ) )
  308. .filter( error -> error.getPageId( ) == null || this.isVisited.contains( error.getPageId( ) ) )
  309. .collect( Collectors.toList( ) );
  310. if ( errors.isEmpty( ) )
  311. {
  312. errors.add( new WizardError( WizardErrorType.Good, "No Errors Detected." ) );
  313. }
  314. return errors;
  315. }
  316. public Collection<WizardError> getErrors( WizardPage<?> page )
  317. {
  318. assert ( SwingUtilities.isEventDispatchThread( ) );
  319. return this.getErrors( page.getId( ) );
  320. }
  321. public Collection<WizardError> getErrors( Object id )
  322. {
  323. assert ( SwingUtilities.isEventDispatchThread( ) );
  324. if ( this.isVisited.contains( id ) )
  325. {
  326. List<WizardError> errors = new ArrayList<WizardError>( );
  327. errors.addAll( this.pageErrors.get( id ) );
  328. errors.addAll( this.userErrors.get( id ) );
  329. return Collections.unmodifiableList( errors );
  330. }
  331. else
  332. {
  333. return Collections.emptyList( );
  334. }
  335. }
  336. public void setErrors( Collection<WizardError> errors )
  337. {
  338. assert ( SwingUtilities.isEventDispatchThread( ) );
  339. this.userErrors.clear( );
  340. for ( WizardError error : errors )
  341. {
  342. this.addError0( error );
  343. }
  344. this.fireErrorsUpdated( );
  345. }
  346. public void clearErrors( )
  347. {
  348. assert ( SwingUtilities.isEventDispatchThread( ) );
  349. this.pageErrors.clear( );
  350. this.userErrors.clear( );
  351. this.fireErrorsUpdated( );
  352. }
  353. public void addErrors( Collection<WizardError> errors )
  354. {
  355. assert ( SwingUtilities.isEventDispatchThread( ) );
  356. for ( WizardError error : errors )
  357. {
  358. this.addError0( error );
  359. }
  360. this.fireErrorsUpdated( );
  361. }
  362. public void addError( WizardError error )
  363. {
  364. assert ( SwingUtilities.isEventDispatchThread( ) );
  365. this.addError0( error );
  366. this.fireErrorsUpdated( );
  367. }
  368. public LinkedList<WizardPage<D>> getPageHistory( )
  369. {
  370. assert ( SwingUtilities.isEventDispatchThread( ) );
  371. return new LinkedList<>( this.pageHistory
  372. .stream( )
  373. .map( this.model::getPageById )
  374. .collect( Collectors.toList( ) ) );
  375. }
  376. public void dispose( )
  377. {
  378. assert ( SwingUtilities.isEventDispatchThread( ) );
  379. this.model.dispose( );
  380. this.ui.dispose( );
  381. }
  382. /**
  383. * Update Wizard data to reflect edits made to the provided page and recalculate errors associated with the page.
  384. *
  385. * @param page the page to update
  386. */
  387. protected void updatePage( WizardPage<D> page )
  388. {
  389. // update the page fields with the settings
  390. page.setData( this.data, false );
  391. // update errors for the page
  392. this.setErrors( page );
  393. }
  394. protected void doLeavePage( WizardPage<D> currentPage )
  395. {
  396. // save the settings for the current page
  397. if ( currentPage != null )
  398. {
  399. this.data = currentPage.updateData( this.data );
  400. this.fireDataUpdated( data );
  401. this.setErrors( currentPage );
  402. this.firePageExited( currentPage );
  403. currentPage.onExit( );
  404. }
  405. }
  406. protected void doEnterPage( WizardPage<D> page )
  407. {
  408. this.isVisited.add( page.getId( ) );
  409. // update the page fields with the settings
  410. this.updatePage( page );
  411. // have the UI show the page
  412. this.ui.show( page );
  413. // notify listeners
  414. this.firePageEntered( page );
  415. page.onEnter( );
  416. }
  417. protected void addError0( WizardError error )
  418. {
  419. this.userErrors.put( error.getPageId( ), error );
  420. }
  421. protected boolean isVisited( Object pageId )
  422. {
  423. return this.isVisited.contains( pageId );
  424. }
  425. protected void fireErrorsUpdated( )
  426. {
  427. for ( ErrorsUpdatedListener listener : this.errorListeners )
  428. {
  429. listener.onErrorsUpdated( );
  430. }
  431. }
  432. protected void firePageEntered( WizardPage<D> page )
  433. {
  434. for ( PageEnteredListener<D> listener : this.pageEnterListeners )
  435. {
  436. listener.onPageEntered( page );
  437. }
  438. }
  439. protected void firePageExited( WizardPage<D> page )
  440. {
  441. for ( PageExitedListener<D> listener : this.pageExitListeners )
  442. {
  443. listener.onPageExited( page );
  444. }
  445. }
  446. protected void fireDataUpdated( D data )
  447. {
  448. for ( DataUpdatedListener<D> listener : this.dataUpdatedListeners )
  449. {
  450. listener.dataUpdated( data );
  451. }
  452. }
  453. protected void fireFinished( )
  454. {
  455. for ( WizardFinishedListener listener : this.finishedListeners )
  456. {
  457. listener.finished( );
  458. }
  459. }
  460. protected void fireCancelled( )
  461. {
  462. for ( WizardCancelledListener listener : this.cancelledListeners )
  463. {
  464. listener.cancelled( );
  465. }
  466. }
  467. }