Example #1
0
  /**
   * Perform an asynchronous parse of the MultiBit.org Atom XML feed using JAXB
   *
   * @return A listenable future containing the result of the asynchronous read
   */
  public static ListenableFuture<AtomFeed> parseMultiBitOrgFeed() {

    ListeningExecutorService executorService =
        SafeExecutors.newSingleThreadExecutor("atom-feed-check");

    return executorService.submit(
        new Callable<AtomFeed>() {
          @Override
          public AtomFeed call() throws Exception {

            URL url = new URL("https://multibit.org/atom.xml");
            URLConnection connection = url.openConnection();

            try (InputStream is = connection.getInputStream()) {
              return JAXB.unmarshal(is, AtomFeed.class);
            }
          }
        });
  }
  @Override
  public void initialiseContent(JPanel contentPanel) {

    // Postpone the creation of the executor service to the last moment
    restoreWalletExecutorService = SafeExecutors.newSingleThreadExecutor("restore-wallet");

    contentPanel.setLayout(
        new MigLayout(
            Panels.migXYLayout(),
            "[][][]", // Column constraints
            "10[24]10[24]10[24]10[24]10[24]10" // Row constraints
            ));

    // Apply the theme
    contentPanel.setBackground(Themes.currentTheme.detailPanelBackground());

    // Initialise to failure
    backupLocationStatusLabel = Labels.newBackupLocationStatus(false);
    walletCreatedStatusLabel = Labels.newWalletCreatedStatus(false);
    caCertificateStatusLabel = Labels.newCACertsInstalledStatus(false);
    synchronizationStatusLabel = Labels.newSynchronizingStatus(false);

    // Start invisible (activates after CA certs completes)
    blocksLeftLabel = Labels.newValueLabel("0");
    blocksLeftStatusLabel = Labels.newBlocksLeft();

    // Make all labels invisible initially
    backupLocationStatusLabel.setVisible(false);
    walletCreatedStatusLabel.setVisible(false);
    caCertificateStatusLabel.setVisible(false);
    synchronizationStatusLabel.setVisible(false);
    blocksLeftLabel.setVisible(false);
    blocksLeftStatusLabel.setVisible(false);

    contentPanel.add(backupLocationStatusLabel, "wrap");
    contentPanel.add(walletCreatedStatusLabel, "wrap");
    contentPanel.add(caCertificateStatusLabel, "wrap");
    contentPanel.add(synchronizationStatusLabel, "wrap");

    contentPanel.add(blocksLeftStatusLabel, "");
    contentPanel.add(blocksLeftLabel, "wrap");
  }
/**
 * Abstract base class to provide the following to UI:
 *
 * <ul>
 *   <li>Provision of common methods to wizards
 * </ul>
 *
 * @param <M> the wizard model
 * @since 0.0.1
 */
public abstract class AbstractWizard<M extends AbstractWizardModel> {

  private static final Logger log = LoggerFactory.getLogger(AbstractWizard.class);

  /** The wizard screen holder card layout to which each wizard screen panel is added */
  private final WizardCardLayout cardLayout = new WizardCardLayout(0, 0);
  /** Keeps all of the wizard screen panels in a card layout */
  private final JPanel wizardScreenHolder = Panels.newPanel(cardLayout);

  private M wizardModel;
  protected Optional wizardParameter = Optional.absent();

  /** True if the wizard supports the Exit button */
  private final boolean exiting;

  /** Maps the panel name to the panel views */
  protected Map<String, AbstractWizardPanelView> wizardViewMap = Maps.newHashMap();

  /** Ensures we only have a single thread managing the wizard hide operation */
  private static final ListeningExecutorService wizardHideExecutorService =
      SafeExecutors.newSingleThreadExecutor("wizard-hide");

  /**
   * @param wizardModel The overall wizard data model containing the aggregate information of all
   *     components in the wizard
   * @param isExiting True if the exit button should trigger an application shutdown
   * @param wizardParameter An optional parameter that can be referenced during construction
   */
  protected AbstractWizard(M wizardModel, boolean isExiting, Optional wizardParameter) {
    this(wizardModel, isExiting, wizardParameter, true);
  }

  /**
   * @param wizardModel The overall wizard data model containing the aggregate information of all
   *     components in the wizard
   * @param isExiting True if the exit button should trigger an application shutdown
   * @param wizardParameter An optional parameter that can be referenced during construction
   * @param escapeIsCancel If true, ESC cancels the wizard, if false, it does nothing
   */
  protected AbstractWizard(
      M wizardModel, boolean isExiting, Optional wizardParameter, boolean escapeIsCancel) {

    Preconditions.checkNotNull(wizardModel, "'model' must be present");

    log.debug("Building wizard...");

    this.wizardModel = wizardModel;
    this.exiting = isExiting;
    this.wizardParameter = wizardParameter;

    // Subscribe to events
    ViewEvents.subscribe(this);
    CoreEvents.subscribe(this);

    // Optionally bind the ESC key to a Cancel event (escape to safety)
    if (escapeIsCancel) {
      wizardScreenHolder
          .getInputMap(JPanel.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
          .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "quit");
      wizardScreenHolder.getActionMap().put("quit", getCancelAction());
    }

    // TODO Bind the ENTER key to a Next/Finish/Apply event to speed up data entry through keyboard
    // wizardPanel.getInputMap(JPanel.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "next");
    // wizardPanel.getActionMap().put("next", getNextAction(null));

    log.debug("Populating view map and firing initial state view events...");

    // Populate based on the current locale
    populateWizardViewMap(wizardViewMap);

    // Once all the views are created allow events to occur
    for (Map.Entry<String, AbstractWizardPanelView> entry : wizardViewMap.entrySet()) {

      // Ensure the panel is in the correct starting state
      entry.getValue().fireInitialStateViewEvents();
    }

    wizardScreenHolder.setMinimumSize(
        new Dimension(MultiBitUI.WIZARD_MIN_WIDTH, MultiBitUI.WIZARD_MIN_HEIGHT));
    wizardScreenHolder.setPreferredSize(
        new Dimension(MultiBitUI.WIZARD_MIN_WIDTH, MultiBitUI.WIZARD_MIN_HEIGHT));
    wizardScreenHolder.setSize(
        new Dimension(MultiBitUI.WIZARD_MIN_WIDTH, MultiBitUI.WIZARD_MIN_HEIGHT));

    // Show the panel specified by the initial state
    show(wizardModel.getPanelName());
  }

  /** This wizard is about to close */
  public void unsubscribe() {
    ViewEvents.unsubscribe(this);
    CoreEvents.unsubscribe(this);
    // Further events are handled by subclasses (e.g. HardwareWallet)
  }

  /**
   * Unsubscribe from all events. This also unsubscribes the internal model and all internal views
   */
  public void unsubscribeAll() {

    unsubscribe();
    for (AbstractWizardPanelView view : getWizardViewMap().values()) {
      view.unsubscribe();
    }

    getWizardModel().unsubscribe();
  }

  /**
   * Show the named panel
   *
   * <p>This is guaranteed to be on the EDT
   *
   * @param panelName The panel name
   */
  public void show(String panelName) {

    Preconditions.checkState(
        SwingUtilities.isEventDispatchThread(), "This method should run on the EDT");

    if (!wizardViewMap.containsKey(panelName)) {
      log.error(
          "'{}' is not a valid panel name. Check the panel has been registered in the view map. Registered panels are\n{}",
          wizardViewMap.keySet());
      return;
    }

    final AbstractWizardPanelView wizardPanelView = wizardViewMap.get(panelName);

    if (!wizardPanelView.isInitialised()) {

      // Initialise the wizard screen panel and add it to the card layout parent
      wizardScreenHolder.add(wizardPanelView.getWizardScreenPanel(true), panelName);
    }

    // De-register any existing default buttons from previous panels
    wizardPanelView.deregisterDefaultButton();

    // Provide warning that the panel is about to be shown
    if (wizardPanelView.beforeShow()) {

      // No abort so show (use info to assist with FEST debugging)
      log.info("Showing wizard panel: {}", panelName);
      cardLayout.show(wizardScreenHolder, panelName);

      // We must ensure that all other EDT processing has completed before
      // calling afterShow() to guarantee visibility of components
      // Failure to do this causes problems with popovers during startup
      SwingUtilities.invokeLater(
          new Runnable() {
            @Override
            public void run() {
              wizardPanelView.afterShow();
            }
          });
    }
  }

  /**
   * Hide the wizard if <code>beforeHide</code> returns true
   *
   * <p>Guaranteed to run on the EDT
   *
   * @param panelName The panel name
   * @param isExitCancel True if this hide operation comes from an exit or cancel
   */
  public void hide(final String panelName, final boolean isExitCancel) {

    log.debug("Hide requested for {} with exitCancel {} ", panelName, isExitCancel);

    if (!wizardViewMap.containsKey(panelName)) {
      log.error(
          "'{}' is not a valid panel name. Check the panel has been registered in the view map. Registered panels are\n{}",
          wizardViewMap.keySet());
      return;
    }

    final AbstractWizardPanelView wizardPanelView = wizardViewMap.get(panelName);

    // Provide warning that the panel is about to be hidden
    if (wizardPanelView.beforeHide(isExitCancel)) {

      // No cancellation so go ahead with the hide
      handleHide(panelName, isExitCancel, wizardPanelView);
    }
  }

  /**
   * Add fresh content to the wizard view map
   *
   * <p>The map will be empty whenever this is called
   */
  protected abstract void populateWizardViewMap(Map<String, AbstractWizardPanelView> wizardViewMap);

  protected Map<String, AbstractWizardPanelView> getWizardViewMap() {
    return wizardViewMap;
  }

  /** @return The wizard panel */
  public JPanel getWizardScreenHolder() {
    return wizardScreenHolder;
  }

  /** @return The wizard panel view associated with the given panel name */
  public AbstractWizardPanelView getWizardPanelView(String panelName) {
    return wizardViewMap.get(panelName);
  }

  /** @return True if the wizard should trigger an "exit" event rather than a "close" */
  public boolean isExiting() {
    return exiting;
  }

  /** @return The standard "exit" action to trigger application shutdown */
  public Action getExitAction() {

    return new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {

        // Can immediately close since no data will be lost
        hide(wizardModel.getPanelName(), true);

        // After panel has hidden we can initiate the shutdown so that MainController
        // will gracefully close the application
        CoreEvents.fireShutdownEvent(ShutdownEvent.ShutdownType.HARD);
      }
    };
  }

  /** @return The standard "cancel" action to trigger the removal of the lightbox */
  public Action getCancelAction() {

    return new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {

        if (getWizardModel().isDirty()) {

          if (Panels.isLightBoxPopoverShowing()) {
            // Ignore this and rely on popover catching the cancel itself
            return;
          }

          // Check with the user about throwing away their data (handle the outcome with a
          // WizardPopoverHideEvent)
          Panels.showLightBoxPopover(
              Popovers.newDiscardYesNoPopoverMaV(getWizardModel().getPanelName())
                  .getView()
                  .newComponentPanel());

        } else {

          // Can immediately close since no data will be lost
          hide(wizardModel.getPanelName(), true);
        }
      }
    };
  }

  /**
   * @param wizardView The wizard view (providing a reference to its underlying panel model)
   * @return The "finish" action based on the model state
   */
  public <P> Action getFinishAction(final AbstractWizardPanelView<M, P> wizardView) {

    return new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {

        // We are finishing and this may be a default button
        // which has non-standard painting behaviour
        ((JButton) e.getSource()).setEnabled(false);

        // Ensure the button disables before hide giving a cleaner transition
        // Nimbus paints the text a different colour to the icon otherwise
        SwingUtilities.invokeLater(
            new Runnable() {
              @Override
              public void run() {
                hide(wizardModel.getPanelName(), false);
              }
            });
      }
    };
  }

  /**
   * @param wizardView The wizard view (providing a reference to its underlying panel model)
   * @return The "apply" action based on the model state
   */
  public <P> Action getApplyAction(final AbstractWizardPanelView<M, P> wizardView) {

    return new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {

        hide(wizardModel.getPanelName(), false);
      }
    };
  }

  /**
   * @param wizardPanelView The wizard panel view (providing a reference to its underlying panel
   *     model)
   * @return The "next" action based on the model state
   */
  public <P> Action getNextAction(final AbstractWizardPanelView<M, P> wizardPanelView) {

    return new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {

        // We are moving to the next panel view and this may be a default button
        // which has non-standard painting behaviour
        ((JButton) e.getSource()).setEnabled(false);

        // Ensure the button disables before hide giving a cleaner transition
        // Nimbus paints the text a different colour to the icon otherwise
        SwingUtilities.invokeLater(
            new Runnable() {
              @Override
              public void run() {
                // Ensure the panel updates its model (the button is outside of the panel itself)
                wizardPanelView.updateFromComponentModels(Optional.absent());

                // Move to the next state
                wizardModel.showNext();

                // Show the panel based on the state
                show(wizardModel.getPanelName());
              }
            });
      }
    };
  }

  /**
   * @param wizardView The wizard view (providing a reference to its underlying panel model)
   * @return The "previous" action based on the model state
   */
  public <P> Action getPreviousAction(final AbstractWizardPanelView<M, P> wizardView) {

    return new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {

        // Ensure the panel updates its model (the button is outside of the panel itself)
        wizardView.updateFromComponentModels(Optional.absent());

        // Aggregate the panel information into the wizard model

        // Move to the previous state
        wizardModel.showPrevious();

        // Show the panel based on the state
        show(wizardModel.getPanelName());
      }
    };
  }

  /**
   * @param wizardView The wizard view (providing a reference to its underlying panel model)
   * @return The "restore" action based on the model state
   */
  public <P> Action getRestoreAction(final AbstractWizardPanelView<M, P> wizardView) {

    return new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {

        // The UI will lock up during handover so prevent further events
        JButton source = (JButton) e.getSource();
        source.setEnabled(false);

        // Since #17 all restore work is done by the welcome wizard
        // See MainController for the hand over code
        hide(CredentialsState.CREDENTIALS_RESTORE.name(), false);
      }
    };
  }

  /**
   * @param wizardView The wizard view (providing a reference to its underlying panel model)
   * @return The "create" action based on the model state
   */
  public <P> Action getCreateAction(final AbstractWizardPanelView<M, P> wizardView) {

    return new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {

        // The UI will lock up during handover so prevent further events
        JButton source = (JButton) e.getSource();
        source.setEnabled(false);

        // Since #17 all create work is done by the welcome wizard
        // See MainController for the hand over code
        hide(CredentialsState.CREDENTIALS_CREATE.name(), false);
      }
    };
  }

  @Subscribe
  public void onWizardPopoverHideEvent(WizardPopoverHideEvent event) {

    if (getWizardModel().getPanelName().equals(event.getPanelName())) {

      if (getWizardModel().isDirty() && !event.isExitCancel()) {

        // User has authorised the underlying panel to be closed
        hide(wizardModel.getPanelName(), true);
      }
    }
  }

  @Subscribe
  public void onWizardDeferredHideEvent(WizardDeferredHideEvent event) {

    Preconditions.checkState(
        SwingUtilities.isEventDispatchThread(),
        "This method should be run on the EDT. Check ViewEvents.");

    // Fail fast
    if (wizardViewMap.isEmpty()) {
      log.trace("Wizard panel view {} is still finalising.", event.getPanelName());
      return;
    }

    String panelName = event.getPanelName();

    if (getWizardModel().getPanelName().equals(panelName)) {

      final AbstractWizardPanelView wizardPanelView = wizardViewMap.get(panelName);

      // This is a deferred hide so don't call hide() again
      handleHide(panelName, event.isExitCancel(), wizardPanelView);
    }
  }

  /**
   * Hide the wizard
   *
   * <p>This method is guaranteed to run on the EDT
   *
   * @param panelName The panel name
   * @param isExitCancel True if this hide operation comes from an exit or cancel
   * @param wizardPanelView The wizard panel view from the wizard view map
   */
  protected void handleHide(
      final String panelName, final boolean isExitCancel, AbstractWizardPanelView wizardPanelView) {

    log.debug("Handle hide starting: '{}' ExitCancel: {}", panelName, isExitCancel);

    // De-register
    wizardPanelView.deregisterDefaultButton();

    // Ensure we unsubscribe the wizard from all further events
    getWizardModel().unsubscribe();
    unsubscribe();

    // Issue the wizard hide event before the hide takes place to give panel views time to update
    ViewEvents.fireWizardHideEvent(panelName, wizardModel, isExitCancel);

    // Required to run on a new thread since this may take some time to complete
    wizardHideExecutorService.submit(
        new Runnable() {
          @Override
          public void run() {

            log.debug("Hide and deregister wizard: '{}'", this.getClass().getSimpleName());

            // Require some extra time to get the rest of the UI started for credentials wizard
            // There is no chance of the system showing a light box during this time so this
            // operation is safe
            if (CredentialsState.CREDENTIALS_ENTER_PASSWORD.name().equals(panelName)) {
              log.trace("Blocking to allow UI startup to complete");
              Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
            }

            // Work through the view map ensuring all components are deregistered from UI events
            log.trace("Deregister {} views and their component(s)", wizardViewMap.size());
            for (Map.Entry<String, AbstractWizardPanelView> entry : wizardViewMap.entrySet()) {

              AbstractWizardPanelView panelView = entry.getValue();

              // Ensure we deregister the wizard panel view (and model if present) for events
              try {

                // Unsubscribe from events
                panelView.unsubscribe();
                log.trace(
                    "Deregistered wizard panel view '{}' from UI events", panelView.getPanelName());

                if (panelView.getPanelModel().isPresent()) {
                  Object panelModel = panelView.getPanelModel().get();
                  // May get some false positives from this approach
                  CoreEvents.unsubscribe(panelModel);
                  log.trace(
                      "Deregistered wizard panel model '{}' from UI events",
                      panelView.getPanelName());
                }

              } catch (NullPointerException | IllegalArgumentException e) {
                log.warn(
                    "Wizard panel model/view '{}' was not registered", panelView.getPanelName(), e);
              }

              // Deregister all components
              @SuppressWarnings("unchecked")
              List<ModelAndView> mavs = panelView.getComponents();
              for (ModelAndView mav : mavs) {
                mav.unsubscribe();
              }
              log.trace(
                  "Closed {} registered component(s) from wizard panel view '{}'",
                  mavs.size(),
                  panelView.getPanelName());

              // Remove the references
              mavs.clear();
            }

            // Depopulate the map to ensure non-AWT references are removed
            wizardViewMap.clear();

            // Hiding the light box must be on the EDT
            SwingUtilities.invokeLater(
                new Runnable() {
                  @Override
                  public void run() {

                    log.trace("Handle hide remove light box: '{}'", panelName);

                    // This removes the reference to the wizard allowing for garbage collection
                    Panels.hideLightBoxIfPresent();

                    // Clear the deferred hide
                    Panels.setDeferredHideEventInProgress(false);
                  }
                });
          }
        });
  }

  /** @return The wizard model */
  public M getWizardModel() {
    return wizardModel;
  }

  public void setWizardModel(M wizardModel) {
    Preconditions.checkNotNull(wizardModel, "'model' must be present");
    this.wizardModel = wizardModel;
  }
}