diff --git a/.gitattributes b/.gitattributes
index 8082dcf94ed..b10c6fc886c 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -14260,6 +14260,7 @@ forge-gui/res/defaults/gauntlet/LOCKED_DotP[!!-~]Preconstructed.dat -text
forge-gui/res/defaults/gauntlet/LOCKED_Swimming[!!-~]With[!!-~]Sharks.dat -text
forge-gui/res/defaults/home.xml svneol=native#text/xml
forge-gui/res/defaults/match.xml svneol=native#text/xml
+forge-gui/res/defaults/no_card.jpg -text
forge-gui/res/defaults/window.xml -text
forge-gui/res/defaults/workshop.xml -text
forge-gui/res/draft/cube_juzamjedi.draft -text
@@ -15915,7 +15916,6 @@ forge-gui/src/main/java/forge/view/arcane/util/CardPanelMouseListener.java svneo
forge-gui/src/main/java/forge/view/arcane/util/OutlinedLabel.java svneol=native#text/plain
forge-gui/src/main/java/forge/view/arcane/util/package-info.java svneol=native#text/plain
forge-gui/src/main/java/forge/view/package-info.java svneol=native#text/plain
-forge-gui/src/main/resources/no_card.jpg -text
forge-gui/src/main/resources/proxy-template.ftl -text
forge-gui/src/site/apt/index.apt -text
forge-gui/src/test/java/forge/BoosterDraft1Test.java svneol=native#text/plain
@@ -16016,6 +16016,11 @@ forge-m-base/src/forge/assets/FSkinColor.java -text
forge-m-base/src/forge/assets/FSkinFont.java -text
forge-m-base/src/forge/assets/FSkinImage.java -text
forge-m-base/src/forge/assets/FSkinTexture.java -text
+forge-m-base/src/forge/assets/FTextureImage.java -text
+forge-m-base/src/forge/assets/ImageCache.java -text
+forge-m-base/src/forge/assets/ImageLoader.java -text
+forge-m-base/src/forge/error/BugReporter.java -text
+forge-m-base/src/forge/error/ExceptionHandler.java -text
forge-m-base/src/forge/model/FModel.java -text
forge-m-base/src/forge/player/LobbyPlayerHuman.java -text
forge-m-base/src/forge/player/PlayerControllerHuman.java -text
@@ -16026,7 +16031,7 @@ forge-m-base/src/forge/screens/constructed/ConstructedScreen.java -text
forge-m-base/src/forge/screens/draft/DraftScreen.java -text
forge-m-base/src/forge/screens/guantlet/GuantletScreen.java -text
forge-m-base/src/forge/screens/home/HomeScreen.java -text
-forge-m-base/src/forge/screens/match/MatchController.java -text
+forge-m-base/src/forge/screens/match/FControl.java -text
forge-m-base/src/forge/screens/match/MatchScreen.java -text
forge-m-base/src/forge/screens/match/views/VAvatar.java -text
forge-m-base/src/forge/screens/match/views/VField.java -text
@@ -16046,11 +16051,13 @@ forge-m-base/src/forge/toolbox/FDisplayObject.java -text
forge-m-base/src/forge/toolbox/FGestureAdapter.java -text
forge-m-base/src/forge/toolbox/FLabel.java -text
forge-m-base/src/forge/toolbox/FList.java -text
+forge-m-base/src/forge/toolbox/FOptionPane.java -text
forge-m-base/src/forge/toolbox/FOverlay.java -text
forge-m-base/src/forge/toolbox/FProgressBar.java -text
forge-m-base/src/forge/toolbox/FScrollPane.java -text
forge-m-base/src/forge/utils/Constants.java -text
forge-m-base/src/forge/utils/ForgePreferences.java -text
+forge-m-base/src/forge/utils/ForgeProfileProperties.java -text
forge-m-base/src/forge/utils/Preferences.java -text
forge-m-base/src/forge/utils/PreferencesStore.java -text
forge-m-base/src/forge/utils/Utils.java -text
diff --git a/forge-gui/src/main/resources/no_card.jpg b/forge-gui/res/defaults/no_card.jpg
similarity index 100%
rename from forge-gui/src/main/resources/no_card.jpg
rename to forge-gui/res/defaults/no_card.jpg
diff --git a/forge-gui/src/main/java/forge/ImageCache.java b/forge-gui/src/main/java/forge/ImageCache.java
index bd0324b55e4..21945314679 100644
--- a/forge-gui/src/main/java/forge/ImageCache.java
+++ b/forge-gui/src/main/java/forge/ImageCache.java
@@ -38,7 +38,6 @@ import org.apache.commons.lang3.StringUtils;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
-import java.io.InputStream;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutionException;
@@ -66,16 +65,9 @@ public class ImageCache {
static {
BufferedImage defImage = null;
try {
- ClassLoader cl = Thread.currentThread().getContextClassLoader();
- InputStream isNoCardJpg = cl.getResourceAsStream("no_card.jpg");
- defImage = ImageIO.read(isNoCardJpg);
- } catch (Exception e) {
- // resource not found; perhaps we're running straight from source
- try {
- defImage = ImageIO.read(new File("src/main/resources/no_card.jpg"));
- } catch (Exception ex) {
- System.err.println("could not load default card image");
- }
+ defImage = ImageIO.read(new File(NewConstants.NO_CARD_FILE));
+ } catch (Exception ex) {
+ System.err.println("could not load default card image");
} finally {
_defaultImage = (null == defImage) ? new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB) : defImage;
}
diff --git a/forge-gui/src/main/java/forge/properties/NewConstants.java b/forge-gui/src/main/java/forge/properties/NewConstants.java
index 40fa2b73f53..82e990f12ed 100644
--- a/forge-gui/src/main/java/forge/properties/NewConstants.java
+++ b/forge-gui/src/main/java/forge/properties/NewConstants.java
@@ -46,6 +46,7 @@ public final class NewConstants {
public static final String CARD_DATA_DIR = _RES_ROOT + "cardsfolder/";
public static final String DECK_CUBE_DIR = _RES_ROOT + "cube";
public static final String AI_PROFILE_DIR = _RES_ROOT + "ai";
+ public static final String NO_CARD_FILE = _RES_ROOT + "defaults/no_card.jpg";
public static final String QUEST_WORLD_DIR = _QUEST_DIR + "worlds/";
public static final String QUEST_PRECON_DIR = _QUEST_DIR + "precons/";
diff --git a/forge-m-base/src/forge/assets/FTextureImage.java b/forge-m-base/src/forge/assets/FTextureImage.java
new file mode 100644
index 00000000000..d18dd55ade2
--- /dev/null
+++ b/forge-m-base/src/forge/assets/FTextureImage.java
@@ -0,0 +1,28 @@
+package forge.assets;
+
+import com.badlogic.gdx.graphics.Texture;
+
+import forge.Forge.Graphics;
+
+public class FTextureImage implements FImage {
+ private final Texture texture;
+
+ public FTextureImage(Texture texture0) {
+ texture = texture0;
+ }
+
+ @Override
+ public float getWidth() {
+ return texture.getWidth();
+ }
+
+ @Override
+ public float getHeight() {
+ return texture.getHeight();
+ }
+
+ @Override
+ public void draw(Graphics g, float x, float y, float w, float h) {
+ g.drawImage(texture, x, y, w, h);
+ }
+}
diff --git a/forge-m-base/src/forge/assets/ImageCache.java b/forge-m-base/src/forge/assets/ImageCache.java
new file mode 100644
index 00000000000..d3bf397698e
--- /dev/null
+++ b/forge-m-base/src/forge/assets/ImageCache.java
@@ -0,0 +1,263 @@
+/*
+ * Forge: Play Magic: the Gathering.
+ * Copyright (C) 2011 Forge Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package forge.assets;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.graphics.Pixmap.Format;
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.utils.Base64Coder;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.LoadingCache;
+
+import forge.ImageKeys;
+import forge.card.CardDb;
+import forge.card.CardRules;
+import forge.card.CardSplitType;
+import forge.game.card.Card;
+import forge.game.player.IHasIcon;
+import forge.item.InventoryItem;
+import forge.item.PaperCard;
+import forge.model.FModel;
+import forge.screens.match.FControl;
+import forge.utils.Constants;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This class stores ALL card images in a cache with soft values. this means
+ * that the images may be collected when they are not needed any more, but will
+ * be kept as long as possible.
+ *
+ * The keys are the following:
+ *
+ *
Keys start with the file name, extension is skipped
+ *
The key without suffix belongs to the unmodified image from the file
+ *
+ *
+ * @author Forge
+ * @version $Id: ImageCache.java 24769 2014-02-09 13:56:04Z Hellfish $
+ */
+public class ImageCache {
+ // short prefixes to save memory
+
+ private static final Set _missingIconKeys = new HashSet();
+ private static final LoadingCache _CACHE = CacheBuilder.newBuilder().softValues().build(new ImageLoader());
+ private static final Texture _defaultImage;
+ static {
+ Texture defImage = new Texture(Gdx.files.internal(Constants.DEFAULT_DUELS_DIR));
+ try {
+ defImage = new Texture(Gdx.files.internal(Constants.DEFAULT_DUELS_DIR));
+ } catch (Exception ex) {
+ System.err.println("could not load default card image");
+ } finally {
+ _defaultImage = (null == defImage) ? new Texture(10, 10, Format.RGBA8888) : defImage;
+ }
+ }
+
+ public static void clear() {
+ _CACHE.invalidateAll();
+ _missingIconKeys.clear();
+ }
+
+ public static Texture getImage(Card card) {
+ final String key;
+ if (!FControl.mayShowCard(card) || card.isFaceDown()) {
+ key = ImageKeys.TOKEN_PREFIX + ImageKeys.MORPH_IMAGE;
+ } else {
+ key = card.getImageKey();
+ }
+ return getImage(key, true);
+ }
+
+ public static Texture getImage(InventoryItem ii) {
+ return getImage(ImageKeys.getImageKey(ii, false), true);
+ }
+
+ /**
+ * retrieve an icon from the cache. returns the current skin's ICO_UNKNOWN if the icon image is not found
+ * in the cache and cannot be loaded from disk.
+ */
+ public static FImage getIcon(IHasIcon ihi) {
+ String imageKey = ihi.getIconImageKey();
+ final Texture icon;
+ if (_missingIconKeys.contains(imageKey) ||
+ null == (icon = getImage(ihi.getIconImageKey(), false))) {
+ _missingIconKeys.add(imageKey);
+ return FSkinImage.UNKNOWN;
+ }
+ return new FTextureImage(icon);
+ }
+
+ /**
+ * This requests the original unscaled image from the cache for the given key.
+ * If the image does not exist then it can return a default image if desired.
+ *
+ * If the requested image is not present in the cache then it attempts to load
+ * the image from file (slower) and then add it to the cache for fast future access.
+ *
+ */
+ public static Texture getImage(String imageKey, boolean useDefaultIfNotFound) {
+ if (StringUtils.isEmpty(imageKey)) {
+ return null;
+ }
+
+ boolean altState = imageKey.endsWith(ImageKeys.BACKFACE_POSTFIX);
+ if (altState) {
+ imageKey = imageKey.substring(0, imageKey.length() - ImageKeys.BACKFACE_POSTFIX.length());
+ }
+ if (imageKey.startsWith(ImageKeys.CARD_PREFIX)) {
+ imageKey = getImageKey(getPaperCardFromImageKey(imageKey.substring(2)), altState, true);
+ if (StringUtils.isBlank(imageKey)) {
+ return _defaultImage;
+ }
+ }
+
+ // Load from file and add to cache if not found in cache initially.
+ Texture image = ImageCache._CACHE.getIfPresent(imageKey);
+
+ // No image file exists for the given key so optionally associate with
+ // a default "not available" image and add to cache for given key.
+ if (image == null) {
+ if (useDefaultIfNotFound) {
+ image = _defaultImage;
+ _CACHE.put(imageKey, _defaultImage);
+ }
+ else {
+ image = null;
+ }
+ }
+ return image;
+ }
+
+ private static PaperCard getPaperCardFromImageKey(String key) {
+ if (key == null) {
+ return null;
+ }
+
+ PaperCard cp = FModel.getMagicDb().getCommonCards().getCard(key);
+ if (cp == null) {
+ cp = FModel.getMagicDb().getVariantCards().getCard(key);
+ }
+ return cp;
+ }
+
+ private static String getImageRelativePath(PaperCard cp, boolean backFace, boolean includeSet, boolean isDownloadUrl) {
+ final String nameToUse = cp == null ? null : getNameToUse(cp, backFace);
+ if ( null == nameToUse )
+ return null;
+
+ StringBuilder s = new StringBuilder();
+
+ CardRules card = cp.getRules();
+ String edition = cp.getEdition();
+ s.append(ImageCache.toMWSFilename(nameToUse));
+
+ final int cntPictures;
+ final boolean hasManyPictures;
+ final CardDb db = !card.isVariant() ? FModel.getMagicDb().getCommonCards() : FModel.getMagicDb().getVariantCards();
+ if (includeSet) {
+ cntPictures = db.getPrintCount(card.getName(), edition);
+ hasManyPictures = cntPictures > 1;
+ } else {
+ // without set number of pictures equals number of urls provided in Svar:Picture
+ String urls = card.getPictureUrl(backFace);
+ cntPictures = StringUtils.countMatches(urls, "\\") + 1;
+
+ // raise the art index limit to the maximum of the sets this card was printed in
+ int maxCntPictures = db.getMaxPrintCount(card.getName());
+ hasManyPictures = maxCntPictures > 1;
+ }
+
+ int artIdx = cp.getArtIndex() - 1;
+ if (hasManyPictures) {
+ if ( cntPictures <= artIdx ) // prevent overflow
+ artIdx = cntPictures == 0 ? 0 : artIdx % cntPictures;
+ s.append(artIdx + 1);
+ }
+
+ // for whatever reason, MWS-named plane cards don't have the ".full" infix
+ if (!card.getType().isPlane() && !card.getType().isPhenomenon()) {
+ s.append(".full");
+ }
+
+ final String fname;
+ if (isDownloadUrl) {
+ s.append(".jpg");
+ fname = Base64Coder.encodeString(s.toString());
+ }
+ else {
+ fname = s.toString();
+ }
+
+ if (includeSet) {
+ String editionAliased = isDownloadUrl ? FModel.getMagicDb().getEditions().getCode2ByCode(edition) : getSetFolder(edition);
+ return String.format("%s/%s", editionAliased, fname);
+ }
+ return fname;
+ }
+
+ public static boolean hasBackFacePicture(PaperCard cp) {
+ CardSplitType cst = cp.getRules().getSplitType();
+ return cst == CardSplitType.Transform || cst == CardSplitType.Flip;
+ }
+
+ public static String getSetFolder(String edition) {
+ return !Constants.CACHE_CARD_PICS_SUBDIR.containsKey(edition)
+ ? FModel.getMagicDb().getEditions().getCode2ByCode(edition) // by default 2-letter codes from MWS are used
+ : Constants.CACHE_CARD_PICS_SUBDIR.get(edition); // may use custom paths though
+ }
+
+ private static String getNameToUse(PaperCard cp, boolean backFace) {
+ final CardRules card = cp.getRules();
+ if (backFace ) {
+ if (hasBackFacePicture(cp)) {
+ return card.getOtherPart().getName();
+ }
+ return null;
+ }
+ if (CardSplitType.Split == cp.getRules().getSplitType()) {
+ return card.getMainPart().getName() + card.getOtherPart().getName();
+ }
+ return cp.getName();
+ }
+
+ public static String getImageKey(PaperCard cp, boolean backFace, boolean includeSet) {
+ return getImageRelativePath(cp, backFace, includeSet, false);
+ }
+
+ public static String getDownloadUrl(PaperCard cp, boolean backFace) {
+ return getImageRelativePath(cp, backFace, true, true);
+ }
+
+ public static String toMWSFilename(String in) {
+ final StringBuffer out = new StringBuffer();
+ char c;
+ for (int i = 0; i < in.length(); i++) {
+ c = in.charAt(i);
+ if ((c == '"') || (c == '/') || (c == ':') || (c == '?')) {
+ out.append("");
+ } else {
+ out.append(c);
+ }
+ }
+ return out.toString();
+ }
+}
diff --git a/forge-m-base/src/forge/assets/ImageLoader.java b/forge-m-base/src/forge/assets/ImageLoader.java
new file mode 100644
index 00000000000..79e5588d02f
--- /dev/null
+++ b/forge-m-base/src/forge/assets/ImageLoader.java
@@ -0,0 +1,97 @@
+package forge.assets;
+
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.graphics.Texture;
+import com.google.common.cache.CacheLoader;
+
+import forge.ImageKeys;
+import forge.error.BugReporter;
+import forge.utils.Constants;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+
+final class ImageLoader extends CacheLoader {
+ // image file extensions for various formats in order of likelihood
+ // the last, empty, string is for keys that come in with an extension already in place
+ private static final String[] _FILE_EXTENSIONS = { ".jpg", ".png", "" };
+
+ @Override
+ public Texture load(String key) {
+ if (StringUtils.isEmpty(key)) {
+ return null;
+ }
+
+ final String path;
+ final String filename;
+ if (key.startsWith(ImageKeys.TOKEN_PREFIX)) {
+ filename = key.substring(ImageKeys.TOKEN_PREFIX.length());
+ path = Constants.CACHE_TOKEN_PICS_DIR;
+ } else if (key.startsWith(ImageKeys.ICON_PREFIX)) {
+ filename = key.substring(ImageKeys.ICON_PREFIX.length());
+ path = Constants.CACHE_ICON_PICS_DIR;
+ } else if (key.startsWith(ImageKeys.BOOSTER_PREFIX)) {
+ filename = key.substring(ImageKeys.BOOSTER_PREFIX.length());
+ path = Constants.CACHE_BOOSTER_PICS_DIR;
+ } else if (key.startsWith(ImageKeys.FATPACK_PREFIX)) {
+ filename = key.substring(ImageKeys.FATPACK_PREFIX.length());
+ path = Constants.CACHE_FATPACK_PICS_DIR;
+ } else if (key.startsWith(ImageKeys.PRECON_PREFIX)) {
+ filename = key.substring(ImageKeys.PRECON_PREFIX.length());
+ path = Constants.CACHE_PRECON_PICS_DIR;
+ } else if (key.startsWith(ImageKeys.TOURNAMENTPACK_PREFIX)) {
+ filename = key.substring(ImageKeys.TOURNAMENTPACK_PREFIX.length());
+ path = Constants.CACHE_TOURNAMENTPACK_PICS_DIR;
+ } else {
+ filename = key;
+ path = Constants.CACHE_CARD_PICS_DIR;
+ }
+
+ Texture ret = _findFile(key, path, filename);
+
+ // some S00 cards are really part of 6ED
+ if (null == ret ) {
+ String s2kAlias = ImageCache.getSetFolder("S00");
+ if ( filename.startsWith(s2kAlias) ) {
+ ret = _findFile(key, path, filename.replace(s2kAlias, ImageCache.getSetFolder("6ED")));
+ }
+ }
+
+ // try without set prefix
+ String setlessFilename = null;
+ if (null == ret && filename.contains("/")) {
+ setlessFilename = filename.substring(filename.indexOf('/') + 1);
+ ret = _findFile(key, path, setlessFilename);
+
+ // try lowering the art index to the minimum for regular cards
+ if (null == ret && setlessFilename.contains(".full")) {
+ ret = _findFile(key, path, setlessFilename.replaceAll("[0-9]*[.]full", "1.full"));
+ }
+ }
+
+ if (null == ret) {
+ System.out.println("File not found, no image created: " + key);
+ }
+
+ return ret;
+ }
+
+ private static Texture _findFile(String key, String path, String filename) {
+ for (String ext : _FILE_EXTENSIONS) {
+ File file = new File(path, filename + ext);
+ //System.out.println(String.format("Searching for %s at: %s", key, file.getAbsolutePath()));
+ if (file.exists()) {
+ //System.out.println(String.format("Found %s at: %s", key, file.getAbsolutePath()));
+ try {
+ return new Texture(new FileHandle(file));
+ } catch (Exception ex) {
+ BugReporter.reportException(ex, "Could not read image file " + file.getAbsolutePath() + " ");
+ break;
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/forge-m-base/src/forge/error/BugReporter.java b/forge-m-base/src/forge/error/BugReporter.java
new file mode 100644
index 00000000000..54f0a5b42b1
--- /dev/null
+++ b/forge-m-base/src/forge/error/BugReporter.java
@@ -0,0 +1,310 @@
+/*
+ * Forge: Play Magic: the Gathering.
+ * Copyright (C) 2011 Forge Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package forge.error;
+
+import forge.toolbox.FOptionPane;
+import forge.util.BuildInfo;
+
+import javax.swing.*;
+
+import java.awt.*;
+import java.awt.datatransfer.StringSelection;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.io.*;
+import java.net.URI;
+import java.util.Map;
+import java.util.Map.Entry;
+
+public class BugReporter {
+ private static final int _STACK_OVERFLOW_MAX_MESSAGE_LEN = 16 * 1024;
+
+ private static boolean dialogShown = false;
+
+ /**
+ * Shows exception information in a format ready to post to the forum as a crash report. Uses the exception's message
+ * as the reason if message is null.
+ */
+ public static void reportException(final Throwable ex, final String message) {
+ if (ex == null) {
+ return;
+ }
+ String threadId = ""; //FThreads.debugGetCurrThreadId()
+ if (message != null) {
+ System.err.printf("%s > %s%n", threadId, message);
+ }
+ System.err.print(threadId + " > " );
+ ex.printStackTrace();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("Description: [describe what you were doing when the crash occurred]\n\n");
+ _buildSpoilerHeader(sb, ex.getClass().getSimpleName());
+ sb.append("\n\n");
+ if (null != message && !message.isEmpty()) {
+ sb.append(threadId).append(" > ").append(message).append("\n");
+ }
+
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ ex.printStackTrace(pw);
+
+ String swStr = sw.toString();
+ if (ex instanceof StackOverflowError &&
+ _STACK_OVERFLOW_MAX_MESSAGE_LEN <= swStr.length()) {
+ // most likely a cycle. only take first portion so the message
+ // doesn't grow too large to post
+ sb.append(swStr, 0, _STACK_OVERFLOW_MAX_MESSAGE_LEN);
+ sb.append("\n... (truncated)");
+ } else {
+ sb.append(swStr);
+ }
+
+ _buildSpoilerFooter(sb);
+
+ _showDialog("Report a crash", sb.toString(), true);
+ }
+
+ /**
+ * Alias for reportException(ex, null).
+ */
+ public static void reportException(final Throwable ex) {
+ reportException(ex, null);
+ }
+
+ /**
+ * Alias for reportException(ex, String.format(format, args)).
+ */
+ public static void reportException(final Throwable ex, final String format, final Object... args) {
+ reportException(ex, String.format(format, args));
+ }
+
+ /**
+ * Shows a forum post template for reporting a bug.
+ */
+ public static void reportBug(String details) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Description: [describe the problem]\n\n");
+ _buildSpoilerHeader(sb, "General bug report");
+ if (null != details && !details.isEmpty()) {
+ sb.append("\n\n");
+ sb.append(details);
+ }
+ _buildSpoilerFooter(sb);
+
+ _showDialog("Report a bug", sb.toString(), false);
+ }
+
+ /**
+ * Shows thread stack information in a format ready to post to the forum.
+ */
+ public static void reportThreadStacks(final String message) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Description: [describe what you were doing at the time]\n\n");
+ _buildSpoilerHeader(sb, "Thread stack dump");
+ sb.append("\n\n");
+ if (null != message && !message.isEmpty()) {
+ sb.append(message);
+ sb.append("\n");
+ }
+
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ final Map traces = Thread.getAllStackTraces();
+ for (final Entry e : traces.entrySet()) {
+ pw.println();
+ pw.printf("%s (%s):%n", e.getKey().getName(), e.getKey().getId());
+ for (final StackTraceElement el : e.getValue()) {
+ pw.println(el);
+ }
+ }
+
+ sb.append(sw.toString());
+ _buildSpoilerFooter(sb);
+ _showDialog("Thread stack dump", sb.toString(), false);
+ }
+
+ /**
+ * Alias for reportThreadStacks(String.format(format, args))
+ */
+ public static void reportThreadStacks(final String format, final Object... args) {
+ reportThreadStacks(String.format(format, args));
+ }
+
+ private static StringBuilder _buildSpoilerHeader(StringBuilder sb, String reportTitle) {
+ sb.append("[spoiler=").append(reportTitle).append("][code]");
+ sb.append("\nForge Version: ").append(BuildInfo.getVersionString());
+ sb.append("\nOperating System: ").append(System.getProperty("os.name"))
+ .append(" ").append(System.getProperty("os.version"))
+ .append(" ").append(System.getProperty("os.arch"));
+ sb.append("\nJava Version: ").append(System.getProperty("java.version"))
+ .append(" ").append(System.getProperty("java.vendor"));
+ return sb;
+ }
+
+ private static StringBuilder _buildSpoilerFooter(StringBuilder sb) {
+ sb.append("[/code][/spoiler]");
+ return sb;
+ }
+
+ private static void _showDialog(String title, String text, boolean showExitAppBtn) {
+ if ( dialogShown )
+ return;
+
+ /*JTextArea area = new JTextArea(text);
+ area.setFont(new Font("Monospaced", Font.PLAIN, 10));
+ area.setEditable(false);
+ area.setLineWrap(true);
+ area.setWrapStyleWord(true);
+
+ String helpText = "A template for a post in the bug reports forum topic is shown below. Just select 'Copy and go to forum' "
+ + "and the template will be copied to your system clipboard and the forum page will open in your browser. "
+ + "Then all you have to do is paste the text into a forum post and edit the description line.";
+ String helpUrlLabel = "Reporting bugs in Forge is very important. We sincerely thank you for your time."
+ + " For help writing a solid bug report, please see:";
+ String helpUrl = "http://www.slightlymagic.net/forum/viewtopic.php?f=26&p=109925#p109925";
+ JPanel helpPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 4, 2));
+ for (String word : helpUrlLabel.split(" ")) {
+ helpPanel.add(new FLabel.Builder().text("" + word + "").useSkinColors(false).build());
+ }
+ helpPanel.add(new FHyperlink.Builder().url(helpUrl).text("this post").useSkinColors(false).build());
+
+ JPanel p = new JPanel(new MigLayout("wrap"));
+ p.add(new FLabel.Builder().text(helpText).useSkinColors(false).build(), "gap 5");
+ p.add(helpPanel, "w 600");
+ p.add(new JScrollPane(area), "w 100%, h 100%, gaptop 5");
+
+ // determine proper forum URL
+ String forgeVersion = BuildInfo.getVersionString();
+ final String url;
+ if (StringUtils.containsIgnoreCase(forgeVersion, "svn")
+ || StringUtils.containsIgnoreCase(forgeVersion, "snapshot")) {
+ url = "http://www.slightlymagic.net/forum/viewtopic.php?f=52&t=6333&start=54564487645#bottom";
+ } else {
+ url = "http://www.slightlymagic.net/forum/viewforum.php?f=26";
+ }
+
+ // Button is not modified, String gets the automatic listener to hide
+ // the dialog
+ ArrayList