From fdb11992237c89464aa32224d72d754c33e2af3f Mon Sep 17 00:00:00 2001 From: Braids Date: Fri, 19 Aug 2011 02:22:03 +0000 Subject: [PATCH] - Issue 106: Add splash screen just under progress bar, and load all cards at startup. --- .gitattributes | 1 + src/main/java/forge/CardReader.java | 50 ++-- .../gui/MultiPhaseProgressMonitorWithETA.java | 228 +++++++++++------- src/main/java/forge/view/FView.java | 8 + .../forge/view/swing/ApplicationView.java | 59 ++++- .../java/forge/view/swing/SplashFrame.java | 118 +++++++++ .../braids/util/UtilFunctions.java | 32 ++- .../card/cardFactory/CardFactoryTest.java | 13 +- 8 files changed, 400 insertions(+), 109 deletions(-) create mode 100644 src/main/java/forge/view/swing/SplashFrame.java diff --git a/.gitattributes b/.gitattributes index f829fb6173f..643c9699fc5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9735,6 +9735,7 @@ src/main/java/forge/view/package-info.java svneol=native#text/plain src/main/java/forge/view/swing/ApplicationView.java svneol=native#text/plain src/main/java/forge/view/swing/Main.java svneol=native#text/plain src/main/java/forge/view/swing/OldGuiNewGame.java svneol=native#text/plain +src/main/java/forge/view/swing/SplashFrame.java -text src/main/java/forge/view/swing/package-info.java svneol=native#text/plain src/main/java/net/slightlymagic/braids/LICENSE.txt svneol=native#text/plain src/main/java/net/slightlymagic/braids/util/ClumsyRunnable.java svneol=native#text/plain diff --git a/src/main/java/forge/CardReader.java b/src/main/java/forge/CardReader.java index a6a70827a18..28da5df7e20 100644 --- a/src/main/java/forge/CardReader.java +++ b/src/main/java/forge/CardReader.java @@ -1,16 +1,12 @@ package forge; -import com.google.code.jyield.Generator; -import com.google.code.jyield.YieldUtils; -import forge.card.trigger.TriggerHandler; -import forge.error.ErrorViewer; -import forge.gui.MultiPhaseProgressMonitorWithETA; -import forge.properties.NewConstants; -import net.slightlymagic.braids.util.UtilFunctions; -import net.slightlymagic.braids.util.generator.FindNonDirectoriesSkipDotDirectoriesGenerator; -import net.slightlymagic.braids.util.generator.GeneratorFunctions; - -import java.io.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.nio.charset.Charset; import java.util.Enumeration; import java.util.Locale; @@ -20,6 +16,20 @@ import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import net.slightlymagic.braids.util.UtilFunctions; +import net.slightlymagic.braids.util.generator.FindNonDirectoriesSkipDotDirectoriesGenerator; +import net.slightlymagic.braids.util.generator.GeneratorFunctions; +import net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor; +import net.slightlymagic.braids.util.progress_monitor.StderrProgressMonitor; + +import com.google.code.jyield.Generator; +import com.google.code.jyield.YieldUtils; + +import forge.card.trigger.TriggerHandler; +import forge.error.ErrorViewer; +import forge.properties.NewConstants; +import forge.view.FView; + /** *

CardReader class.

@@ -165,12 +175,19 @@ public class CardReader implements Runnable, NewConstants { protected final Card loadCardsUntilYouFind(final String cardName) { Card result = null; - MultiPhaseProgressMonitorWithETA monitor; + BraidsProgressMonitor monitor = null; + final FView view = Singletons.getView(); + if (view != null) { + monitor = view.getCardLoadingProgressMonitor(); + } + + if (monitor == null) { + monitor = new StderrProgressMonitor(1, 0L); + } + if (zip != null) { - monitor = new MultiPhaseProgressMonitorWithETA("Forge - Loading card database from zip file", 1, - estimatedFilesRemaining, 1.0f); - + monitor.setTotalUnitsThisPhase(estimatedFilesRemaining); ZipEntry entry; // zipEnum was initialized in the constructor. @@ -197,8 +214,7 @@ public class CardReader implements Runnable, NewConstants { findNonDirsIterable = YieldUtils.toIterable(findNonDirsGen); } - monitor = new MultiPhaseProgressMonitorWithETA("Forge - Loading card database from files", 1, - estimatedFilesRemaining, 1.0f); + monitor.setTotalUnitsThisPhase(estimatedFilesRemaining); for (File cardTxtFile : findNonDirsIterable) { if (!cardTxtFile.getName().endsWith(".txt")) { diff --git a/src/main/java/forge/gui/MultiPhaseProgressMonitorWithETA.java b/src/main/java/forge/gui/MultiPhaseProgressMonitorWithETA.java index 2b31112c7e5..ba9d0d3b4ed 100644 --- a/src/main/java/forge/gui/MultiPhaseProgressMonitorWithETA.java +++ b/src/main/java/forge/gui/MultiPhaseProgressMonitorWithETA.java @@ -1,9 +1,14 @@ package forge.gui; +import java.lang.reflect.InvocationTargetException; + import javax.swing.JDialog; import javax.swing.JProgressBar; +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; import forge.Gui_ProgressBarWindow; +import net.slightlymagic.braids.util.UtilFunctions; import net.slightlymagic.braids.util.progress_monitor.BaseProgressMonitor; /** @@ -32,7 +37,11 @@ public class MultiPhaseProgressMonitorWithETA extends BaseProgressMonitor { } /** * Create a GUI progress monitor and open its first dialog. - * + * + * Like all swing components, this constructor must be invoked from the + * swing Event Dispatching Thread. The rest of the methods of this class + * are exempt from this requirement. + * * @param title the title to give the dialog box(es) * * @param numPhases the total number of phases to expect @@ -51,108 +60,150 @@ public class MultiPhaseProgressMonitorWithETA extends BaseProgressMonitor { long totalUnitsFirstPhase, float minUIUpdateIntervalSec, float[] phaseWeights) { - super(numPhases, totalUnitsFirstPhase, minUIUpdateIntervalSec, + super(numPhases, totalUnitsFirstPhase, minUIUpdateIntervalSec, phaseWeights); - + + if (!SwingUtilities.isEventDispatchThread()) { + throw new IllegalStateException("must be called from within an event dispatch thread"); + } + this.title = title; + + if (totalUnitsFirstPhase > 0 && dialog == null) { + throw new IllegalStateException("dialog is null"); + } } - - /** + + /** * For developer testing. + * + * @param args ignored */ - public static void main(String[] args) { + public static void main(final String[] args) { System.out.println("Initializing..."); - MultiPhaseProgressMonitorWithETA monitor = - new MultiPhaseProgressMonitorWithETA("Testing 2 phases", 2, 5000, - 1.0f, null); - - System.out.println("Running..."); + SwingUtilities.invokeLater(new Runnable() { + public void run() { - for (int i = 0; i <= 5000; i++) { - monitor.incrementUnitsCompletedThisPhase(1); - - System.out.print("\ri = " + i); - - try { - Thread.sleep(1); - } catch (InterruptedException ignored) { - ; - } - } - System.out.println(); - - monitor.markCurrentPhaseAsComplete(2000); + final int totalUnitsFirstPhase = 5000; + final MultiPhaseProgressMonitorWithETA monitor = + new MultiPhaseProgressMonitorWithETA("Testing 2 phases", 2, totalUnitsFirstPhase, 1.0f, + new float[] {2, 1}); - for (int i = 0; i <= 2000; i++) { - monitor.incrementUnitsCompletedThisPhase(1); - - System.out.print("\ri = " + i); - - try { - Thread.sleep(1); - } catch (InterruptedException ignored) { - ; - } - } - - monitor.markCurrentPhaseAsComplete(0); + SwingWorker worker = new SwingWorker() { + @Override + public Object doInBackground() { - System.out.println(); - - System.out.println("Done!"); + System.out.println("Running..."); + + for (int i = 0; i <= totalUnitsFirstPhase; i++) { + monitor.incrementUnitsCompletedThisPhase(1); + + System.out.print("\ri = " + i); + + try { + Thread.sleep(1); + } catch (InterruptedException ignored) { + // blank + } + } + System.out.println(); + + final int totalUnitsSecondPhase = 2000; + monitor.markCurrentPhaseAsComplete(totalUnitsSecondPhase); + + for (int i = 0; i <= totalUnitsSecondPhase; i++) { + monitor.incrementUnitsCompletedThisPhase(1); + + System.out.print("\ri = " + i); + + try { + Thread.sleep(1); + } catch (InterruptedException ignored) { + // blank + } + } + + monitor.markCurrentPhaseAsComplete(0); + + System.out.println(); + System.out.println("Done!"); + + return null; + } + + }; + + worker.execute(); + } + }); } - - + + @Override /** * @param numUnits cannot be higher than Integer.MAX_VALUE * * @see net.slightlymagic.braids.util.progress_monitor.ProgressMonitor#setTotalUnitsThisPhase(long) */ - public void setTotalUnitsThisPhase(long numUnits) { + public void setTotalUnitsThisPhase(final long numUnits) { super.setTotalUnitsThisPhase(numUnits); if (numUnits > Integer.MAX_VALUE) { throw new IllegalArgumentException("numUnits must be <= " + Integer.MAX_VALUE); } - if (numUnits > 0) { - // (Re)create the progress bar. - if (dialog != null) { - dialog.dispose(); - dialog = null; - } - - dialog = new Gui_ProgressBarWindow(); - dialog.setTitle(title); - dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); - dialog.setVisible(true); - dialog.setResizable(true); - dialog.getProgressBar().setIndeterminate(false); - dialog.setProgressRange(0, (int) numUnits); - dialog.reset(); + if (numUnits > 0) { + // dialog must exist before we exit this method. + UtilFunctions.invokeInEventDispatchThreadAndWait(new Runnable() { + public void run() { + // (Re)create the progress bar. + if (dialog != null) { + dialog.dispose(); + dialog = null; + } + + dialog = new Gui_ProgressBarWindow(); + } + }); + } + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + dialog.setTitle(title); + dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + dialog.setVisible(true); + dialog.setResizable(true); + dialog.getProgressBar().setIndeterminate(false); + dialog.setProgressRange(0, (int) numUnits); + dialog.reset(); + + JProgressBar bar = dialog.getProgressBar(); + bar.setString(""); + bar.setStringPainted(true); + bar.setValue(0); + } + }); - JProgressBar bar = dialog.getProgressBar(); - bar.setString(""); - bar.setStringPainted(true); - bar.setValue(0); - } } - + @Override /** * @see net.slightlymagic.braids.util.progress_monitor.ProgressMonitor#incrementUnitsCompletedThisPhase(long) */ - public void incrementUnitsCompletedThisPhase(long numUnits) { + public void incrementUnitsCompletedThisPhase(final long numUnits) { super.incrementUnitsCompletedThisPhase(numUnits); - - for (int i = 0 ; i < numUnits ; i++) { - dialog.increment(); - } + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + for (int i = 0 ; i < numUnits ; i++) { + dialog.increment(); + } + } + }); if (shouldUpdateUI()) { @@ -187,23 +238,38 @@ public class MultiPhaseProgressMonitorWithETA extends BaseProgressMonitor { * * @param message the message to display */ - public void displayUpdate(String message) { + public void displayUpdate(final String message) { - // i've been having trouble getting the dialog to display its title. - dialog.setTitle(title); + final Runnable proc = new Runnable() { + public void run() { + // i've been having trouble getting the dialog to display its title. + dialog.setTitle(title); - JProgressBar bar = dialog.getProgressBar(); - bar.setString(message); - - justUpdatedUI(); + JProgressBar bar = dialog.getProgressBar(); + bar.setString(message); + + justUpdatedUI(); + } + }; + + if (SwingUtilities.isEventDispatchThread()) { + proc.run(); + } + else { + SwingUtilities.invokeLater(proc); + } } - + @Override - public void dispose() { - getDialog().dispose(); + public final void dispose() { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + getDialog().dispose(); + } + }); } - + /** * @return the JDialog for the current phase; use this judiciously to diff --git a/src/main/java/forge/view/FView.java b/src/main/java/forge/view/FView.java index 95af949aaa5..e5107c3c263 100644 --- a/src/main/java/forge/view/FView.java +++ b/src/main/java/forge/view/FView.java @@ -1,5 +1,6 @@ package forge.view; +import net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor; import forge.model.FModel; /** @@ -15,4 +16,11 @@ public interface FView { */ void setModel(FModel model); + /** + * Get the progress monitor for loading all cards at once. + * + * @return a progress monitor having only one phase; may be null + */ + BraidsProgressMonitor getCardLoadingProgressMonitor(); + } diff --git a/src/main/java/forge/view/swing/ApplicationView.java b/src/main/java/forge/view/swing/ApplicationView.java index 8c70264ebfb..be97992e315 100644 --- a/src/main/java/forge/view/swing/ApplicationView.java +++ b/src/main/java/forge/view/swing/ApplicationView.java @@ -1,8 +1,13 @@ package forge.view.swing; +import java.lang.reflect.InvocationTargetException; + import javax.swing.SwingUtilities; import javax.swing.UIManager; +import net.slightlymagic.braids.util.UtilFunctions; +import net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor; + import com.esotericsoftware.minlog.Log; import forge.AllZone; @@ -22,11 +27,46 @@ import forge.view.swing.OldGuiNewGame.CardStackOffsetAction; * The main view for Forge: a java swing application. */ public class ApplicationView implements FView { + + private SplashFrame splashFrame; + /** - * Constructor. + * The splashFrame field is guaranteed to exist when this constructor + * exits. */ - public ApplicationView() { // NOPMD by Braids on 8/7/11 1:14 PM: Damnation if it's here; Damnation if it's not. - // TODO: insert splash window here + public ApplicationView() { + + // We must use invokeAndWait here to fulfill the constructor's + // contract. + + UtilFunctions.invokeInEventDispatchThreadAndWait(new Runnable() { + public void run() { + splashFrame = new SplashFrame(); + } + }); + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + splashFrame.setVisible(true); + } + }); + } + + /* (non-Javadoc) + * @see forge.view.FView#getCardLoadingProgressMonitor() + */ + @Override + public final BraidsProgressMonitor getCardLoadingProgressMonitor() { + BraidsProgressMonitor result; + + if (splashFrame == null) { + result = null; + } + else { + result = splashFrame.getMonitor(); + } + + return result; } /* (non-Javadoc) @@ -73,11 +113,23 @@ public class ApplicationView implements FView { } }); + AllZone.getCardFactory(); // forces preloading of all cards try { Constant.Runtime.GameType[0] = Constant.GameType.Constructed; SwingUtilities.invokeLater(new Runnable() { // NOPMD by Braids on 8/7/11 1:07 PM: this isn't a web app public void run() { AllZone.setComputer(new ComputerAI_Input(new ComputerAI_General())); + + getCardLoadingProgressMonitor().dispose(); + + // Enable only one of the following two lines. The second + // is useful for debugging. + + splashFrame.dispose(); + //splashFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + + + splashFrame = null; new OldGuiNewGame(); } }); @@ -85,6 +137,5 @@ public class ApplicationView implements FView { ErrorViewer.showError(ex); } - } } diff --git a/src/main/java/forge/view/swing/SplashFrame.java b/src/main/java/forge/view/swing/SplashFrame.java new file mode 100644 index 00000000000..ab7337e6db7 --- /dev/null +++ b/src/main/java/forge/view/swing/SplashFrame.java @@ -0,0 +1,118 @@ +package forge.view.swing; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Rectangle; + +import javax.swing.ImageIcon; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; + +import forge.gui.MultiPhaseProgressMonitorWithETA; + +/** + * Shows the splash frame as the application starts. + */ +@SuppressWarnings("serial") +public class SplashFrame extends JFrame { + + private static final Color WHITE_COLOR = new Color(255, 255, 255); + private static final int DISCLAIMER_EAST_WEST_PADDING_PX = 40; // NOPMD by Braids on 8/17/11 9:06 PM + private static final int DISCLAIMER_FONT_SIZE = 9; // NOPMD by Braids on 8/17/11 9:06 PM + private static final int DISCLAIMER_NORTH_PADDING_PX = 300; // NOPMD by Braids on 8/17/11 9:06 PM + private static final int DISCLAIMER_HEIGHT_PX = 20; // NOPMD by Braids on 8/17/11 9:06 PM + + private MultiPhaseProgressMonitorWithETA monitor; + + + /** + *

Create the frame; this must be called from an event + * dispatch thread.

+ * + * + * throws {@link IllegalStateException} if not called from an event + * dispatch thread. + */ + public SplashFrame() { + super(); + + if (!SwingUtilities.isEventDispatchThread()) { + throw new IllegalStateException("must be called from an event dispatch thread"); + } + + setUndecorated(true); + + final ImageIcon bgIcon = new ImageIcon("res/images/ui/forgeSplash by moomarc.jpg"); + + final int splashWidthPx = bgIcon.getIconWidth(); + final int splashHeightPx = bgIcon.getIconHeight(); + + monitor = new MultiPhaseProgressMonitorWithETA("Loading card database", 1, + 1, 1.0f); + + final JDialog progressBarDialog = monitor.getDialog(); + + final Rectangle progressRect = progressBarDialog.getBounds(); + + setMinimumSize(new Dimension(splashWidthPx, splashHeightPx)); + setLocation(progressRect.x, progressRect.y + progressRect.height); + + setResizable(false); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + final JPanel contentPane = new JPanel(); + contentPane.setBorder(new EmptyBorder(0, 0, 0, 0)); + setContentPane(contentPane); + contentPane.setLayout(null); + + final JLabel lblDisclaimer = new JLabel("Forge is not affiliated in any way with Wizards of the Coast."); + + // we can't do multiline labels. + //+ "\nIt is open source software, released under the GNU Public License." + //+ "\n And while we have your attention, go buy some Magic: the Gathering cards!" + + lblDisclaimer.setBounds(DISCLAIMER_EAST_WEST_PADDING_PX, DISCLAIMER_NORTH_PADDING_PX, + splashWidthPx - (2 * DISCLAIMER_EAST_WEST_PADDING_PX), + DISCLAIMER_HEIGHT_PX); + + lblDisclaimer.setFont(new Font("Tahoma", Font.PLAIN, DISCLAIMER_FONT_SIZE)); + lblDisclaimer.setHorizontalAlignment(SwingConstants.CENTER); + lblDisclaimer.setForeground(WHITE_COLOR); + contentPane.add(lblDisclaimer); + + + // Add background image. + contentPane.setOpaque(false); + final JLabel bgLabel = new JLabel(bgIcon); + + // Do not pass Integer.MIN_VALUE directly here; it must be packaged in an Integer + // instance. Otherwise, GUI components will not draw unless moused over. + getLayeredPane().add(bgLabel, Integer.valueOf(Integer.MIN_VALUE)); + + bgLabel.setBounds(0, 0, bgIcon.getIconWidth(), bgIcon.getIconHeight()); + + pack(); + } + + /** + * Getter for monitor. + * @return the MultiPhaseProgressMonitorWithETA in the lower section of this JFrame + */ + public final MultiPhaseProgressMonitorWithETA getMonitor() { + return monitor; + } + + /** + * Setter for monitor. + * @param neoMonitor the MultiPhaseProgressMonitorWithETA in the lower section of this JFrame + */ + protected final void setMonitor(final MultiPhaseProgressMonitorWithETA neoMonitor) { + this.monitor = neoMonitor; + } +} diff --git a/src/main/java/net/slightlymagic/braids/util/UtilFunctions.java b/src/main/java/net/slightlymagic/braids/util/UtilFunctions.java index 36eb330f2b3..889893a5d81 100644 --- a/src/main/java/net/slightlymagic/braids/util/UtilFunctions.java +++ b/src/main/java/net/slightlymagic/braids/util/UtilFunctions.java @@ -1,9 +1,12 @@ package net.slightlymagic.braids.util; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import javax.swing.SwingUtilities; + /** * Some general-purpose functions. */ @@ -39,9 +42,36 @@ public final class UtilFunctions { exn.setStackTrace(slice(new StackTraceElement[len], trace, 1)); throw exn; } - /** + * Invoke the given Runnable in an Event Dispatch Thread and wait for it + * to finish; but try to use SwingUtilities.invokeLater instead whenever + * feasible. + * + * Exceptions generated by SwingUtilities.invokeAndWait (if used), are + * rethrown as RuntimeExceptions. + * + * @see javax.swing.SwingUtilities#invokeLater(Runnable) + * + * @param proc the Runnable to run + */ + public static void invokeInEventDispatchThreadAndWait(final Runnable proc) { + if (SwingUtilities.isEventDispatchThread()) { + // Just run in the current thread. + proc.run(); + } + else { + try { + SwingUtilities.invokeAndWait(proc); + } catch (InterruptedException exn) { + throw new RuntimeException(exn); + } catch (InvocationTargetException exn) { + throw new RuntimeException(exn); + } + } + } + + /** * Create an array from the (rest of) an iterator's output; * this function is horribly inefficient. * diff --git a/src/test/java/forge/card/cardFactory/CardFactoryTest.java b/src/test/java/forge/card/cardFactory/CardFactoryTest.java index 33b379a8793..ebb938711ca 100644 --- a/src/test/java/forge/card/cardFactory/CardFactoryTest.java +++ b/src/test/java/forge/card/cardFactory/CardFactoryTest.java @@ -8,6 +8,7 @@ import forge.view.swing.OldGuiNewGame; import net.slightlymagic.braids.util.ClumsyRunnable; import net.slightlymagic.braids.util.testng.BraidsAssertFunctions; import org.testng.Assert; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.util.Set; @@ -24,13 +25,14 @@ import java.util.TreeSet; @Test(groups = {"UnitTest"}, timeOut = 5000) public class CardFactoryTest implements NewConstants { - private static CardFactoryInterface factory; - static { + private CardFactoryInterface factory; + + @BeforeMethod + public final void setUp() { OldGuiNewGame.loadDynamicGamedata(); factory = new LazyCardFactory(ForgeProps.getFile(CARDSFOLDER)); } - /** * Just a quick test to see if Arc-Slogger is in the database, and if it * has the correct owner. @@ -44,10 +46,8 @@ public class CardFactoryTest implements NewConstants { /** * Make sure the method throws an exception when it's supposed to. - * - * This doesn't work with LazyCardFactory, so it is too slow to enable by default. */ - @Test(enabled = false, timeOut = 5000) + @Test(enabled = true, timeOut = 5000) public final void test_getRandomCombinationWithoutRepetition_tooLarge() { BraidsAssertFunctions.assertThrowsException(IllegalArgumentException.class, new ClumsyRunnable() { @@ -72,6 +72,7 @@ public class CardFactoryTest implements NewConstants { */ @Test(enabled = false, timeOut = 5000) public final void test_getRandomCombinationWithoutRepetition_oneTenth() { + factory = new PreloadingCardFactory(ForgeProps.getFile(CARDSFOLDER)); int divisor = 10; final CardList actual = factory.getRandomCombinationWithoutRepetition(factory.size() / divisor);