package org.multibit.hd.ui.views.wizards;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.eventbus.Subscribe;
import org.multibit.hd.core.events.CoreEvents;
import org.multibit.hd.core.events.EnvironmentEvent;
import org.multibit.hd.core.services.CoreServices;
import org.multibit.hd.ui.events.view.ComponentChangedEvent;
import org.multibit.hd.ui.events.view.ViewEvents;
import org.multibit.hd.ui.events.view.WizardButtonEnabledEvent;
import org.multibit.hd.ui.languages.Languages;
import org.multibit.hd.ui.languages.MessageKey;
import org.multibit.hd.ui.views.components.Labels;
import org.multibit.hd.ui.views.components.ModelAndView;
import org.multibit.hd.ui.views.components.Panels;
import org.multibit.hd.ui.views.components.display_environment_alert.DisplayEnvironmentAlertModel;
import org.multibit.hd.ui.views.components.display_environment_alert.DisplayEnvironmentAlertView;
import org.multibit.hd.ui.views.components.panels.PanelDecorator;
import org.multibit.hd.ui.views.fonts.AwesomeIcon;
import org.multibit.hd.ui.views.themes.NimbusDecorator;
import org.multibit.hd.ui.views.themes.Themes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.util.List;
/**
* <p>Abstract base class providing the following to wizard panel views:</p>
* <ul>
* <li>Standard methods common to wizard panel views</li>
* </ul>
* <p>A wizard panel view contains three sections: title, content and buttons. It relies on
* its implementers to provide the panel containing the specific components for the
* user interaction.</p>
*
* @param <M> the wizard model
* @param <P> the wizard panel model
*
* @since 0.0.1
*/
public abstract class AbstractWizardPanelView<M extends AbstractWizardModel, P> {
/**
* Avoid sharing this logger since the naming becomes confusing
*/
private static final Logger log = LoggerFactory.getLogger(AbstractWizardPanelView.class);
/**
* The overall wizard model
*/
private final M wizardModel;
/**
* The panel name to identify this panel and filter events
*/
private final String panelName;
/**
* The optional panel model (some panels are read only views)
*/
private Optional<P> panelModel = Optional.absent();
/**
* The wizard screen panel (title, contents, buttons)
*/
private JPanel wizardScreenPanel;
/**
* The content panel with the specific components for data entry/review
*/
private JPanel contentPanel;
/**
* True if the components making up this screen have been created
*/
private boolean hasComponents = false;
/**
* True if the contents making up this screen have been populated
*/
private boolean initialised = false;
// Components
private List<ModelAndView> components = Lists.newArrayList();
// Buttons
private Optional<JButton> exitButton = Optional.absent();
private Optional<JButton> cancelButton = Optional.absent();
private Optional<JButton> nextButton = Optional.absent();
private Optional<JButton> previousButton = Optional.absent();
private Optional<JButton> createButton = Optional.absent();
private Optional<JButton> restoreButton = Optional.absent();
private Optional<JButton> finishButton = Optional.absent();
private Optional<JButton> applyButton = Optional.absent();
// Labels
/**
* The title label in case it needs to be modified after initialisation (e.g. changing a hardware wallet)
*/
protected JLabel title;
/**
* @param wizard The wizard
* @param panelName The panel name to filter events from components
* @param backgroundIcon The icon for the content section background
* @param titleKey The key for the title section text
* @param values The values for the title key
*/
public AbstractWizardPanelView(
AbstractWizard<M> wizard,
String panelName,
AwesomeIcon backgroundIcon,
MessageKey titleKey,
Object... values) {
Preconditions.checkNotNull(wizard, "'wizard' must be present");
Preconditions.checkNotNull(titleKey, "'title' must be present");
this.wizardModel = wizard.getWizardModel();
this.panelName = panelName;
// All wizard panel views can receive Core and View events
ViewEvents.subscribe(this);
CoreEvents.subscribe(this);
// All wizard screen panels are decorated with the same theme and
// layout at creation so just need a simple panel to begin with
wizardScreenPanel = Panels.newRoundedPanel();
// All wizard panels require a backing model
newPanelModel();
// Create a new wizard panel and apply the wizard theme
PanelDecorator.applyWizardTheme(wizardScreenPanel);
// Add the title to the wizard
title = Labels.newTitleLabel(titleKey, values);
// Spans all 4 cells to max width and is the last to shrink
// Aligns to the top in the center with a fixed height of 90px
wizardScreenPanel.add(title, "span 4,shrink 200,aligny top,align center,h 90lp!,push,wrap");
// Provide a basic empty content panel (allows lazy initialisation later)
contentPanel = Panels.newDetailBackgroundPanel(backgroundIcon);
// Add it to the wizard panel as a placeholder
wizardScreenPanel.add(contentPanel, "span 4,grow,push,wrap");
// Add the buttons to the wizard
initialiseButtons(wizard);
}
/**
* <p>The wizard is closing so unsubscribe</p>
*/
public void unsubscribe() {
ViewEvents.unsubscribe(this);
CoreEvents.unsubscribe(this);
}
/**
* <p>Called when the wizard is first created to initialise the panel model.</p>
*
* <p>Implementers must create a new panel model and bind it to the overall wizard</p>
*/
public abstract void newPanelModel();
/**
* <p>Initialise the content section of the wizard panel just before first showing</p>
* <p>Implementers should set the layout and populate the components</p>
* <h3>Example panel creation</h3>
* <pre>
* ... initialise components ...
*
* getComponents().add(aComponent);
*
* contentPanel.setLayout(new MigLayout(
* Panels.migXYLayout(),
* "[][]", // Column constraints
* "[]10[]10[][][]10[][]" // Row constraints
* ));
*
* ... populate panel ...
* </pre>
*
* @param contentPanel The empty content panel with the current theme and initial background icon
*/
public abstract void initialiseContent(JPanel contentPanel);
/**
* <p>Initialise the content section of the wizard panel</p>
* <p>Implementers should use <code>PanelDecorator</code> to add buttons</p>
*
* @param wizard The wizard providing exit/cancel information for button selection
*/
protected abstract void initialiseButtons(AbstractWizard<M> wizard);
/**
* @param mavs The ModelAndView instances to register as potentially containing UI event handlers
*/
public void registerComponents(ModelAndView... mavs) {
Preconditions.checkNotNull(mavs, "'mavs' must be present");
// Add one by one to verify references
for (ModelAndView mav : mavs) {
Preconditions.checkNotNull(mav, "'mav' must be present");
components.add(mav);
}
}
/**
* <p>Reduced visibility since this is only required during the wizard hide process</p>
*
* @return The list of {@link ModelAndView} entries for this view to allow the deregister of UI events. Can be empty, but never null.
*/
/* package local */ List<ModelAndView> getComponents() {
return components;
}
/**
* @return The wizard model providing aggregated state information
*/
public M getWizardModel() {
return wizardModel;
}
/**
* @return The panel model specific to this view. Never null.
*/
public Optional<P> getPanelModel() {
return panelModel;
}
/**
* @return The panel name associated with this view
*/
public String getPanelName() {
return panelName;
}
/**
* <p>Get the overall wizard screen panel (title, content, buttons) lazily initialising the content as necessary</p>
*
* @param initialiseContent True if the wizard screen content should be initialised
*
* @return The wizard panel
*/
public JPanel getWizardScreenPanel(boolean initialiseContent) {
if (initialiseContent) {
if (!isInitialised()) {
initialiseContent(contentPanel);
setInitialised(true);
// At this point the user cannot have made changes
getWizardModel().setDirty(false);
}
}
return wizardScreenPanel;
}
/**
* @param panelModel The panel model
*/
public void setPanelModel(P panelModel) {
this.panelModel = Optional.fromNullable(panelModel);
}
/**
* <p>Update the view with any required view events to create a clean initial state (all initialisation will have completed)</p>
*
* <p>Default implementation is to disable the "next" button</p>
*/
public void fireInitialStateViewEvents() {
// Default is to disable the Next button
ViewEvents.fireWizardButtonEnabledEvent(getPanelName(), WizardButton.NEXT, false);
}
/**
* @return The "exit" button for this view
*/
public JButton getExitButton() {
return exitButton.get();
}
public void setExitButton(JButton exitButton) {
this.exitButton = Optional.fromNullable(exitButton);
}
/**
* @return The "cancel" button for this view
*/
public JButton getCancelButton() {
return cancelButton.get();
}
public void setCancelButton(JButton cancelButton) {
this.cancelButton = Optional.fromNullable(cancelButton);
}
/**
* @return The "next" button for this view
*/
public JButton getNextButton() {
return nextButton.get();
}
public void setNextButton(JButton nextButton) {
this.nextButton = Optional.fromNullable(nextButton);
}
/**
* @return The "previous" button for this view
*/
public JButton getPreviousButton() {
if (previousButton.isPresent()) {
return previousButton.get();
} else {
return null;
}
}
public void setPreviousButton(JButton previousButton) {
this.previousButton = Optional.fromNullable(previousButton);
}
/**
* @return The "restore" button for this view
*/
public JButton getRestoreButton() {
return restoreButton.get();
}
public void setRestoreButton(JButton recoverButton) {
this.restoreButton = Optional.fromNullable(recoverButton);
}
/**
* @return The "create" button for this view
*/
public JButton getCreateButton() {
return createButton.get();
}
public void setCreateButton(JButton createButton) {
this.createButton = Optional.fromNullable(createButton);
}
/**
* @return The "finish" button for this view
*/
public JButton getFinishButton() {
return finishButton.get();
}
public void setFinishButton(JButton finishButton) {
this.finishButton = Optional.fromNullable(finishButton);
}
/**
* @return The "apply" button for this view
*/
public JButton getApplyButton() {
return applyButton.get();
}
public void setApplyButton(JButton applyButton) {
this.applyButton = Optional.fromNullable(applyButton);
}
/**
* <p>Called before this wizard panel is about to be shown</p>
*
* <p>Typically this is where a panel view would reference the wizard model to obtain earlier values for display</p>
*
* <p>This method is guaranteed to run on the EDT</p>
*
* @return True if the panel can be shown, false if the show operation should be aborted
*/
public boolean beforeShow() {
// Default is to return OK
return true;
}
/**
* <p>Called after this wizard panel has been shown</p>
*
* <p>Typically this is where a panel view would attempt to:</p>
* <ul>
* <li>set the focus for its primary component using (see later)</li>
* <li>register a default button to speed up keyboard data entry</li>
* </ul>
*
* <p>To set focus to a primary component use this construct:</p>
*
* <pre>
* getCancelButton().requestFocusInWindow();
* </pre>
*
* <p>This method is guaranteed to run on the EDT</p>
*/
public void afterShow() {
// Do nothing
}
/**
* <p>Standard handling for environment popovers</p>
*
* @param displayEnvironmentPopoverMaV The display environment popover MaV
*/
protected void checkForEnvironmentEventPopover(ModelAndView<DisplayEnvironmentAlertModel, DisplayEnvironmentAlertView> displayEnvironmentPopoverMaV) {
// Don't log this activity since it floods the logs
// Check for any environment alerts
Optional<EnvironmentEvent> environmentEvent = CoreServices.getApplicationEventService().getLatestEnvironmentEvent();
if (environmentEvent.isPresent()) {
log.debug("Showing environment event popover");
// Provide the event as the model
displayEnvironmentPopoverMaV.getModel().setValue(environmentEvent.get());
// Show the environment alert as a popover
JPanel popoverPanel = displayEnvironmentPopoverMaV.getView().newComponentPanel();
// Potentially decorate the panel (or do nothing)
switch (environmentEvent.get().getSummary().getAlertType()) {
case DEBUGGER_ATTACHED:
popoverPanel.add(Panels.newDebuggerWarning(), "align center,wrap");
break;
case UNSUPPORTED_FIRMWARE_ATTACHED:
popoverPanel.add(Panels.newUnsupportedFirmware(), "align center,wrap");
break;
case DEPRECATED_FIRMWARE_ATTACHED:
popoverPanel.add(Panels.newDeprecatedFirmware(), "align center,wrap");
break;
case UNSUPPORTED_CONFIGURATION_ATTACHED:
popoverPanel.add(Panels.newUnsupportedConfigurationPassphrase(), "align center,wrap");
break;
default:
// Do nothing and discard the environment event to prevent multiple showings
CoreServices.getApplicationEventService().onEnvironmentEvent(null);
return;
}
// Check for an existing light box popover
if (!Panels.isLightBoxPopoverShowing()) {
// Show the popover
Panels.showLightBoxPopover(popoverPanel);
}
// Discard the environment event now that the user is aware (this prevents multiple showings)
CoreServices.getApplicationEventService().onEnvironmentEvent(null);
}
}
/**
* <p>Called before this wizard is about to be hidden.</p>
*
* <p>Typically this is where a panel view would {@link #updateFromComponentModels}, but implementations will vary</p>
*
* <p>This method is guaranteed to run on the EDT</p>
*
* @param isExitCancel True if this hide action comes from a exit or cancel operation
*
* @return True if the panel can be hidden, false if the hide operation should be aborted (perhaps due to a data error)
*/
public boolean beforeHide(boolean isExitCancel) {
// Default is to return OK
return true;
}
/**
* <p>Called when a wizard state transition occurs (e.g. "next" button click) and in response to a {@link org.multibit.hd.ui.events.view.ComponentChangedEvent}</p>
*
* <p>Implementers must:</p>
* <ol>
* <li>Update their panel model to reflect the component models (unless there is a direct reference)</li>
* <li>Update the wizard model if the panel model data is valid</li>
* </ol>
*
* <p>This method is guaranteed to run on the EDT</p>
*
* @param componentModel The component model (
*/
public abstract void updateFromComponentModels(Optional componentModel);
/**
* <p>Deregisters the default button (called automatically when the wizard closes)</p>
*/
public void deregisterDefaultButton() {
Panels.getApplicationFrame().getRootPane().setDefaultButton(null);
}
/**
* <p>Registers the default button for the wizard panel. Use this method with caution since it may give unintended side effects that affect the user experience.</p>
*
* @param button The button to use as the default (triggered on an "ENTER" key release)
*/
public void registerDefaultButton(JButton button) {
Panels.getApplicationFrame().getRootPane().setDefaultButton(button);
// Remove the binding for pressed
Panels.getApplicationFrame().getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke("ENTER"), "none");
// Target the binding for released
Panels.getApplicationFrame().getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke("released ENTER"), "press");
if (button.getText().equalsIgnoreCase(Languages.safeText(MessageKey.EXIT))) {
NimbusDecorator.applyThemeColor(Themes.currentTheme.dangerAlertBackground(), button);
} else {
NimbusDecorator.applyThemeColor(Themes.currentTheme.buttonDefaultBackground(), button);
}
}
/**
* @return True if this wizard panel has been initialised (lazy loading needs this)
*/
public boolean isInitialised() {
return initialised;
}
public void setInitialised(boolean initialised) {
this.initialised = initialised;
}
/**
* @return True if the components are all non-null (early events against uninitialised views need this to filter)
*/
public boolean hasComponents() {
return hasComponents;
}
public void setHasComponents(boolean hasComponents) {
this.hasComponents = hasComponents;
}
/**
* <p>React to a "wizard button enable" event</p>
*
* @param event The wizard button enable event
*/
@Subscribe
public void onWizardButtonEnabled(final WizardButtonEnabledEvent event) {
Preconditions.checkNotNull(event, "'event' must be present");
Preconditions.checkNotNull(panelName, "'panelName' must be present");
// Is the event applicable?
if (!event.getPanelName().equals(panelName)) {
return;
}
SwingUtilities.invokeLater(
new Runnable() {
@Override
public void run() {
// Enable the button if present
switch (event.getWizardButton()) {
case CANCEL:
if (cancelButton.isPresent()) {
cancelButton.get().setEnabled(event.isEnabled());
}
break;
case EXIT:
if (exitButton.isPresent()) {
exitButton.get().setEnabled(event.isEnabled());
}
break;
case NEXT:
if (nextButton.isPresent()) {
nextButton.get().setEnabled(event.isEnabled());
}
break;
case PREVIOUS:
if (previousButton.isPresent()) {
previousButton.get().setEnabled(event.isEnabled());
}
break;
case FINISH:
if (finishButton.isPresent()) {
finishButton.get().setEnabled(event.isEnabled());
}
break;
case APPLY:
if (applyButton.isPresent()) {
applyButton.get().setEnabled(event.isEnabled());
}
break;
case RESTORE:
if (restoreButton.isPresent()) {
restoreButton.get().setEnabled(event.isEnabled());
}
break;
case CREATE:
if (createButton.isPresent()) {
createButton.get().setEnabled(event.isEnabled());
}
break;
default:
// No dothing
}
}
});
}
/**
* <p>React to a "component model changed" event</p>
*
* @param event The wizard button enable event
*/
@Subscribe
public void onWizardComponentModelChangedEvent(ComponentChangedEvent event) {
if (panelName.equals(event.getPanelName())) {
// Default behaviour is to update
updateFromComponentModels(event.getComponentModel());
}
}
}