Aug 2011 GUI Overhaul

this topic started in the main Forge forum under the subject Know any UI Developers? (Bounty Inside). it moved here because it was getting very technical.
High Quality Resources for Collectible Card Games
https://www.slightlymagic.net/forum/
https://www.slightlymagic.net/forum/viewtopic.php?f=52&t=5204
it's hard to use a flag to determine which class to extend (JComponent or JDialog).Rob Cashwalker wrote:How about rewrite the progress bar dialog to have a flag property for splash screen mode vs regular progress bar mode?
Index: src/main/java/net/slightlymagic/braids/util/UtilFunctions.java
===================================================================
--- src/main/java/net/slightlymagic/braids/util/UtilFunctions.java (revision 9802)
+++ src/main/java/net/slightlymagic/braids/util/UtilFunctions.java (working copy)
@@ -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.
*/
@@ -40,6 +43,30 @@
throw exn;
}
+ /**
+ * Invoke the given Runnable in an Event Dispatch Thread and wait for it
+ * to finish.
+ *
+ * Exceptions generated by SwingUtilities.invokeAndWait (if used), are
+ * rethrown as RuntimeExceptions.
+ *
+ * @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;
Index: src/main/java/forge/view/swing/SplashFrame.java
===================================================================
--- src/main/java/forge/view/swing/SplashFrame.java (revision 0)
+++ src/main/java/forge/view/swing/SplashFrame.java (revision 0)
@@ -0,0 +1,107 @@
+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.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.
+ */
+ public SplashFrame() {
+ super();
+ 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;
+ }
+}
Index: src/main/java/forge/view/FView.java
===================================================================
--- src/main/java/forge/view/FView.java (revision 9802)
+++ src/main/java/forge/view/FView.java (working copy)
@@ -1,5 +1,6 @@
package forge.view;
+import net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor;
import forge.model.FModel;
/**
@@ -15,4 +16,11 @@
*/
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();
+
}
Index: src/main/java/forge/view/swing/ApplicationView.java
===================================================================
--- src/main/java/forge/view/swing/ApplicationView.java (revision 9802)
+++ src/main/java/forge/view/swing/ApplicationView.java (working copy)
@@ -1,8 +1,12 @@
package forge.view.swing;
+import java.lang.reflect.InvocationTargetException;
+
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
+import net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor;
+
import com.esotericsoftware.minlog.Log;
import forge.AllZone;
@@ -22,14 +26,45 @@
* The main view for Forge: a java swing application.
*/
public class ApplicationView implements FView {
+
+ private SplashFrame splashFrame;
+
/**
* Constructor.
*/
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
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
+ splashFrame = new SplashFrame();
+ splashFrame.setVisible(true);
}
+ });
+ } catch (InterruptedException exn) {
+ throw new RuntimeException(exn);
+ } catch (InvocationTargetException exn) {
+ throw new RuntimeException(exn);
+ }
+ }
/* (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)
* @see forge.view.FView#setModel(forge.model.FModel)
*/
@Override
@@ -73,11 +108,23 @@
}
});
+ 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 +132,5 @@
ErrorViewer.showError(ex);
}
-
}
}
Index: src/main/java/forge/gui/MultiPhaseProgressMonitorWithETA.java
===================================================================
--- src/main/java/forge/gui/MultiPhaseProgressMonitorWithETA.java (revision 9802)
+++ src/main/java/forge/gui/MultiPhaseProgressMonitorWithETA.java (working copy)
@@ -1,9 +1,13 @@
package forge.gui;
+import java.lang.reflect.InvocationTargetException;
+
import javax.swing.JDialog;
import javax.swing.JProgressBar;
+import javax.swing.SwingUtilities;
import forge.Gui_ProgressBarWindow;
+import net.slightlymagic.braids.util.UtilFunctions;
import net.slightlymagic.braids.util.progress_monitor.BaseProgressMonitor;
/**
@@ -33,6 +37,10 @@
/**
* 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
@@ -54,6 +62,10 @@
super(numPhases, totalUnitsFirstPhase, minUIUpdateIntervalSec,
phaseWeights);
+ if (!SwingUtilities.isEventDispatchThread()) {
+ throw new IllegalStateException("must be called from within an event dispatch thread");
+ }
+
this.title = title;
}
@@ -65,10 +77,25 @@
System.out.println("Initializing...");
- MultiPhaseProgressMonitorWithETA monitor =
+ // This is a trick to get an output variable from a Runnable.
+ final MultiPhaseProgressMonitorWithETA[] monitorPointer = new MultiPhaseProgressMonitorWithETA[1];
+
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
+ monitorPointer[0] =
new MultiPhaseProgressMonitorWithETA("Testing 2 phases", 2, 5000,
1.0f, null);
+ }
+ });
+ } catch (InterruptedException exn) {
+ throw new RuntimeException(exn);
+ } catch (InvocationTargetException exn) {
+ throw new RuntimeException(exn);
+ }
+ MultiPhaseProgressMonitorWithETA monitor = monitorPointer[0];
+
System.out.println("Running...");
for (int i = 0; i <= 5000; i++) {
@@ -113,13 +140,15 @@
*
* @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);
}
+ final Runnable proc = new Runnable() {
+ public void run() {
if (numUnits > 0) {
// (Re)create the progress bar.
if (dialog != null) {
@@ -142,7 +171,11 @@
bar.setValue(0);
}
}
+ };
+ UtilFunctions.invokeInEventDispatchThreadAndWait(proc);
+ }
+
@Override
/**
* @see net.slightlymagic.braids.util.progress_monitor.ProgressMonitor#incrementUnitsCompletedThisPhase(long)
@@ -151,8 +184,12 @@
super.incrementUnitsCompletedThisPhase(numUnits);
for (int i = 0 ; i < numUnits ; i++) {
+ UtilFunctions.invokeInEventDispatchThreadAndWait(new Runnable() {
+ public void run() {
dialog.increment();
}
+ });
+ }
if (shouldUpdateUI()) {
@@ -187,8 +224,10 @@
*
* @param message the message to display
*/
- public void displayUpdate(String message) {
+ public void displayUpdate(final String message) {
+ final Runnable proc = new Runnable() {
+ public void run() {
// i've been having trouble getting the dialog to display its title.
dialog.setTitle(title);
@@ -197,12 +236,25 @@
justUpdatedUI();
}
+ };
+ if (SwingUtilities.isEventDispatchThread()) {
+ proc.run();
+ }
+ else {
+ SwingUtilities.invokeLater(proc);
+ }
+ }
+
@Override
- public void dispose() {
+ public final void dispose() {
+ UtilFunctions.invokeInEventDispatchThreadAndWait(new Runnable() {
+ public void run() {
getDialog().dispose();
}
+ });
+ }
/**
Index: src/main/java/forge/CardReader.java
===================================================================
--- src/main/java/forge/CardReader.java (revision 9802)
+++ src/main/java/forge/CardReader.java (working copy)
@@ -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,7 +16,21 @@
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;
+
+
/**
* <p>CardReader class.</p>
*
@@ -165,12 +175,19 @@
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 @@
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")) {
Index: res/images/ui/forgeSplash by moomarc.jpg
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Property changes on: res/images/ui/forgeSplash by moomarc.jpg
___________________________________________________________________
Added: svn:mime-type
+ application/octet-stream
Index: src/test/java/forge/card/cardFactory/CardFactoryTest.java
===================================================================
--- src/test/java/forge/card/cardFactory/CardFactoryTest.java (revision 9802)
+++ src/test/java/forge/card/cardFactory/CardFactoryTest.java (working copy)
@@ -8,6 +8,7 @@
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 @@
@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 @@
/**
* 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 @@
*/
@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);
SplashFrame constructor now throws IllegalStateException if it is not called from the event dispatch thread.Snacko wrote:SplashFrame constructor accesses swing methods and creates swing objects so all of this has to be moved to Swing dispatch thread. You run under the assumption that everyone will remember to run your object on sdt (objects should be self contained).
i have reduced all uses except one to invokeLater. part of the ApplicationView constructor's contract is that the splashFrame field has been initialized before the constructor exits. i placed splashFrame's construction in an invokeAndWait and splashFrame.setVisible in an invokeLater. i could have avoided this by requiring that ApplicationView be called from an event dispatch thread, but i wanted the class to be more "self contained".Snacko wrote:Also never ever use invokeAndWait as it's a bad practice to use blocking IO (you should design it to run as invokeLater).
i wasn't able to find anything in SwingWorker to help with getting output from a Runnable, so i just placed everything in MultiPhaseProgressMonitorWithETA.main inside an invokeLater. besides, that main method is just a quick method for testing. it is never called from the normal Forge application.Snacko wrote:To get output form Runnable, please use SwingWorker as it has all you need.
I'll trim the artwork if you want. Or would you prefer me to adjust the artwork to either fill the area better, or just adjust the border trim to allow for more disclaimer/info text (I seem to recall Braids saying she couldn't fit much text in there)Braids wrote:if you want to close it, that's ok. someone can move the progress bar later, or trim the bottom part out of the splash image.
i like the idea of using the bottom rectangle for more text. perhaps we can get an undecorated TextArea in there without scrollbars, etc. for multiple lines.moomarc wrote:I'll trim the artwork if you want. Or would you prefer me to adjust the artwork to either fill the area better, or just adjust the border trim to allow for more disclaimer/info text (I seem to recall Braids saying she couldn't fit much text in there)Braids wrote:if you want to close it, that's ok. someone can move the progress bar later, or trim the bottom part out of the splash image.Well done on the splash!
No problem. This way more attention was drawn to it and it was attributed to me, so more people know I did the artwork now.Braids wrote:i'm sorry the splash image didn't make it into the recent beta. i added but forgot to commit the image.at least it starts up ok without it!