package org.multibit.hd.ui.views;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import net.miginfocom.swing.MigLayout;
import org.multibit.hd.core.config.Configurations;
import org.multibit.hd.core.events.CoreEvents;
import org.multibit.hd.core.events.ShutdownEvent;
import org.multibit.hd.core.managers.WalletManager;
import org.multibit.hd.core.services.CoreServices;
import org.multibit.hd.core.utils.OSUtils;
import org.multibit.hd.hardware.core.HardwareWalletService;
import org.multibit.hd.ui.MultiBitUI;
import org.multibit.hd.ui.events.view.ViewEvents;
import org.multibit.hd.ui.languages.Languages;
import org.multibit.hd.ui.languages.MessageKey;
import org.multibit.hd.ui.views.components.Images;
import org.multibit.hd.ui.views.components.Panels;
import org.multibit.hd.ui.views.fonts.TitleFontDecorator;
import org.multibit.hd.ui.views.themes.Themes;
import org.multibit.hd.ui.views.wizards.Wizards;
import org.multibit.hd.ui.views.wizards.credentials.CredentialsRequestType;
import org.multibit.hd.core.dto.WalletMode;
import org.multibit.hd.ui.views.wizards.welcome.WelcomeWizardState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.Locale;
/**
* <p>View to provide the following to application:</p>
* <ul>
* <li>Provision of components and layout for the main frame</li>
* </ul>
*
* @since 0.0.1
*/
public class MainView extends JFrame {
private static final Logger log = LoggerFactory.getLogger(MainView.class);
private HeaderView headerView;
private SidebarView sidebarView;
private DetailView detailView;
private FooterView footerView;
// Need to track if a wizard was showing before a refresh occurred
private boolean showExitingWelcomeWizard = false;
private boolean showExitingCredentialsWizard = false;
private boolean isCentered = false;
/**
* The credentials type to show when starting the credentials wizard
*/
private CredentialsRequestType credentialsRequestType = CredentialsRequestType.PASSWORD;
private boolean repeatLatestEvents = true;
// The Panel.applicationFrame is a global singleton in nature
@SuppressFBWarnings({"ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD"})
public MainView() {
// Define the minimum size for the frame
setMinimumSize(new Dimension(MultiBitUI.UI_MIN_WIDTH, MultiBitUI.UI_MIN_HEIGHT));
// Set the starting size
setSize(new Dimension(MultiBitUI.UI_MIN_WIDTH, MultiBitUI.UI_MIN_HEIGHT));
// Ensure app does not have Java coffee up icon on Windows
if (OSUtils.isWindows()) {
setIconImage(Images.newLogoIconImage());
}
// Provide all panels with a reference to the main frame
Panels.setApplicationFrame(this);
// Add a glass pane which dims the whole screen - used for switch (MainController#handleSwitchWallet)
// It also absorbs keystrokes and mouse events
JComponent glassPane = new JComponent() {
public void paintComponent(Graphics g) {
g.setColor(new Color(0, 0, 0, 50));
g.fillRect(0, 0, getWidth(), getHeight());
super.paintComponent(g);
}
};
glassPane.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
}
});
glassPane.addMouseListener(new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
});
getRootPane().setGlassPane(glassPane);
addWindowListener(
new WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent windowEvent) {
log.info("Hard shutdown from 'window closing' event");
CoreEvents.fireShutdownEvent(ShutdownEvent.ShutdownType.HARD);
}
});
addComponentListener(
new ComponentAdapter() {
@Override
public void componentMoved(ComponentEvent e) {
updateConfiguration();
}
@Override
public void componentResized(ComponentEvent e) {
updateConfiguration();
}
/**
* Keep the current configuration updated
*/
private void updateConfiguration() {
Rectangle bounds = getBounds();
String lastFrameBounds = String.format("%d,%d,%d,%d", bounds.x, bounds.y, bounds.width, bounds.height);
Configurations.currentConfiguration.getAppearance().setLastFrameBounds(lastFrameBounds);
}
});
}
/**
* <p>Rebuild the contents of the main view based on the current configuration and theme</p>
*
* @param isLanguageChange True if this refresh is because of a language change
*/
public void refresh(final boolean isLanguageChange) {
if (SwingUtilities.isEventDispatchThread()) {
refreshOnEventThread(isLanguageChange);
} else {
// Start the main view refresh on the EDT
SwingUtilities.invokeLater(
new Runnable() {
@Override
public void run() {
refreshOnEventThread(isLanguageChange);
}
});
}
}
/**
* <p>Rebuild the contents of the main view based on the current configuration and theme on the Swing Event thread</p>
*
* @param isLanguageChange True if this refresh is because of a language change
*/
private void refreshOnEventThread(boolean isLanguageChange) {
Preconditions.checkState(SwingUtilities.isEventDispatchThread(), "Must be in the EDT. Check MainController.");
Locale locale = Configurations.currentConfiguration.getLocale();
log.debug("Refreshing MainView with locale '{}'", locale);
// Ensure the title font is updated depending on the new locale
TitleFontDecorator.refresh(locale);
// Ensure the frame title matches the new language and wallet name
if (WalletManager.INSTANCE.getCurrentWalletSummary().isPresent()) {
setTitle(
Languages.safeText(MessageKey.MULTIBIT_HD_TITLE)
+ " - "
+ WalletManager.INSTANCE.getCurrentWalletSummary().get().getName());
} else {
// Do not have a wallet yet
setTitle(Languages.safeText(MessageKey.MULTIBIT_HD_TITLE));
}
// Clear out all the old content
getContentPane().removeAll();
// Rebuild the main content
getContentPane().add(createMainContent());
// Usually the latest events need to be repeated after a configuration change
// Switch wallet should ignore them
if (repeatLatestEvents) {
log.debug("Repeating earlier events...");
// Ensure the wallet balance is propagated out
if (WalletManager.INSTANCE.getCurrentWalletBalance().isPresent()) {
ViewEvents.fireBalanceChangedEvent(
WalletManager.INSTANCE.getCurrentWalletBalance().get(),
WalletManager.INSTANCE.getCurrentWalletBalanceWithUnconfirmed().get(),
null,
Optional.<String>absent());
}
// Catch up on recent events
CoreServices.getApplicationEventService().repeatLatestEvents();
}
// Check for any wizards that were showing before the refresh occurred
if (showExitingWelcomeWizard) {
// This section must come after a deferred hide has completed
// Select the appropriate wallet mode
final WalletMode walletMode;
if (CredentialsRequestType.HARDWARE.equals(credentialsRequestType)) {
Optional<HardwareWalletService> currentHardwareWalletService = CoreServices.getCurrentHardwareWalletService();
walletMode = WalletMode.of(currentHardwareWalletService);
} else {
walletMode = WalletMode.STANDARD;
}
// Determine the appropriate starting screen for the welcome wizard
// Must have run before so perform some additional checks
if ((WalletMode.TREZOR == walletMode || WalletMode.KEEP_KEY == walletMode)
&& !isLanguageChange) {
// Starting with an uninitialised hardware wallet
log.debug("Showing exiting welcome wizard (select hardware wallet)");
Panels.showLightBox(Wizards.newExitingWelcomeWizard(WelcomeWizardState.WELCOME_SELECT_WALLET, walletMode).getWizardScreenHolder());
} else {
log.debug("Showing exiting welcome wizard (select language)");
Panels.showLightBox(Wizards.newExitingWelcomeWizard(WelcomeWizardState.WELCOME_SELECT_LANGUAGE, walletMode).getWizardScreenHolder());
}
} else if (showExitingCredentialsWizard) {
// This section must come after a deferred hide has completed
log.debug("Showing exiting credentials wizard");
// Force an exit if the user can't get through
Panels.showLightBox(Wizards.newExitingCredentialsWizard(credentialsRequestType).getWizardScreenHolder());
} else {
log.debug("Showing detail view");
// No wizards so this reset is a wallet unlock or settings change
// The AbstractWizard.handleHide credentials unlock thread will close the wizard later
// to get the effect of everything happening behind the wizard
// Clear out all the cached screens
if (detailView != null) {
detailView.clearScreenCache();
}
}
// Use Configuration to get the last frame bounds
resizeToLastFrameBounds();
if (isCentered) {
GraphicsDevice defaultScreen = getGraphicsDevices().get(0);
GraphicsConfiguration defaultConfiguration = defaultScreen.getDefaultConfiguration();
Rectangle sb = defaultConfiguration.getBounds();
setBounds(
sb.x + sb.width / 2 - getWidth() / 2,
sb.y + sb.height / 2 - getHeight() / 2,
getWidth(),
getHeight()
);
}
log.debug("Show UI");
setVisible(true);
// Repeat the the last frame bounds to overcome bug in Swing setting x=0
resizeToLastFrameBounds();
log.debug("Refresh complete");
}
/**
* @return True if the exiting welcome wizard will be shown on a reset
*/
public boolean isShowExitingWelcomeWizard() {
return showExitingWelcomeWizard;
}
/**
* @param show True if the exiting welcome wizard should be shown during the next refresh
*/
public void setShowExitingWelcomeWizard(boolean show) {
showExitingWelcomeWizard = show;
}
/**
* @param show True if the exiting credentials wizard should be shown during the next refresh
*/
public void setShowExitingCredentialsWizard(boolean show) {
showExitingCredentialsWizard = show;
}
/**
* Attempt to get focus to the sidebar
*/
public void sidebarRequestFocus() {
sidebarView.requestFocus();
}
/**
* Update the sidebar wallet name tree node
*
* @param walletName The wallet name
*/
public void sidebarWalletName(String walletName) {
sidebarView.updateWalletTreeNode(walletName);
}
/**
* @return The contents of the main panel (header, body and footer)
*/
private JPanel createMainContent() {
Preconditions.checkState(SwingUtilities.isEventDispatchThread(), "Must execute on the EDT");
// Create the main panel and place it in this frame
MigLayout layout = new MigLayout(
Panels.migXYLayout(),
"[]", // Columns
"0[]0[]0[33:33:33]" // Rows
);
JPanel mainPanel = Panels.newPanel(layout);
// Require opaque to ensure the color is shown
mainPanel.setOpaque(true);
// Unsubscribe from events
if (headerView != null) {
log.debug("Unsubscribe existing views");
unsubscribe();
}
log.debug("Creating fresh views under MainView...");
// Create supporting views (rebuild every time for language support)
headerView = new HeaderView();
// At present we are always in single wallet mode
sidebarView = new SidebarView();
detailView = new DetailView();
footerView = new FooterView();
// Create a splitter pane
JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
// Set the divider width (3 is about right for a clean look)
splitPane.setDividerSize(3);
int sidebarWidth = MultiBitUI.SIDEBAR_LHS_PREF_WIDTH;
try {
sidebarWidth = Integer.parseInt(Configurations.currentConfiguration.getAppearance().getSidebarWidth());
} catch (NumberFormatException e) {
log.warn("Sidebar width configuration is not a number - using default");
}
if (Languages.isLeftToRight()) {
splitPane.setLeftComponent(sidebarView.getContentPanel());
splitPane.setRightComponent(detailView.getContentPanel());
splitPane.setDividerLocation(sidebarWidth);
} else {
splitPane.setLeftComponent(detailView.getContentPanel());
splitPane.setRightComponent(sidebarView.getContentPanel());
splitPane.setDividerLocation(Panels.getApplicationFrame().getWidth() - sidebarWidth);
}
// Sets the colouring for divider and borders
splitPane.setBackground(Themes.currentTheme.text());
splitPane.setBorder(
BorderFactory.createMatteBorder(
1, 0, 1, 0,
Themes.currentTheme.text()
));
splitPane.applyComponentOrientation(Languages.currentComponentOrientation());
splitPane.addPropertyChangeListener(
JSplitPane.DIVIDER_LOCATION_PROPERTY,
new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent pce) {
// Keep the current configuration up to date
Configurations.currentConfiguration.getAppearance().setSidebarWidth(String.valueOf(pce.getNewValue()));
}
}
);
// Add the supporting panels
mainPanel.add(headerView.getContentPanel(), "growx,shrink,wrap"); // Ensure header size remains fixed
mainPanel.add(splitPane, "grow,push,wrap");
mainPanel.add(footerView.getContentPanel(), "growx, growy"); // Ensure footer size remains fixed using row height sizing
return mainPanel;
}
/**
* This view is about to close so all child views should unsubscribe from events
*/
public void unsubscribe() {
if (headerView != null) {
headerView.unregister();
}
if (sidebarView != null) {
sidebarView.unregister();
}
if (detailView != null) {
detailView.unregister();
}
if (footerView != null) {
footerView.unregister();
}
}
/**
* <p>Resize the frame to the last bounds</p>
*/
private void resizeToLastFrameBounds() {
String frameDimension = Configurations.currentConfiguration.getAppearance().getLastFrameBounds();
if (frameDimension != null) {
String[] lastFrameDimension = frameDimension.split(",");
if (lastFrameDimension.length == 4) {
try {
int x = Integer.parseInt(lastFrameDimension[0]);
int y = Integer.parseInt(lastFrameDimension[1]);
int w = Integer.parseInt(lastFrameDimension[2]);
int h = Integer.parseInt(lastFrameDimension[3]);
Rectangle newBounds = new Rectangle(x, y, w, h);
// Not centered
isCentered = false;
// Place the frame in the desired position (setBounds() does not work)
setPreferredSize(new Dimension(newBounds.width, newBounds.height));
setSize(new Dimension(newBounds.width, newBounds.height));
setLocation(newBounds.x, newBounds.y);
return;
} catch (NumberFormatException e) {
log.error("Incorrect format in configuration - using defaults", e);
}
} else if (lastFrameDimension.length == 2) {
log.debug("Using partial coordinates");
try {
int w = Integer.parseInt(lastFrameDimension[0]);
int h = Integer.parseInt(lastFrameDimension[1]);
Dimension newBounds = new Dimension(w, h);
// Center in main screen
isCentered = true;
// Place the frame in the desired position (setBounds() does not work)
setPreferredSize(new Dimension(newBounds.width, newBounds.height));
setSize(new Dimension(newBounds.width, newBounds.height));
return;
} catch (NumberFormatException e) {
log.error("Incorrect format in configuration - using defaults", e);
}
}
}
log.debug("Using default coordinates");
// By default center in main screen
isCentered = true;
// Set preferred size based on internal defaults
setPreferredSize(new Dimension(MultiBitUI.UI_MIN_WIDTH, MultiBitUI.UI_MIN_HEIGHT));
}
/**
* @return The available graphics devices with the default in position 0
*/
private List<GraphicsDevice> getGraphicsDevices() {
List<GraphicsDevice> devices = Lists.newArrayList();
// Get the default screen device
GraphicsDevice defaultScreenDevice = GraphicsEnvironment
.getLocalGraphicsEnvironment()
.getDefaultScreenDevice();
for (GraphicsDevice gd : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) {
if (GraphicsDevice.TYPE_RASTER_SCREEN == gd.getType()) {
if (defaultScreenDevice == gd) {
devices.add(0, gd);
} else {
devices.add(gd);
}
}
}
Preconditions.checkState(!devices.isEmpty(), "'devices' must not be empty. Is machine in headless mode?");
return devices;
}
public void setCredentialsRequestType(CredentialsRequestType credentialsRequestType) {
this.credentialsRequestType = credentialsRequestType;
}
public CredentialsRequestType getCredentialsRequestType() {
return credentialsRequestType;
}
public void setRepeatLatestEvents(boolean repeatLatestEvents) {
this.repeatLatestEvents = repeatLatestEvents;
}
public boolean isRepeatLatestEvents() {
return repeatLatestEvents;
}
}