Refactor out swing-related code from forge-gui into new forge-gui-desktop module

This commit is contained in:
drdev
2014-04-08 23:01:39 +00:00
parent 18dd421935
commit 9439bbbc57
535 changed files with 31219 additions and 19307 deletions

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

View File

@@ -0,0 +1,26 @@
SET app_name create-dmg
VERSION create-dmg.cur create-dmg heads/master
NEWDIR build.dir temp %-build -
NEWFILE create-dmg.zip featured %.zip %
COPYTO [build.dir]
INTO create-dmg [create-dmg.cur]/create-dmg
INTO sample [create-dmg.cur]/sample
INTO support [create-dmg.cur]/support
SUBSTVARS [build.dir<alter>]/create-dmg [[]]
ZIP [create-dmg.zip]
INTO [build-files-prefix] [build.dir]
PUT megabox-builds create-dmg.zip
PUT megabox-builds build.log
PUT s3-builds create-dmg.zip
PUT s3-builds build.log

View File

@@ -0,0 +1,210 @@
#! /bin/bash
# Create a read-only disk image of the contents of a folder
set -e;
function pure_version() {
echo '1.0.0.2'
}
function version() {
echo "create-dmg $(pure_version)"
}
function usage() {
version
echo "Creates a fancy DMG file."
echo "Usage: $(basename $0) options... image.dmg source_folder"
echo "All contents of source_folder will be copied into the disk image."
echo "Options:"
echo " --volname name"
echo " set volume name (displayed in the Finder sidebar and window title)"
echo " --volicon icon.icns"
echo " set volume icon"
echo " --background pic.png"
echo " set folder background image (provide png, gif, jpg)"
echo " --window-pos x y"
echo " set position the folder window"
echo " --window-size width height"
echo " set size of the folder window"
echo " --icon-size icon_size"
echo " set window icons size (up to 128)"
echo " --icon file_name x y"
echo " set position of the file's icon"
echo " --hide-extension file_name"
echo " hide the extension of file"
echo " --custom-icon file_name custom_icon_or_sample_file x y"
echo " set position and custom icon"
echo " --app-drop-link x y"
echo " make a drop link to Applications, at location x,y"
echo " --eula eula_file"
echo " attach a license file to the dmg"
echo " --version show tool version number"
echo " -h, --help display this help"
exit 0
}
WINX=10
WINY=60
WINW=500
WINH=350
ICON_SIZE=128
while test "${1:0:1}" = "-"; do
case $1 in
--volname)
VOLUME_NAME="$2"
shift; shift;;
--volicon)
VOLUME_ICON_FILE="$2"
shift; shift;;
--background)
BACKGROUND_FILE="$2"
BACKGROUND_FILE_NAME="$(basename $BACKGROUND_FILE)"
BACKGROUND_CLAUSE="set background picture of opts to file \".background:$BACKGROUND_FILE_NAME\""
shift; shift;;
--icon-size)
ICON_SIZE="$2"
shift; shift;;
--window-pos)
WINX=$2; WINY=$3
shift; shift; shift;;
--window-size)
WINW=$2; WINH=$3
shift; shift; shift;;
--icon)
POSITION_CLAUSE="${POSITION_CLAUSE}set position of item \"$2\" to {$3, $4}
"
shift; shift; shift; shift;;
--hide-extension)
HIDING_CLAUSE="${HIDING_CLAUSE}set the extension hidden of item \"$2\" to true"
shift; shift;;
--custom-icon)
shift; shift; shift; shift; shift;;
-h | --help)
usage;;
--version)
version; exit 0;;
--pure-version)
pure_version; exit 0;;
--app-drop-link)
APPLICATION_LINK=$2
APPLICATION_CLAUSE="set position of item \"Applications\" to {$2, $3}
"
shift; shift; shift;;
--eula)
EULA_RSRC=$2
shift; shift;;
-*)
echo "Unknown option $1. Run with --help for help."
exit 1;;
esac
done
test -z "$2" && {
echo "Not enough arguments. Invoke with --help for help."
exit 1
}
DMG_PATH="$1"
DMG_DIRNAME="$(dirname "$DMG_PATH")"
DMG_DIR="$(cd $DMG_DIRNAME > /dev/null; pwd)"
DMG_NAME="$(basename "$DMG_PATH")"
DMG_TEMP_NAME="$DMG_DIR/rw.${DMG_NAME}"
SRC_FOLDER="$(cd "$2" > /dev/null; pwd)"
test -z "$VOLUME_NAME" && VOLUME_NAME="$(basename "$DMG_PATH" .dmg)"
AUX_PATH="$(dirname $0)/support"
test -d "$AUX_PATH" || {
echo "Cannot find support directory: $AUX_PATH"
exit 1
}
if [ -f "$SRC_FOLDER/.DS_Store" ]; then
echo "Deleting any .DS_Store in source folder"
rm "$SRC_FOLDER/.DS_Store"
fi
# Create the image
echo "Creating disk image..."
test -f "${DMG_TEMP_NAME}" && rm -f "${DMG_TEMP_NAME}"
ACTUAL_SIZE=`du -sm "$SRC_FOLDER" | sed -e 's/ .*//g'`
DISK_IMAGE_SIZE=$(expr $ACTUAL_SIZE + 20)
hdiutil create -srcfolder "$SRC_FOLDER" -volname "${VOLUME_NAME}" -fs HFS+ -fsargs "-c c=64,a=16,e=16" -format UDRW -size ${DISK_IMAGE_SIZE}m "${DMG_TEMP_NAME}"
# mount it
echo "Mounting disk image..."
MOUNT_DIR="/Volumes/${VOLUME_NAME}"
# try unmount dmg if it was mounted previously (e.g. developer mounted dmg, installed app and forgot to unmount it)
echo "Unmounting disk image..."
DEV_NAME=$(hdiutil info | egrep '^/dev/' | sed 1q | awk '{print $1}')
test -d "${MOUNT_DIR}" && hdiutil detach "${DEV_NAME}"
echo "Mount directory: $MOUNT_DIR"
DEV_NAME=$(hdiutil attach -readwrite -noverify -noautoopen "${DMG_TEMP_NAME}" | egrep '^/dev/' | sed 1q | awk '{print $1}')
echo "Device name: $DEV_NAME"
if ! test -z "$BACKGROUND_FILE"; then
echo "Copying background file..."
test -d "$MOUNT_DIR/.background" || mkdir "$MOUNT_DIR/.background"
cp "$BACKGROUND_FILE" "$MOUNT_DIR/.background/$BACKGROUND_FILE_NAME"
fi
if ! test -z "$APPLICATION_LINK"; then
echo "making link to Applications dir"
echo $MOUNT_DIR
ln -s /Applications "$MOUNT_DIR/Applications"
fi
if ! test -z "$VOLUME_ICON_FILE"; then
echo "Copying volume icon file '$VOLUME_ICON_FILE'..."
cp "$VOLUME_ICON_FILE" "$MOUNT_DIR/.VolumeIcon.icns"
SetFile -c icnC "$MOUNT_DIR/.VolumeIcon.icns"
fi
# run applescript
APPLESCRIPT=$(mktemp -t createdmg)
cat "$AUX_PATH/template.applescript" | sed -e "s/WINX/$WINX/g" -e "s/WINY/$WINY/g" -e "s/WINW/$WINW/g" -e "s/WINH/$WINH/g" -e "s/BACKGROUND_CLAUSE/$BACKGROUND_CLAUSE/g" -e "s/ICON_SIZE/$ICON_SIZE/g" | perl -pe "s/POSITION_CLAUSE/$POSITION_CLAUSE/g" | perl -pe "s/APPLICATION_CLAUSE/$APPLICATION_CLAUSE/g" | perl -pe "s/HIDING_CLAUSE/$HIDING_CLAUSE/" >"$APPLESCRIPT"
echo "Running Applescript: /usr/bin/osascript \"${APPLESCRIPT}\" \"${VOLUME_NAME}\""
"/usr/bin/osascript" "${APPLESCRIPT}" "${VOLUME_NAME}" || true
echo "Done running the applescript..."
sleep 4
rm "$APPLESCRIPT"
# make sure it's not world writeable
echo "Fixing permissions..."
chmod -Rf go-w "${MOUNT_DIR}" &> /dev/null || true
echo "Done fixing permissions."
# make the top window open itself on mount:
echo "Blessing started"
bless --folder "${MOUNT_DIR}" --openfolder "${MOUNT_DIR}"
echo "Blessing finished"
if ! test -z "$VOLUME_ICON_FILE"; then
# tell the volume that it has a special file attribute
SetFile -a C "$MOUNT_DIR"
fi
# unmount
echo "Unmounting disk image..."
hdiutil detach "${DEV_NAME}"
# compress image
echo "Compressing disk image..."
hdiutil convert "${DMG_TEMP_NAME}" -format UDZO -imagekey zlib-level=9 -o "${DMG_DIR}/${DMG_NAME}"
rm -f "${DMG_TEMP_NAME}"
# adding EULA resources
if [ ! -z "${EULA_RSRC}" -a "${EULA_RSRC}" != "-null-" ]; then
echo "adding EULA resources"
"${AUX_PATH}/dmg-license.py" "${DMG_DIR}/${DMG_NAME}" "${EULA_RSRC}"
fi
echo "Disk image done"
exit 0

View File

@@ -0,0 +1,3 @@
#!/bin/sh
cd "`dirname \"$0\"`"
java -Xmx1024m -jar $project.build.finalName$

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

View File

@@ -0,0 +1,3 @@
#!/bin/sh
cd "`dirname \"$0\"`"
java -Xmx1024m -jar $project.build.finalName$

View File

@@ -0,0 +1,215 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
<!--
Checkstyle configuration that checks the sun coding conventions from:
- the Java Language Specification at
http://java.sun.com/docs/books/jls/second_edition/html/index.html
- the Sun Code Conventions at http://java.sun.com/docs/codeconv/
- the Javadoc guidelines at
http://java.sun.com/j2se/javadoc/writingdoccomments/index.html
- the JDK Api documentation http://java.sun.com/j2se/docs/api/index.html
- some best practices
Checkstyle is very configurable. Be sure to read the documentation at
http://checkstyle.sf.net (or in your downloaded distribution).
Most Checks are configurable, be sure to consult the documentation.
To completely disable a check, just comment it out or delete it from the file.
Finally, it is worth reading the documentation.
-->
<module name="Checker">
<!--
If you set the basedir property below, then all reported file
names will be relative to the specified directory. See
http://checkstyle.sourceforge.net/5.x/config.html#Checker
<property name="basedir" value="${basedir}"/>
-->
<!-- Checks that a package-info.java file exists for each package. -->
<!-- See http://checkstyle.sf.net/config_javadoc.html#JavadocPackage -->
<!-- <module name="JavadocPackage"/> -->
<!-- Checks whether files end with a new line. -->
<!-- See http://checkstyle.sf.net/config_misc.html#NewlineAtEndOfFile -->
<module name="NewlineAtEndOfFile"><property name="lineSeparator" value="lf"/></module>
<!-- Checks that property files contain the same keys. -->
<!-- See http://checkstyle.sf.net/config_misc.html#Translation -->
<module name="Translation"/>
<!-- Checks for Size Violations. -->
<!-- See http://checkstyle.sf.net/config_sizes.html -->
<!-- <module name="FileLength"/> -->
<!-- Checks for whitespace -->
<!-- See http://checkstyle.sf.net/config_whitespace.html -->
<module name="FileTabCharacter"/>
<!-- Miscellaneous other checks. -->
<!-- See http://checkstyle.sf.net/config_misc.html -->
<module name="RegexpSingleline">
<property name="format" value="(^|[^\*])\s+$"/>
<property name="minimum" value="0"/>
<property name="maximum" value="0"/>
<property name="message" value="Line has trailing spaces."/>
</module>
<module name="TreeWalker">
<property name="cacheFile" value="${cacheFile}"/>
<!-- Checks for Javadoc comments. -->
<!-- See http://checkstyle.sf.net/config_javadoc.html -->
<module name="JavadocMethod">
<property name="scope" value="package"/>
</module>
<module name="JavadocType">
<property name="scope" value="package"/>
</module>
<module name="JavadocVariable">
<property name="scope" value="package"/>
</module>
<module name="JavadocStyle"/>
<!-- Checks for Naming Conventions. -->
<!-- See http://checkstyle.sf.net/config_naming.html -->
<module name="ConstantName"/>
<module name="LocalFinalVariableName"/>
<module name="LocalVariableName"/>
<module name="MemberName"/>
<!-- Allow test_ methods to have underscores in them. -->
<module name="MethodName">
<property name="format" value="^[a-z][a-zA-Z0-9]*$|^test_[_a-zA-Z0-9]*$"/>
</module>
<module name="PackageName"/>
<module name="ParameterName"/>
<module name="StaticVariableName"/>
<module name="TypeName"/>
<!-- Checks for Headers -->
<!-- See http://checkstyle.sf.net/config_header.html -->
<!-- <module name="Header"> -->
<!-- The follow property value demonstrates the ability -->
<!-- to have access to ANT properties. In this case it uses -->
<!-- the ${basedir} property to allow Checkstyle to be run -->
<!-- from any directory within a project. See property -->
<!-- expansion, -->
<!-- http://checkstyle.sf.net/config.html#properties -->
<!-- <property -->
<!-- name="headerFile" -->
<!-- value="${basedir}/java.header"/> -->
<!-- </module> -->
<!-- Following interprets the header file as regular expressions. -->
<!-- <module name="RegexpHeader"/> -->
<!-- Checks for imports -->
<!-- See http://checkstyle.sf.net/config_import.html -->
<module name="AvoidStarImport"/>
<module name="IllegalImport"/>
<!-- defaults to sun.* packages -->
<module name="RedundantImport"/>
<module name="UnusedImports"/>
<!-- Checks for Size Violations. -->
<!-- See http://checkstyle.sf.net/config_sizes.html -->
<!-- <module name="LineLength">
<property name="max" value="180"/>
</module>
<module name="MethodLength"/>
<module name="ParameterNumber"/> -->
<!-- Checks for whitespace -->
<!-- See http://checkstyle.sf.net/config_whitespace.html -->
<module name="EmptyForIteratorPad"/>
<module name="GenericWhitespace"/>
<module name="MethodParamPad"/>
<module name="NoWhitespaceAfter">
<property name="tokens" value="BNOT, DEC, DOT, INC, LNOT, UNARY_MINUS, UNARY_PLUS"/>
</module>
<module name="NoWhitespaceBefore"/>
<module name="OperatorWrap"/>
<module name="ParenPad"/>
<module name="TypecastParenPad"/>
<module name="WhitespaceAfter"/>
<module name="WhitespaceAround"/>
<!-- Modifier Checks -->
<!-- See http://checkstyle.sf.net/config_modifiers.html -->
<module name="ModifierOrder"/>
<module name="RedundantModifier"/>
<!-- Checks for blocks. You know, those {}'s -->
<!-- See http://checkstyle.sf.net/config_blocks.html -->
<module name="AvoidNestedBlocks"/>
<!-- Allow empty blocks. <module name="EmptyBlock"/> -->
<module name="LeftCurly">
<!-- Place left curly at EOL for one-line clauses,
and on next line for multi-line clauses.
-->
<!-- <property name="option" value="nlow" /> -->
</module>
<module name="NeedBraces"/>
<!-- Allow for right curly to appear on same or next line. <module name="RightCurly"/> -->
<!-- Checks for common coding problems -->
<!-- See http://checkstyle.sf.net/config_coding.html -->
<!-- <module name="AvoidInlineConditionals"/> -->
<module name="DoubleCheckedLocking"/>
<!-- MY FAVOURITE -->
<module name="EmptyStatement"/>
<module name="EqualsHashCode"/>
<!-- <module name="HiddenField"/> -->
<module name="IllegalInstantiation"/>
<module name="InnerAssignment"/>
<!-- <module name="MagicNumber"/> -->
<module name="MissingSwitchDefault"/>
<module name="RedundantThrows"/>
<module name="SimplifyBooleanExpression"/>
<module name="SimplifyBooleanReturn"/>
<!-- Checks for class design -->
<!-- See http://checkstyle.sf.net/config_design.html -->
<!-- <module name="DesignForExtension"/> -->
<module name="FinalClass"/>
<!-- <module name="HideUtilityClassConstructor"/> -->
<module name="InterfaceIsType"/>
<module name="VisibilityModifier"/>
<!-- Miscellaneous other checks. -->
<!-- See http://checkstyle.sf.net/config_misc.html -->
<module name="ArrayTypeStyle"/>
<!-- <module name="FinalParameters"/> -->
<!-- <module name="TodoComment"/> -->
<module name="UpperEll"/>
</module>
</module>

View File

@@ -0,0 +1,141 @@
#! /usr/bin/env python
"""
This script adds a license file to a DMG. Requires Xcode and a plain ascii text
license file.
Obviously only runs on a Mac.
Copyright (C) 2011 Jared Hobbs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import os
import sys
import tempfile
import optparse
class Path(str):
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
os.unlink(self)
def mktemp(dir=None, suffix=''):
(fd, filename) = tempfile.mkstemp(dir=dir, suffix=suffix)
os.close(fd)
return Path(filename)
def main(options, args):
dmgFile, license = args
with mktemp('.') as tmpFile:
with open(tmpFile, 'w') as f:
f.write("""data 'LPic' (5000) {
$"0002 0011 0003 0001 0000 0000 0002 0000"
$"0000 000E 0006 0001 0005 0007 0000 0007"
$"0008 0000 0047 0009 0000 0034 000A 0001"
$"0035 000B 0001 0020 000C 0000 0011 000D"
$"0000 005B 0004 0000 0033 000F 0001 000C"
$"0010 0000 000B 000E 0000"
};\n\n""")
with open(license, 'r') as l:
f.write('data \'TEXT\' (5002, "English") {\n')
for line in l:
if len(line) < 1000:
f.write(' "' + line.strip().replace('"', '\\"') +
'\\n"\n')
else:
for liner in line.split('.'):
f.write(' "' +
liner.strip().replace('"', '\\"') +
'. \\n"\n')
f.write('};\n\n')
f.write("""resource 'STR#' (5002, "English") {
{
"English",
"Agree",
"Disagree",
"Print",
"Save...",
"IMPORTANT - By clicking on the \\"Agree\\" button, you agree "
"to be bound by the terms of the License Agreement.",
"Software License Agreement",
"This text cannot be saved. This disk may be full or locked, or the "
"file may be locked.",
"Unable to print. Make sure you have selected a printer."
}
};""")
os.system('/usr/bin/hdiutil unflatten -quiet "%s"' % dmgFile)
os.system('%s "%s/"*.r %s -a -o "%s"' %
(options.rez, options.flat_carbon, tmpFile, dmgFile))
os.system('/usr/bin/hdiutil flatten -quiet "%s"' % dmgFile)
if options.compression is not None:
os.system('cp %s %s.temp.dmg' % (dmgFile, dmgFile))
os.remove(dmgFile)
if options.compression == "bz2":
os.system('hdiutil convert %s.temp.dmg -format UDBZ -o %s' %
(dmgFile, dmgFile))
elif options.compression == "gz":
os.system('hdiutil convert %s.temp.dmg -format ' % dmgFile +
'UDZO -imagekey zlib-devel=9 -o %s' % dmgFile)
os.remove('%s.temp.dmg' % dmgFile)
print "Successfully added license to '%s'" % dmgFile
if __name__ == '__main__':
parser = optparse.OptionParser()
parser.set_usage("""%prog <dmgFile> <licenseFile> [OPTIONS]
This program adds a software license agreement to a DMG file.
It requires Xcode and a plain ascii text <licenseFile>.
See --help for more details.""")
parser.add_option(
'--rez',
'-r',
action='store',
default='/Applications/Xcode.app/Contents/Developer/Tools/Rez',
help='The path to the Rez tool. Defaults to %default'
)
parser.add_option(
'--flat-carbon',
'-f',
action='store',
default='/Applications/Xcode.app/Contents/Developer/Platforms'
'/MacOSX.platform/Developer/SDKs/MacOSX10.7.sdk'
'/Developer/Headers/FlatCarbon',
help='The path to the FlatCarbon headers. Defaults to %default'
)
parser.add_option(
'--compression',
'-c',
action='store',
choices=['bz2', 'gz'],
default=None,
help='Optionally compress dmg using specified compression type. '
'Choices are bz2 and gz.'
)
options, args = parser.parse_args()
cond = len(args) != 2 or not os.path.exists(options.rez) \
or not os.path.exists(options.flat_carbon)
if cond:
parser.print_usage()
sys.exit(1)
main(options, args)

View File

@@ -0,0 +1,77 @@
on run (volumeName)
tell application "Finder"
tell disk (volumeName as string)
open
set theXOrigin to WINX
set theYOrigin to WINY
set theWidth to WINW
set theHeight to WINH
set theBottomRightX to (theXOrigin + theWidth)
set theBottomRightY to (theYOrigin + theHeight)
set dsStore to "\"" & "/Volumes/" & volumeName & "/" & ".DS_STORE\""
tell container window
set current view to icon view
set toolbar visible to false
set statusbar visible to false
set the bounds to {theXOrigin, theYOrigin, theBottomRightX, theBottomRightY}
set statusbar visible to false
end tell
set opts to the icon view options of container window
tell opts
set icon size to ICON_SIZE
set arrangement to not arranged
end tell
BACKGROUND_CLAUSE
-- Positioning
POSITION_CLAUSE
-- Hiding
HIDING_CLAUSE
-- Application Link Clause
APPLICATION_CLAUSE
close
open
update without registering applications
-- Force saving of the size
delay 1
tell container window
set statusbar visible to false
set the bounds to {theXOrigin, theYOrigin, theBottomRightX - 10, theBottomRightY - 10}
end tell
update without registering applications
end tell
delay 1
tell disk (volumeName as string)
tell container window
set statusbar visible to false
set the bounds to {theXOrigin, theYOrigin, theBottomRightX, theBottomRightY}
end tell
update without registering applications
end tell
--give the finder some time to write the .DS_Store file
delay 3
set waitTime to 0
set ejectMe to false
repeat while ejectMe is false
delay 1
set waitTime to waitTime + 1
if (do shell script "[ -f " & dsStore & " ]; echo $?") = "0" then set ejectMe to true
end repeat
log "waited " & waitTime & " seconds for .DS_STORE to be created."
end tell
end run

View File

@@ -0,0 +1,100 @@
<html>
<head>
<title>Tail-based by Web Sockets</title>
<link href="css/core.css" media="all" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="js/jquery/jquery-1.9.1.min.js"></script>
<script type="text/javascript" src="js/observable.js"></script>
<script type="text/javascript" src="js/socket.js"></script>
<script type='text/javascript' src="js/main.js"></script>
</head>
<body>
<div class="wrap">
<div class="title">
<span>Your server: <b>ws://</b></span>
<input id="ws_uri" type="text" value="localhost:81/" name="uri"/>
<button id="connect" name="connect" class="cn">Connect</button>
<button id="disconn" name="disconn" class="dc" disabled="disabled">Disconnect</button>
</div>
<ul class="messages" id='messages'></ul>
<div class="packets" id='input'>
<span>Raw text to send:</span>
<input type="text" name="packet" value="" />
<button id="send" class="send">Send</button>
</div>
</div>
<script type="text/javascript">
if (!window.WebSocket)
alert("WebSocket not supported by this browser");
var server = Socket();
var listener = {
onOpen : function() {
$('#input').slideDown();
},
onMessage : function(m) {
if (m.data) {
addLi("incoming", m.data);
}
},
onClose : function(m) {
addLi("error", "Connection was closed (" + m.code + "): " + m.reason);
onDisconnectClicked();
$('#input').fadeOut();
}
};
server.addObserver(listener);
function addLi(className, text) {
var spanText = document.createElement('li');
spanText.className = className;
spanText.innerHTML = text;
var messageBox = $('#messages')[0];
messageBox.appendChild(spanText);
messageBox.scrollTop = messageBox.scrollHeight - messageBox.clientHeight;
}
function onConnectClicked() {
var uri = $("#ws_uri").val()
addLi("connecting", "Connecting to ws://" + uri + " ..." )
server.connect(uri);
$('#connect').attr("disabled", "disabled")
$('#disconn').removeAttr("disabled")
}
function onDisconnectClicked() {
server.close();
$('#disconn').attr("disabled", "disabled")
$('#connect').removeAttr("disabled")
}
function onSendClicked() {
var toSend = $("#input input").val();
$("#input input").val("");
addLi("outcoming", toSend);
server.send(toSend)
}
function onInputKey(event) {
if( event.keyCode == 13 )
onSendClicked();
}
function onReady() {
$('#connect').on("click", onConnectClicked);
$('#disconn').on("click", onDisconnectClicked);
$('#send').on("click", onSendClicked);
$("#input input").on("keypress", onInputKey);
}
$(onReady)
</script>
</body>
</html>

View File

@@ -0,0 +1,23 @@
ul, ol { margin: 0; }
div { border: 0px solid black; }
div.wrap { width: 640px; margin: 100px auto;}
.title { height: 24px; background-color: #ddd; padding: 4px; border: 1px solid black; border-bottom: 0px }
.title input {width: 300px; }
.title span { display: inline-block; width: 120px; text-align: right; }
.messages { height: 30ex; overflow: auto; background-color: #fff; padding: 4px; border: 1px solid black; list-style: none; }
.messages .incoming { color: #006; }
.messages .incoming:before { content: "<< "}
.messages .outcoming { color: #060; }
.messages .outcoming:before { content: ">> "}
.messages .error { color: #600; }
.messages li:nth-child(2n) { background-color: #f7f7f7; }
.packets { padding: 4px; background-color: #ddd; border: 1px solid black; border-top: 0px; display: none; }
.packets span { display: inline-block; width: 120px; text-align: right; }
.packets input {width: 400px; }
.packets button { width: 100px; }
span.alert { font-style: italic; }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,23 @@
var Observable = function(eventNames) {
var _t = {};
var observers = {};
_t.addObserver = function(obj) {
for(var i = 0; i < eventNames.length; i++) {
var evName = eventNames[i]
var method = obj["on" + evName];
if( typeof(method) === 'function') {
var handlers = observers[evName]
if( 'undefined' === typeof(handler))
handlers = observers[evName] = [];
handlers.push(obj);
}
}
}
_t.fireEvent = function() { // usually invoked as .apply(EventName, args)
var q = observers[this]
if ( q ) for( var i = 0; i < q.length; i++ ) q[i]['on'+ this].apply(q[i], arguments);
}
return _t;
}

View File

@@ -0,0 +1,27 @@
// There should be some kind of fallback to Flash-powered sockets (IE 9-, Opera with sockets switched off)
var Socket = function() {
var _t = Observable(["Open", "Message", "Close", "Error"]);
function onOpen() { _t.fireEvent.apply("Open", arguments); }
function onClose() { _t.fireEvent.apply("Close", arguments); }
function onError() { _t.fireEvent.apply("Error", arguments); }
function onMessage() { _t.fireEvent.apply("Message", arguments); }
var ws;
_t.connect = function(location) {
ws = new WebSocket("ws://" + location);
ws.onopen = onOpen;
ws.onmessage = onMessage;
ws.onclose = onClose;
ws.onerror = onError;
}
// _t.getWs = function() { return ws; }
_t.isOpen = function() { return ws && ws.readyState == ws.OPEN; }
_t.close = function() { ws && ws.close(); }
_t.send = function(text) { text != null && text.length > 0 && ws && ws.send(text); };
return _t;
};

View File

@@ -0,0 +1,369 @@
package forge;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.MenuElement;
import javax.swing.MenuSelectionManager;
import javax.swing.SwingUtilities;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Function;
import forge.assets.FSkinProp;
import forge.assets.ISkinImage;
import forge.control.FControl;
import forge.error.BugReporter;
import forge.events.UiEvent;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameType;
import forge.game.card.Card;
import forge.game.combat.Combat;
import forge.game.event.GameEventTurnBegan;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.LobbyPlayer;
import forge.game.player.Player;
import forge.game.player.RegisteredPlayer;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.gui.CardListViewer;
import forge.gui.FNetOverlay;
import forge.gui.GuiChoose;
import forge.gui.GuiUtils;
import forge.gui.SOverlayUtils;
import forge.gui.framework.SDisplayUtil;
import forge.gui.framework.SLayoutIO;
import forge.interfaces.IButton;
import forge.interfaces.IGuiBase;
import forge.item.PaperCard;
import forge.match.input.InputQueue;
import forge.net.FServer;
import forge.screens.match.CMatchUI;
import forge.screens.match.VMatchUI;
import forge.screens.match.ViewWinLose;
import forge.screens.match.controllers.CPrompt;
import forge.screens.match.controllers.CStack;
import forge.screens.match.views.VField;
import forge.screens.match.views.VHand;
import forge.screens.match.views.VPrompt;
import forge.toolbox.FButton;
import forge.toolbox.FOptionPane;
import forge.toolbox.FSkin;
import forge.toolbox.FSkin.SkinImage;
import forge.toolbox.special.PhaseLabel;
import forge.util.BuildInfo;
public class GuiDesktop implements IGuiBase {
public void invokeInEdtLater(Runnable runnable) {
SwingUtilities.invokeLater(runnable);
}
public void invokeInEdtAndWait(final Runnable proc) {
if (SwingUtilities.isEventDispatchThread()) {
// Just run in the current thread.
proc.run();
}
else {
try {
SwingUtilities.invokeAndWait(proc);
}
catch (final InterruptedException exn) {
throw new RuntimeException(exn);
}
catch (final InvocationTargetException exn) {
throw new RuntimeException(exn);
}
}
}
public boolean isGuiThread() {
return SwingUtilities.isEventDispatchThread();
}
public String getAssetsRoot() {
return StringUtils.containsIgnoreCase(BuildInfo.getVersionString(), "svn") ?
"../forge-gui/res/" : "res/";
}
public boolean mayShowCard(Card card) {
return Singletons.getControl().mayShowCard(card);
}
public void reportBug(String details) {
BugReporter.reportBug(details);
}
public void reportException(Throwable ex) {
BugReporter.reportException(ex);
}
public void reportException(Throwable ex, String message) {
BugReporter.reportException(ex, message);
}
public boolean showConfirmDialog(String message) {
return FOptionPane.showConfirmDialog(message);
}
public ISkinImage getUnskinnedIcon(String path) {
return new FSkin.UnskinnedIcon(path);
}
public int showOptionDialog(String message, String title, ISkinImage icon, String[] options, int defaultOption) {
return FOptionPane.showOptionDialog(message, title, (SkinImage)icon, options, defaultOption);
}
public <T> T showInputDialog(String message, String title, ISkinImage icon, T initialInput, T[] inputOptions) {
return FOptionPane.showInputDialog(message, title, (SkinImage)icon, initialInput, inputOptions);
}
public <T> List<T> getChoices(final String message, final int min, final int max, final Collection<T> choices, final T selected, final Function<T, String> display) {
return GuiChoose.getChoices(message, min, max, choices, selected, display);
}
public <T> List<T> order(final String title, final String top, final int remainingObjectsMin, final int remainingObjectsMax,
final List<T> sourceChoices, final List<T> destChoices, final Card referenceCard, final boolean sideboardingMode) {
return GuiChoose.order(title, top, sourceChoices, referenceCard);
}
public void showCardList(final String title, final String message, final List<PaperCard> list) {
final CardListViewer cardView = new CardListViewer(title, message, list);
cardView.setVisible(true);
cardView.dispose();
}
public IButton getBtnOK() {
return VMatchUI.SINGLETON_INSTANCE.getBtnOK();
}
public IButton getBtnCancel() {
return VMatchUI.SINGLETON_INSTANCE.getBtnCancel();
}
public void focusButton(final IButton button) {
// ensure we don't steal focus from an overlay
if (!SOverlayUtils.overlayHasFocus()) {
FThreads.invokeInEdtLater(new Runnable() {
@Override
public void run() {
((FButton)button).requestFocusInWindow();
}
});
}
}
public void flashIncorrectAction() {
SDisplayUtil.remind(VPrompt.SINGLETON_INSTANCE);
}
public void updatePhase() {
PhaseHandler pH = Singletons.getControl().getObservedGame().getPhaseHandler();
Player p = pH.getPlayerTurn();
PhaseType ph = pH.getPhase();
final CMatchUI matchUi = CMatchUI.SINGLETON_INSTANCE;
PhaseLabel lbl = matchUi.getFieldViewFor(p).getPhaseIndicator().getLabelFor(ph);
matchUi.resetAllPhaseButtons();
if (lbl != null) lbl.setActive(true);
}
public void updateTurn(final GameEventTurnBegan event, final Game game) {
VField nextField = CMatchUI.SINGLETON_INSTANCE.getFieldViewFor(event.turnOwner);
SDisplayUtil.showTab(nextField);
CPrompt.SINGLETON_INSTANCE.updateText(game);
}
public void updatePlayerControl() {
CMatchUI.SINGLETON_INSTANCE.initHandViews(FServer.getLobby().getGuiPlayer());
SLayoutIO.loadLayout(null);
VMatchUI.SINGLETON_INSTANCE.populate();
for (VHand h : VMatchUI.SINGLETON_INSTANCE.getHands()) {
h.getLayoutControl().updateHand();
}
}
public void finishGame() {
new ViewWinLose(Singletons.getControl().getObservedGame());
SOverlayUtils.showOverlay();
}
public void updateStack() {
CStack.SINGLETON_INSTANCE.update();
}
public void startMatch(GameType gameType, List<RegisteredPlayer> players) {
FControl.instance.startMatch(gameType, players);
}
public void setPanelSelection(Card c) {
GuiUtils.setPanelSelection(c);
}
public SpellAbility getAbilityToPlay(List<SpellAbility> abilities, Object triggerEvent) {
if (triggerEvent == null) {
if (abilities.isEmpty()) {
return null;
}
if (abilities.size() == 1) {
return abilities.get(0);
}
return GuiChoose.oneOrNone("Choose ability to play", abilities);
}
if (abilities.isEmpty()) {
return null;
}
if (abilities.size() == 1 && !abilities.get(0).promptIfOnlyPossibleAbility()) {
if (abilities.get(0).canPlay()) {
return abilities.get(0); //only return ability if it's playable, otherwise return null
}
return null;
}
//show menu if mouse was trigger for ability
final JPopupMenu menu = new JPopupMenu("Abilities");
boolean enabled;
boolean hasEnabled = false;
int shortcut = KeyEvent.VK_1; //use number keys as shortcuts for abilities 1-9
for (final SpellAbility ab : abilities) {
enabled = ab.canPlay();
if (enabled) {
hasEnabled = true;
}
GuiUtils.addMenuItem(menu, FSkin.encodeSymbols(ab.toString(), true),
shortcut > 0 ? KeyStroke.getKeyStroke(shortcut, 0) : null,
new Runnable() {
@Override
public void run() {
CPrompt.SINGLETON_INSTANCE.getInputControl().selectAbility(ab);
}
}, enabled);
if (shortcut > 0) {
shortcut++;
if (shortcut > KeyEvent.VK_9) {
shortcut = 0; //stop adding shortcuts after 9
}
}
}
if (hasEnabled) { //only show menu if at least one ability can be played
SwingUtilities.invokeLater(new Runnable() { //use invoke later to ensure first ability selected by default
public void run() {
MenuSelectionManager.defaultManager().setSelectedPath(new MenuElement[]{menu, menu.getSubElements()[0]});
}
});
MouseEvent mouseEvent = (MouseEvent) triggerEvent;
menu.show(mouseEvent.getComponent(), mouseEvent.getX(), mouseEvent.getY());
}
return null; //delay ability until choice made
}
public void hear(LobbyPlayer player, String message) {
FNetOverlay.SINGLETON_INSTANCE.addMessage(player.getName(), message);
}
public int getAvatarCount() {
if (FSkin.isLoaded()) {
FSkin.getAvatars().size();
}
return 0;
}
@Override
public int showOptionDialog(String message, String title, FSkinProp icon, String[] options, int defaultOption) {
return FOptionPane.showOptionDialog(message, title, FSkin.getImage(icon), options, defaultOption);
}
@Override
public <T> T showInputDialog(String message, String title, FSkinProp icon, T initialInput, T[] inputOptions) {
return FOptionPane.showInputDialog(message, title, FSkin.getImage(icon), initialInput, inputOptions);
}
@Override
public void fireEvent(UiEvent e) {
CMatchUI.SINGLETON_INSTANCE.fireEvent(e);
}
@Override
public void setCard(Card card) {
CMatchUI.SINGLETON_INSTANCE.setCard(card);
}
@Override
public void showCombat(Combat combat) {
CMatchUI.SINGLETON_INSTANCE.showCombat(combat);
}
@Override
public void setUsedToPay(Card card, boolean b) {
CMatchUI.SINGLETON_INSTANCE.setUsedToPay(card, b);
}
@Override
public void setHighlighted(Player player, boolean b) {
CMatchUI.SINGLETON_INSTANCE.setHighlighted(player, b);
}
@Override
public void showPromptMessage(String message) {
CMatchUI.SINGLETON_INSTANCE.showMessage(message);
}
@Override
public boolean stopAtPhase(Player playerTurn, PhaseType phase) {
return CMatchUI.SINGLETON_INSTANCE.stopAtPhase(playerTurn, phase);
}
@Override
public InputQueue getInputQueue() {
return FControl.instance.getInputQueue();
}
@Override
public Game getGame() {
return FControl.instance.getObservedGame();
}
@Override
public void updateZones(List<Pair<Player, ZoneType>> zonesToUpdate) {
CMatchUI.SINGLETON_INSTANCE.updateZones(zonesToUpdate);
}
@Override
public void updateCards(Set<Card> cardsToUpdate) {
CMatchUI.SINGLETON_INSTANCE.updateCards(cardsToUpdate);
}
@Override
public void updateManaPool(List<Player> manaPoolUpdate) {
CMatchUI.SINGLETON_INSTANCE.updateManaPool(manaPoolUpdate);
}
@Override
public void updateLives(List<Player> livesUpdate) {
CMatchUI.SINGLETON_INSTANCE.updateLives(livesUpdate);
}
@Override
public void endCurrentGame() {
FControl.instance.endCurrentGame();
}
@Override
public Map<Card, Integer> getDamageToAssign(Card attacker, List<Card> blockers,
int damageDealt, GameEntity defender, boolean overrideOrder) {
return CMatchUI.SINGLETON_INSTANCE.getDamageToAssign(attacker, blockers,
damageDealt, defender, overrideOrder);
}
}

View File

@@ -0,0 +1,218 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
import com.google.common.cache.LoadingCache;
import com.mortennobel.imagescaling.ResampleOp;
import forge.assets.FSkinProp;
import forge.assets.ImageUtil;
import forge.game.card.Card;
import forge.game.player.IHasIcon;
import forge.item.InventoryItem;
import forge.properties.ForgeConstants;
import forge.toolbox.FSkin;
import forge.toolbox.FSkin.SkinIcon;
import org.apache.commons.lang3.StringUtils;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutionException;
/**
* 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.
* <p/>
* The keys are the following:
* <ul>
* <li>Keys start with the file name, extension is skipped</li>
* <li>The key without suffix belongs to the unmodified image from the file</li>
* </ul>
*
* @author Forge
* @version $Id: ImageCache.java 25093 2014-03-08 05:36:37Z drdev $
*/
public class ImageCache {
// short prefixes to save memory
private static final Set<String> _missingIconKeys = new HashSet<String>();
private static final LoadingCache<String, BufferedImage> _CACHE = CacheBuilder.newBuilder().softValues().build(new ImageLoader());
private static final BufferedImage _defaultImage;
static {
BufferedImage defImage = null;
try {
defImage = ImageIO.read(new File(ForgeConstants.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;
}
}
public static void clear() {
_CACHE.invalidateAll();
_missingIconKeys.clear();
}
/**
* retrieve an image from the cache. returns null if the image is not found in the cache
* and cannot be loaded from disk. pass -1 for width and/or height to avoid resizing in that dimension.
*/
public static BufferedImage getImage(Card card, int width, int height) {
final String key;
if (!Singletons.getControl().mayShowCard(card) || card.isFaceDown()) {
key = ImageKeys.TOKEN_PREFIX + ImageKeys.MORPH_IMAGE;
} else {
key = card.getImageKey();
}
return scaleImage(key, width, height, true);
}
/**
* retrieve an image from the cache. returns null if the image is not found in the cache
* and cannot be loaded from disk. pass -1 for width and/or height to avoid resizing in that dimension.
*/
public static BufferedImage getImage(InventoryItem ii, int width, int height) {
return scaleImage(ImageKeys.getImageKey(ii, false), width, height, 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 SkinIcon getIcon(IHasIcon ihi) {
String imageKey = ihi.getIconImageKey();
final BufferedImage i;
if (_missingIconKeys.contains(imageKey) ||
null == (i = scaleImage(ihi.getIconImageKey(), -1, -1, false))) {
_missingIconKeys.add(imageKey);
return FSkin.getIcon(FSkinProp.ICO_UNKNOWN);
}
return new FSkin.UnskinnedIcon(i);
}
/**
* 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.
* <p>
* 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.
* </p>
*/
public static BufferedImage getOriginalImage(String imageKey, boolean useDefaultIfNotFound) {
if (null == 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 = ImageUtil.getImageKey(ImageUtil.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.
BufferedImage original = getImage(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 (original == null) {
if (useDefaultIfNotFound) {
original = _defaultImage;
_CACHE.put(imageKey, _defaultImage);
} else {
original = null;
}
}
return original;
}
private static BufferedImage scaleImage(String key, final int width, final int height, boolean useDefaultImage) {
if (StringUtils.isEmpty(key) || (3 > width && -1 != width) || (3 > height && -1 != height)) {
// picture too small or key not defined; return a blank
return null;
}
String resizedKey = String.format("%s#%dx%d", key, width, height);
final BufferedImage cached = _CACHE.getIfPresent(resizedKey);
if (null != cached) {
//System.out.println("found cached image: " + resizedKey);
return cached;
}
BufferedImage original = getOriginalImage(key, useDefaultImage);
if (original == null) { return null; }
// Calculate the scale required to best fit the image into the requested
// (width x height) dimensions whilst retaining aspect ratio.
double scaleX = (-1 == width ? 1 : (double)width / original.getWidth());
double scaleY = (-1 == height? 1 : (double)height / original.getHeight());
double bestFitScale = Math.min(scaleX, scaleY);
if ((bestFitScale > 1) && !ImageUtil.mayEnlarge()) {
bestFitScale = 1;
}
BufferedImage result;
if (1 == bestFitScale) {
result = original;
} else {
int destWidth = (int)(original.getWidth() * bestFitScale);
int destHeight = (int)(original.getHeight() * bestFitScale);
ResampleOp resampler = new ResampleOp(destWidth, destHeight);
result = resampler.filter(original, null);
}
//System.out.println("caching image: " + resizedKey);
_CACHE.put(resizedKey, result);
return result;
}
/**
* Returns the Image corresponding to the key.
*/
private static BufferedImage getImage(final String key) {
FThreads.assertExecutedByEdt(true);
try {
return ImageCache._CACHE.get(key);
} catch (final ExecutionException ex) {
if (ex.getCause() instanceof NullPointerException) {
return null;
}
ex.printStackTrace();
return null;
} catch (final InvalidCacheLoadException ex) {
// should be when a card legitimately has no image
return null;
}
}
}

View File

@@ -0,0 +1,99 @@
package forge;
import com.google.common.cache.CacheLoader;
import forge.assets.ImageUtil;
import forge.error.BugReporter;
import forge.properties.ForgeConstants;
import org.apache.commons.lang3.StringUtils;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
final class ImageLoader extends CacheLoader<String, BufferedImage> {
// 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 BufferedImage 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 = ForgeConstants.CACHE_TOKEN_PICS_DIR;
} else if (key.startsWith(ImageKeys.ICON_PREFIX)) {
filename = key.substring(ImageKeys.ICON_PREFIX.length());
path = ForgeConstants.CACHE_ICON_PICS_DIR;
} else if (key.startsWith(ImageKeys.BOOSTER_PREFIX)) {
filename = key.substring(ImageKeys.BOOSTER_PREFIX.length());
path = ForgeConstants.CACHE_BOOSTER_PICS_DIR;
} else if (key.startsWith(ImageKeys.FATPACK_PREFIX)) {
filename = key.substring(ImageKeys.FATPACK_PREFIX.length());
path = ForgeConstants.CACHE_FATPACK_PICS_DIR;
} else if (key.startsWith(ImageKeys.PRECON_PREFIX)) {
filename = key.substring(ImageKeys.PRECON_PREFIX.length());
path = ForgeConstants.CACHE_PRECON_PICS_DIR;
} else if (key.startsWith(ImageKeys.TOURNAMENTPACK_PREFIX)) {
filename = key.substring(ImageKeys.TOURNAMENTPACK_PREFIX.length());
path = ForgeConstants.CACHE_TOURNAMENTPACK_PICS_DIR;
} else {
filename = key;
path = ForgeConstants.CACHE_CARD_PICS_DIR;
}
BufferedImage ret = _findFile(key, path, filename);
// some S00 cards are really part of 6ED
if (null == ret ) {
String s2kAlias = ImageUtil.getSetFolder("S00");
if ( filename.startsWith(s2kAlias) ) {
ret = _findFile(key, path, filename.replace(s2kAlias, ImageUtil.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 BufferedImage _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 ImageIO.read(file);
} catch (IOException ex) {
BugReporter.reportException(ex, "Could not read image file " + file.getAbsolutePath() + " ");
break;
}
}
}
return null;
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge;
import forge.control.FControl;
import forge.model.FModel;
import forge.view.FView;
/**
* Provides global/static access to singleton instances.
*/
public final class Singletons {
private static boolean initialized = false;
private static FView view = null;
private static FControl control = null;
/**
* IMPORTANT - does not return view frame! Must call
* getFrame() from FView for that.
*/
public static FView getView() { return view; }
public static FControl getControl() { return control; }
public static void initializeOnce(boolean withUi) {
FThreads.assertExecutedByEdt(false);
synchronized (Singletons.class) {
if (initialized) {
throw new IllegalStateException("Singletons.initializeOnce really cannot be called again");
}
initialized = true;
}
if (withUi) {
view = FView.SINGLETON_INSTANCE;
}
FModel.initialize(view == null ? null : view.getSplash().getProgressBar());
if (withUi) {
control = FControl.instance;
}
}
// disallow instantiation
private Singletons() { }
}

View File

@@ -0,0 +1,673 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.control;
import forge.FThreads;
import forge.ImageCache;
import forge.Singletons;
import forge.ai.LobbyPlayerAi;
import forge.assets.FSkinProp;
import forge.control.KeyboardShortcuts.Shortcut;
import forge.game.Game;
import forge.game.GameRules;
import forge.game.GameType;
import forge.game.Match;
import forge.game.card.Card;
import forge.game.player.LobbyPlayer;
import forge.game.player.Player;
import forge.game.player.RegisteredPlayer;
import forge.gui.GuiDialog;
import forge.gui.SOverlayUtils;
import forge.gui.framework.*;
import forge.player.GamePlayerUtil;
import forge.player.LobbyPlayerHuman;
import forge.match.input.InputQueue;
import forge.menus.ForgeMenu;
import forge.model.FModel;
import forge.net.FServer;
import forge.properties.ForgePreferences;
import forge.properties.ForgePreferences.FPref;
import forge.properties.ForgeConstants;
import forge.quest.QuestController;
import forge.quest.data.QuestPreferences.QPref;
import forge.quest.io.QuestDataIO;
import forge.screens.deckeditor.CDeckEditorUI;
import forge.screens.match.CMatchUI;
import forge.screens.match.VMatchUI;
import forge.screens.match.controllers.CDock;
import forge.screens.match.controllers.CLog;
import forge.screens.match.controllers.CPrompt;
import forge.screens.match.controllers.CStack;
import forge.screens.match.views.VAntes;
import forge.screens.match.views.VDev;
import forge.screens.match.views.VField;
import forge.sound.SoundSystem;
import forge.toolbox.FOptionPane;
import forge.toolbox.FSkin;
import forge.toolbox.special.PhaseIndicator;
import forge.view.FFrame;
import forge.view.FView;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* FControl.
* </p>
* Controls all Forge UI functionality inside one JFrame. This class switches
* between various display screens in that JFrame. Controllers are instantiated
* separately by each screen's top level view class.
*/
public enum FControl implements KeyEventDispatcher {
instance;
private ForgeMenu forgeMenu;
private List<Shortcut> shortcuts;
private JLayeredPane display;
private FScreen currentScreen;
private boolean altKeyLastDown;
private CloseAction closeAction;
public static enum CloseAction {
NONE,
CLOSE_SCREEN,
EXIT_FORGE
}
private final SoundSystem soundSystem = new SoundSystem();
/**
* <p>
* FControl.
* </p>
* Controls all Forge UI functionality inside one JFrame. This class
* switches between various display screens in that JFrame. Controllers are
* instantiated separately by each screen's top level view class.
*/
private FControl() {
Singletons.getView().getFrame().addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(final WindowEvent e) {
switch (closeAction) {
case NONE: //prompt user for close action if not previously specified
String[] options = {"Close Screen", "Exit Forge", "Cancel"};
int reply = FOptionPane.showOptionDialog(
"Forge now supports navigation tabs which allow closing and switching between different screens with ease. "
+ "As a result, you no longer need to use the X button in the upper right to close the current screen and go back."
+ "\n\n"
+ "Please select what you want to happen when clicking the X button in the upper right. This choice will be used "
+ "going forward and you will not see this message again. You can change this behavior at any time in Preferences.",
"Select Your Close Action",
FOptionPane.INFORMATION_ICON,
options,
2);
switch (reply) {
case 0: //Close Screen
setCloseAction(CloseAction.CLOSE_SCREEN);
windowClosing(e); //call again to apply chosen close action
return;
case 1: //Exit Forge
setCloseAction(CloseAction.EXIT_FORGE);
windowClosing(e); //call again to apply chosen close action
return;
}
break;
case CLOSE_SCREEN:
Singletons.getView().getNavigationBar().closeSelectedTab();
break;
case EXIT_FORGE:
if (exitForge()) { return; }
break;
}
//prevent closing Forge if we reached this point
Singletons.getView().getFrame().setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
}
});
}
public CloseAction getCloseAction() {
return this.closeAction;
}
public void setCloseAction(CloseAction closeAction0) {
if (this.closeAction == closeAction0) { return; }
this.closeAction = closeAction0;
Singletons.getView().getNavigationBar().updateBtnCloseTooltip();
final ForgePreferences prefs = FModel.getPreferences();
prefs.setPref(FPref.UI_CLOSE_ACTION, closeAction0.toString());
prefs.save();
}
public boolean canExitForge(boolean forRestart) {
String action = (forRestart ? "Restart" : "Exit");
String userPrompt = "Are you sure you wish to " + (forRestart ? "restart" : "exit") + " Forge?";
if (this.game != null) {
userPrompt = "A game is currently active. " + userPrompt;
}
if (!FOptionPane.showConfirmDialog(userPrompt, action + " Forge", action, "Cancel", this.game == null)) { //default Yes if no game active
return false;
}
if (!CDeckEditorUI.SINGLETON_INSTANCE.canSwitchAway(true)) {
return false;
}
return true;
}
public boolean exitForge() {
if (!canExitForge(false)) {
return false;
}
Singletons.getView().getFrame().setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
System.exit(0);
return true;
}
/** After view and model have been initialized, control can start.
* @param isHeadlessMode */
public void initialize() {
// Preloads skin components (using progress bar).
FSkin.loadFull(true);
this.shortcuts = KeyboardShortcuts.attachKeyboardShortcuts();
this.display = FView.SINGLETON_INSTANCE.getLpnDocument();
final ForgePreferences prefs = FModel.getPreferences();
this.closeAction = CloseAction.valueOf(prefs.getPref(FPref.UI_CLOSE_ACTION));
FView.SINGLETON_INSTANCE.setSplashProgessBarMessage("About to load current quest.");
// Preload quest data if present
final File dirQuests = new File(ForgeConstants.QUEST_SAVE_DIR);
final String questname = FModel.getQuestPreferences().getPref(QPref.CURRENT_QUEST);
final File data = new File(dirQuests.getPath(), questname);
if (data.exists()) {
FModel.getQuest().load(QuestDataIO.loadData(data));
}
// Handles resizing in null layouts of layers in JLayeredPane as well as saving window layout
final FFrame window = Singletons.getView().getFrame();
window.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(final ComponentEvent e) {
sizeChildren();
window.updateNormalBounds();
SLayoutIO.saveWindowLayout();
}
@Override
public void componentMoved(final ComponentEvent e) {
window.updateNormalBounds();
SLayoutIO.saveWindowLayout();
}
});
FView.SINGLETON_INSTANCE.getLpnDocument().addMouseListener(SOverflowUtil.getHideOverflowListener());
FView.SINGLETON_INSTANCE.getLpnDocument().addComponentListener(SResizingUtil.getWindowResizeListener());
setGlobalKeyboardHandler();
FView.SINGLETON_INSTANCE.setSplashProgessBarMessage("Opening main window...");
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Singletons.getView().initialize();
}
});
}
private void setGlobalKeyboardHandler() {
KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
manager.addKeyEventDispatcher(this);
}
public ForgeMenu getForgeMenu() {
if (this.forgeMenu == null) {
this.forgeMenu = new ForgeMenu();
}
return this.forgeMenu;
}
public FScreen getCurrentScreen() {
return this.currentScreen;
}
/**
* Switches between display screens in top level JFrame.
*/
public boolean setCurrentScreen(FScreen screen) {
return setCurrentScreen(screen, false);
}
public boolean setCurrentScreen(FScreen screen, boolean previousScreenClosed) {
//TODO: Uncomment the line below if this function stops being used to refresh
//the current screen in some places (such as Continue and Restart in the match screen)
//if (this.currentScreen == screen) { return; }
//give previous screen a chance to perform special switch handling and/or cancel switching away from screen
if (this.currentScreen != screen && !Singletons.getView().getNavigationBar().canSwitch(screen)) {
return false;
}
if (this.currentScreen == FScreen.MATCH_SCREEN) { //hide targeting overlay and reset image if was on match screen
SOverlayUtils.hideTargetingOverlay();
if (isMatchBackgroundImageVisible()) {
FView.SINGLETON_INSTANCE.getPnlInsets().setForegroundImage(new ImageIcon());
}
}
clearChildren(JLayeredPane.DEFAULT_LAYER);
SOverlayUtils.hideOverlay();
ImageCache.clear(); //reduce memory usage by clearing image cache when switching screens
this.currentScreen = screen;
//load layout for new current screen
try {
SLayoutIO.loadLayout(null);
} catch (InvalidLayoutFileException ex) {
GuiDialog.message("Your " + screen.getTabCaption() + " layout file could not be read. It will be deleted after you press OK.\nThe game will proceed with default layout.");
if (screen.deleteLayoutFile()) {
SLayoutIO.loadLayout(null); //try again
}
}
screen.getView().populate();
screen.getController().initialize();
if (screen == FScreen.MATCH_SCREEN) {
if (isMatchBackgroundImageVisible()) {
FView.SINGLETON_INSTANCE.getPnlInsets().setForegroundImage(FSkin.getIcon(FSkinProp.BG_MATCH));
}
SOverlayUtils.showTargetingOverlay();
}
Singletons.getView().getNavigationBar().updateSelectedTab();
return true;
}
private boolean isMatchBackgroundImageVisible() {
return FModel.getPreferences().getPrefBoolean(FPref.UI_MATCH_IMAGE_VISIBLE);
}
public boolean ensureScreenActive(FScreen screen) {
if (this.currentScreen == screen) { return true; }
return setCurrentScreen(screen);
}
/** @return List<Shortcut> A list of attached keyboard shortcut descriptions and properties. */
public List<Shortcut> getShortcuts() {
return this.shortcuts;
}
/** Remove all children from a specified layer. */
private void clearChildren(final int layer0) {
final Component[] children = FView.SINGLETON_INSTANCE.getLpnDocument().getComponentsInLayer(layer0);
for (final Component c : children) {
display.remove(c);
}
}
/** Sizes children of JLayeredPane to fully fit their layers. */
private void sizeChildren() {
Component[] children = display.getComponentsInLayer(JLayeredPane.DEFAULT_LAYER);
if (children.length != 0) { children[0].setSize(display.getSize()); }
children = display.getComponentsInLayer(FView.TARGETING_LAYER);
if (children.length != 0) { children[0].setSize(display.getSize()); }
children = display.getComponentsInLayer(JLayeredPane.MODAL_LAYER);
if (children.length != 0) { children[0].setSize(display.getSize()); }
}
public Player getCurrentPlayer() {
// try current priority
Player currentPriority = game.getPhaseHandler().getPriorityPlayer();
if (null != currentPriority && currentPriority.getLobbyPlayer() == FServer.getLobby().getGuiPlayer()) {
return currentPriority;
}
// otherwise find just any player, belonging to this lobbyplayer
for (Player p : game.getPlayers()) {
if (p.getLobbyPlayer() == FServer.getLobby().getGuiPlayer()) {
return p;
}
}
return null;
}
public boolean mayShowCard(Card c) {
return game == null || !gameHasHumanPlayer || c.canBeShownTo(getCurrentPlayer());
}
/**
* TODO: Write javadoc for this method.
* @return
*/
public SoundSystem getSoundSystem() {
return soundSystem;
}
private Game game;
private boolean gameHasHumanPlayer;
public Game getObservedGame() {
return game;
}
public final void stopGame() {
List<Player> pp = new ArrayList<Player>();
for (Player p : game.getPlayers()) {
if (p.getOriginalLobbyPlayer() == FServer.getLobby().getGuiPlayer()) {
pp.add(p);
}
}
boolean hasHuman = !pp.isEmpty();
if (pp.isEmpty()) {
pp.addAll(game.getPlayers()); // no human? then all players surrender!
}
for (Player p: pp) {
p.concede();
}
Player priorityPlayer = game.getPhaseHandler().getPriorityPlayer();
boolean humanHasPriority = priorityPlayer == null || priorityPlayer.getLobbyPlayer() == FServer.getLobby().getGuiPlayer();
if (hasHuman && humanHasPriority) {
game.getAction().checkGameOverCondition();
}
else {
game.isGameOver(); // this is synchronized method - it's used to make Game-0 thread see changes made here
inputQueue.onGameOver(false); //release any waiting input, effectively passing priority
}
playbackControl.onGameStopRequested();
}
private InputQueue inputQueue;
public InputQueue getInputQueue() {
return inputQueue;
}
public final void startGameWithUi(final Match match) {
if (this.game != null) {
this.setCurrentScreen(FScreen.MATCH_SCREEN);
SOverlayUtils.hideOverlay();
FOptionPane.showMessageDialog("Cannot start a new game while another game is already in progress.");
return; //TODO: See if it's possible to run multiple games at once without crashing
}
setPlayerName(match.getPlayers());
final Game newGame = match.createGame();
attachToGame(newGame);
// It's important to run match in a different thread to allow GUI inputs to be invoked from inside game.
// Game is set on pause while gui player takes decisions
game.getAction().invoke(new Runnable() {
@Override
public void run() {
match.startGame(newGame);
}
});
}
public final void endCurrentGame() {
if (this.game == null) { return; }
Singletons.getView().getNavigationBar().closeTab(FScreen.MATCH_SCREEN);
this.game = null;
}
private final FControlGameEventHandler fcVisitor = new FControlGameEventHandler();
private final FControlGamePlayback playbackControl = new FControlGamePlayback();
private void attachToGame(Game game0) {
if (game0.getRules().getGameType() == GameType.Quest) {
QuestController qc = FModel.getQuest();
// Reset new list when the Match round starts, not when each game starts
if (game0.getMatch().getPlayedGames().isEmpty()) {
qc.getCards().resetNewList();
}
game0.subscribeToEvents(qc); // this one listens to player's mulligans ATM
}
inputQueue = new InputQueue();
this.game = game0;
game.subscribeToEvents(Singletons.getControl().getSoundSystem());
LobbyPlayer humanLobbyPlayer = FServer.getLobby().getGuiPlayer();
// The UI controls should use these game data as models
CMatchUI.SINGLETON_INSTANCE.initMatch(game.getRegisteredPlayers(), humanLobbyPlayer);
CDock.SINGLETON_INSTANCE.setModel(game, humanLobbyPlayer);
CStack.SINGLETON_INSTANCE.setModel(game.getStack(), humanLobbyPlayer);
CLog.SINGLETON_INSTANCE.setModel(game.getGameLog());
actuateMatchPreferences();
VAntes.SINGLETON_INSTANCE.setModel(game.getRegisteredPlayers());
setCurrentScreen(FScreen.MATCH_SCREEN);
SDisplayUtil.showTab(EDocID.REPORT_LOG.getDoc());
CPrompt.SINGLETON_INSTANCE.getInputControl().setGame(game);
// Listen to DuelOutcome event to show ViewWinLose
game.subscribeToEvents(fcVisitor);
// Add playback controls to match if needed
gameHasHumanPlayer = false;
for (Player p : game.getPlayers()) {
if (p.getController().getLobbyPlayer() == FServer.getLobby().getGuiPlayer())
gameHasHumanPlayer = true;
}
if (!gameHasHumanPlayer) {
game.subscribeToEvents(playbackControl);
}
for (final VField field : VMatchUI.SINGLETON_INSTANCE.getFieldViews()) {
field.getDetailsPanel().getLblLibrary().setHoverable(ForgePreferences.DEV_MODE);
}
// per player observers were set in CMatchUI.SINGLETON_INSTANCE.initMatch
//Set Field shown to current player.
VField nextField = CMatchUI.SINGLETON_INSTANCE.getFieldViewFor(game.getPlayers().get(0));
SDisplayUtil.showTab(nextField);
}
/* (non-Javadoc)
* @see java.awt.KeyEventDispatcher#dispatchKeyEvent(java.awt.event.KeyEvent)
*/
@Override
public boolean dispatchKeyEvent(KeyEvent e) {
// Show Forge menu if Alt key pressed without modifiers and released without pressing any other keys in between
if (e.getKeyCode() == KeyEvent.VK_ALT) {
if (e.getID() == KeyEvent.KEY_RELEASED) {
if (altKeyLastDown) {
forgeMenu.show(true);
return true;
}
}
else if (e.getID() == KeyEvent.KEY_PRESSED && e.getModifiers() == KeyEvent.ALT_MASK) {
altKeyLastDown = true;
}
}
else {
altKeyLastDown = false;
if (e.getID() == KeyEvent.KEY_PRESSED) {
if (forgeMenu.handleKeyEvent(e)) { //give Forge menu the chance to handle the key event
return true;
}
}
else if (e.getID() == KeyEvent.KEY_RELEASED) {
if (e.getKeyCode() == KeyEvent.VK_CONTEXT_MENU) {
forgeMenu.show();
}
}
}
//Allow the event to be redispatched
return false;
}
/**
* Prompts user for a name that will be used instead of "Human" during gameplay.
* <p>
* This is a one time only event that is triggered when starting a game and the
* PLAYER_NAME setting is blank. Does not apply to a hotseat game.
*/
private void setPlayerName(List<RegisteredPlayer> players) {
final ForgePreferences prefs = FModel.getPreferences();
if (StringUtils.isBlank(prefs.getPref(FPref.PLAYER_NAME))) {
boolean isPlayerOneHuman = players.get(0).getPlayer() instanceof LobbyPlayerHuman;
boolean isPlayerTwoComputer = players.get(1).getPlayer() instanceof LobbyPlayerAi;
if (isPlayerOneHuman && isPlayerTwoComputer) {
GamePlayerUtil.setPlayerName();
}
}
}
public void startMatch(GameType gameType, List<RegisteredPlayer> players) {
startMatch(gameType, null, players);
}
public void startMatch(GameType gameType, List<GameType> appliedVariants, List<RegisteredPlayer> players) {
boolean useRandomFoil = FModel.getPreferences().getPrefBoolean(FPref.UI_RANDOM_FOIL);
for(RegisteredPlayer rp : players) {
rp.setRandomFoil(useRandomFoil);
}
GameRules rules = new GameRules(gameType);
if (null != appliedVariants && !appliedVariants.isEmpty())
rules.setAppliedVariants(appliedVariants);
rules.setPlayForAnte(FModel.getPreferences().getPrefBoolean(FPref.UI_ANTE));
rules.setManaBurn(FModel.getPreferences().getPrefBoolean(FPref.UI_MANABURN));
rules.canCloneUseTargetsImage = FModel.getPreferences().getPrefBoolean(FPref.UI_CLONE_MODE_SOURCE);
final Match mc = new Match(rules, players);
SOverlayUtils.startGameOverlay();
SOverlayUtils.showOverlay();
FThreads.invokeInEdtLater(new Runnable(){
@Override
public void run() {
startGameWithUi(mc);
SOverlayUtils.hideOverlay();
}
});
}
/**
* TODO: Needs to be reworked for efficiency with rest of prefs saves in
* codebase.
*/
public void writeMatchPreferences() {
final ForgePreferences prefs = FModel.getPreferences();
final List<VField> fieldViews = VMatchUI.SINGLETON_INSTANCE.getFieldViews();
// AI field is at index [1]
PhaseIndicator fvAi = fieldViews.get(1).getPhaseIndicator();
prefs.setPref(FPref.PHASE_AI_UPKEEP, String.valueOf(fvAi.getLblUpkeep().getEnabled()));
prefs.setPref(FPref.PHASE_AI_DRAW, String.valueOf(fvAi.getLblDraw().getEnabled()));
prefs.setPref(FPref.PHASE_AI_MAIN1, String.valueOf(fvAi.getLblMain1().getEnabled()));
prefs.setPref(FPref.PHASE_AI_BEGINCOMBAT, String.valueOf(fvAi.getLblBeginCombat().getEnabled()));
prefs.setPref(FPref.PHASE_AI_DECLAREATTACKERS, String.valueOf(fvAi.getLblDeclareAttackers().getEnabled()));
prefs.setPref(FPref.PHASE_AI_DECLAREBLOCKERS, String.valueOf(fvAi.getLblDeclareBlockers().getEnabled()));
prefs.setPref(FPref.PHASE_AI_FIRSTSTRIKE, String.valueOf(fvAi.getLblFirstStrike().getEnabled()));
prefs.setPref(FPref.PHASE_AI_COMBATDAMAGE, String.valueOf(fvAi.getLblCombatDamage().getEnabled()));
prefs.setPref(FPref.PHASE_AI_ENDCOMBAT, String.valueOf(fvAi.getLblEndCombat().getEnabled()));
prefs.setPref(FPref.PHASE_AI_MAIN2, String.valueOf(fvAi.getLblMain2().getEnabled()));
prefs.setPref(FPref.PHASE_AI_EOT, String.valueOf(fvAi.getLblEndTurn().getEnabled()));
prefs.setPref(FPref.PHASE_AI_CLEANUP, String.valueOf(fvAi.getLblCleanup().getEnabled()));
// Human field is at index [0]
PhaseIndicator fvHuman = fieldViews.get(0).getPhaseIndicator();
prefs.setPref(FPref.PHASE_HUMAN_UPKEEP, String.valueOf(fvHuman.getLblUpkeep().getEnabled()));
prefs.setPref(FPref.PHASE_HUMAN_DRAW, String.valueOf(fvHuman.getLblDraw().getEnabled()));
prefs.setPref(FPref.PHASE_HUMAN_MAIN1, String.valueOf(fvHuman.getLblMain1().getEnabled()));
prefs.setPref(FPref.PHASE_HUMAN_BEGINCOMBAT, String.valueOf(fvHuman.getLblBeginCombat().getEnabled()));
prefs.setPref(FPref.PHASE_HUMAN_DECLAREATTACKERS, String.valueOf(fvHuman.getLblDeclareAttackers().getEnabled()));
prefs.setPref(FPref.PHASE_HUMAN_DECLAREBLOCKERS, String.valueOf(fvHuman.getLblDeclareBlockers().getEnabled()));
prefs.setPref(FPref.PHASE_HUMAN_FIRSTSTRIKE, String.valueOf(fvHuman.getLblFirstStrike().getEnabled()));
prefs.setPref(FPref.PHASE_HUMAN_COMBATDAMAGE, String.valueOf(fvHuman.getLblCombatDamage().getEnabled()));
prefs.setPref(FPref.PHASE_HUMAN_ENDCOMBAT, String.valueOf(fvHuman.getLblEndCombat().getEnabled()));
prefs.setPref(FPref.PHASE_HUMAN_MAIN2, String.valueOf(fvHuman.getLblMain2().getEnabled()));
prefs.setPref(FPref.PHASE_HUMAN_EOT, fvHuman.getLblEndTurn().getEnabled());
prefs.setPref(FPref.PHASE_HUMAN_CLEANUP, fvHuman.getLblCleanup().getEnabled());
final VDev v = VDev.SINGLETON_INSTANCE;
// prefs.setPref(FPref.DEV_MILLING_LOSS, v.getLblMilling().getEnabled());
prefs.setPref(FPref.DEV_UNLIMITED_LAND, v.getLblUnlimitedLands().getEnabled());
prefs.save();
}
/**
* TODO: Needs to be reworked for efficiency with rest of prefs saves in
* codebase.
*/
public void actuateMatchPreferences() {
final ForgePreferences prefs = FModel.getPreferences();
final List<VField> fieldViews = VMatchUI.SINGLETON_INSTANCE.getFieldViews();
ForgePreferences.DEV_MODE = prefs.getPrefBoolean(FPref.DEV_MODE_ENABLED);
ForgePreferences.UPLOAD_DRAFT = ForgePreferences.NET_CONN; // && prefs.getPrefBoolean(FPref.UI_UPLOAD_DRAFT);
// AI field is at index [0]
PhaseIndicator fvAi = fieldViews.get(1).getPhaseIndicator();
fvAi.getLblUpkeep().setEnabled(prefs.getPrefBoolean(FPref.PHASE_AI_UPKEEP));
fvAi.getLblDraw().setEnabled(prefs.getPrefBoolean(FPref.PHASE_AI_DRAW));
fvAi.getLblMain1().setEnabled(prefs.getPrefBoolean(FPref.PHASE_AI_MAIN1));
fvAi.getLblBeginCombat().setEnabled(prefs.getPrefBoolean(FPref.PHASE_AI_BEGINCOMBAT));
fvAi.getLblDeclareAttackers().setEnabled(prefs.getPrefBoolean(FPref.PHASE_AI_DECLAREATTACKERS));
fvAi.getLblDeclareBlockers().setEnabled(prefs.getPrefBoolean(FPref.PHASE_AI_DECLAREBLOCKERS));
fvAi.getLblFirstStrike().setEnabled(prefs.getPrefBoolean(FPref.PHASE_AI_FIRSTSTRIKE));
fvAi.getLblCombatDamage().setEnabled(prefs.getPrefBoolean(FPref.PHASE_AI_COMBATDAMAGE));
fvAi.getLblEndCombat().setEnabled(prefs.getPrefBoolean(FPref.PHASE_AI_ENDCOMBAT));
fvAi.getLblMain2().setEnabled(prefs.getPrefBoolean(FPref.PHASE_AI_MAIN2));
fvAi.getLblEndTurn().setEnabled(prefs.getPrefBoolean(FPref.PHASE_AI_EOT));
fvAi.getLblCleanup().setEnabled(prefs.getPrefBoolean(FPref.PHASE_AI_CLEANUP));
// Human field is at index [1]
PhaseIndicator fvHuman = fieldViews.get(0).getPhaseIndicator();
fvHuman.getLblUpkeep().setEnabled(prefs.getPrefBoolean(FPref.PHASE_HUMAN_UPKEEP));
fvHuman.getLblDraw().setEnabled(prefs.getPrefBoolean(FPref.PHASE_HUMAN_DRAW));
fvHuman.getLblMain1().setEnabled(prefs.getPrefBoolean(FPref.PHASE_HUMAN_MAIN1));
fvHuman.getLblBeginCombat().setEnabled(prefs.getPrefBoolean(FPref.PHASE_HUMAN_BEGINCOMBAT));
fvHuman.getLblDeclareAttackers().setEnabled(prefs.getPrefBoolean(FPref.PHASE_HUMAN_DECLAREATTACKERS));
fvHuman.getLblDeclareBlockers().setEnabled(prefs.getPrefBoolean(FPref.PHASE_HUMAN_DECLAREBLOCKERS));
fvHuman.getLblFirstStrike().setEnabled(prefs.getPrefBoolean(FPref.PHASE_HUMAN_FIRSTSTRIKE));
fvHuman.getLblCombatDamage().setEnabled(prefs.getPrefBoolean(FPref.PHASE_HUMAN_COMBATDAMAGE));
fvHuman.getLblEndCombat().setEnabled(prefs.getPrefBoolean(FPref.PHASE_HUMAN_ENDCOMBAT));
fvHuman.getLblMain2().setEnabled(prefs.getPrefBoolean(FPref.PHASE_HUMAN_MAIN2));
fvHuman.getLblEndTurn().setEnabled(prefs.getPrefBoolean(FPref.PHASE_HUMAN_EOT));
fvHuman.getLblCleanup().setEnabled(prefs.getPrefBoolean(FPref.PHASE_HUMAN_CLEANUP));
//Singletons.getView().getViewMatch().setLayoutParams(prefs.getPref(FPref.UI_LAYOUT_PARAMS));
}
}

View File

@@ -0,0 +1,284 @@
package forge.control;
import forge.Singletons;
import forge.gui.framework.EDocID;
import forge.gui.framework.FScreen;
import forge.gui.framework.SDisplayUtil;
import forge.model.FModel;
import forge.properties.ForgePreferences.FPref;
import forge.screens.home.settings.VSubmenuPreferences.KeyboardShortcutField;
import forge.screens.match.CMatchUI;
import forge.screens.match.controllers.CDock;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Consolidates keyboard shortcut assembly into one location
* for all shortcuts in the project.
*
* Just map a new Shortcut object here, set a default in preferences,
* and you're done.
*/
public class KeyboardShortcuts {
/**
* Attaches all keyboard shortcuts for match UI,
* and returns a list of shortcuts with necessary properties for later access.
*
* @return List<Shortcut> Shortcut objects
*/
@SuppressWarnings("serial")
public static List<Shortcut> attachKeyboardShortcuts() {
final JComponent c = Singletons.getView().getFrame().getLayeredPane();
final InputMap im = c.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
final ActionMap am = c.getActionMap();
List<Shortcut> list = new ArrayList<Shortcut>();
//========== Match Shortcuts
/** Show stack. */
final Action actShowStack = new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
if (Singletons.getControl().getCurrentScreen() != FScreen.MATCH_SCREEN) { return; }
SDisplayUtil.showTab(EDocID.REPORT_STACK.getDoc());
}
};
/** Show combat. */
final Action actShowCombat = new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
if (Singletons.getControl().getCurrentScreen() != FScreen.MATCH_SCREEN) { return; }
SDisplayUtil.showTab(EDocID.REPORT_COMBAT.getDoc());
}
};
/** Show console. */
final Action actShowConsole = new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
if (Singletons.getControl().getCurrentScreen() != FScreen.MATCH_SCREEN) { return; }
SDisplayUtil.showTab(EDocID.REPORT_LOG.getDoc());
}
};
/** Show players panel. */
final Action actShowPlayers = new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
if (Singletons.getControl().getCurrentScreen() != FScreen.MATCH_SCREEN) { return; }
SDisplayUtil.showTab(EDocID.REPORT_PLAYERS.getDoc());
}
};
/** Show dev panel. */
final Action actShowDev = new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
if (Singletons.getControl().getCurrentScreen() != FScreen.MATCH_SCREEN) { return; }
if (FModel.getPreferences().getPrefBoolean(FPref.DEV_MODE_ENABLED)) {
SDisplayUtil.showTab(EDocID.DEV_MODE.getDoc());
}
}
};
/** Concede game. */
final Action actConcede = new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
if (Singletons.getControl().getCurrentScreen() != FScreen.MATCH_SCREEN) { return; }
CMatchUI.SINGLETON_INSTANCE.concede();
}
};
/** End turn. */
final Action actEndTurn = new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
if (Singletons.getControl().getCurrentScreen() != FScreen.MATCH_SCREEN) { return; }
CDock.SINGLETON_INSTANCE.endTurn();
}
};
/** Alpha Strike. */
final Action actAllAttack = new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
if (Singletons.getControl().getCurrentScreen() != FScreen.MATCH_SCREEN) { return; }
CDock.SINGLETON_INSTANCE.alphaStrike();
}
};
/** Targeting visualization overlay. */
final Action actTgtOverlay = new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
if (Singletons.getControl().getCurrentScreen() != FScreen.MATCH_SCREEN) { return; }
CDock.SINGLETON_INSTANCE.toggleTargeting();
}
};
//========== Instantiate shortcut objects and add to list.
list.add(new Shortcut(FPref.SHORTCUT_SHOWSTACK, "Match: show stack panel", actShowStack, am, im));
list.add(new Shortcut(FPref.SHORTCUT_SHOWCOMBAT, "Match: show combat panel", actShowCombat, am, im));
list.add(new Shortcut(FPref.SHORTCUT_SHOWCONSOLE, "Match: show console panel", actShowConsole, am, im));
list.add(new Shortcut(FPref.SHORTCUT_SHOWPLAYERS, "Match: show players panel", actShowPlayers, am, im));
list.add(new Shortcut(FPref.SHORTCUT_SHOWDEV, "Match: show dev panel", actShowDev, am, im));
list.add(new Shortcut(FPref.SHORTCUT_CONCEDE, "Match: concede game", actConcede, am, im));
list.add(new Shortcut(FPref.SHORTCUT_ENDTURN, "Match: pass priority until EOT or next stack event", actEndTurn, am, im));
list.add(new Shortcut(FPref.SHORTCUT_ALPHASTRIKE, "Match: Alpha Strike (attack with all available)", actAllAttack, am, im));
list.add(new Shortcut(FPref.SHORTCUT_SHOWTARGETING, "Match: toggle targeting visual overlay", actTgtOverlay, am, im));
return list;
} // End initMatchShortcuts()
/**
*
* Instantiates a shortcut key instance with various properties for use
* throughout the project.
*
*/
public static class Shortcut {
/** */
private final FPref prefkeys;
/** */
private final String description;
/** */
private final Action handler;
/** */
private final ActionMap actionMap;
/** */
private final InputMap inputMap;
/** */
private KeyStroke key;
/** */
private String str;
/**
*
* Instantiates a shortcut key instance with various properties for use
* throughout the project.
*
* @param prefkey0 String, ident key in forge.preferences
* @param description0 String, description of shortcut function
* @param handler0 Action, action on execution of shortcut
* @param am0 ActionMap, of container targeted by shortcut
* @param im0 InputMap, of container targeted by shortcut
*/
public Shortcut(final FPref prefkey0, final String description0,
final Action handler0, final ActionMap am0, final InputMap im0) {
prefkeys = prefkey0;
description = description0;
handler = handler0;
actionMap = am0;
inputMap = im0;
attach();
}
/** @return {@link java.lang.String} */
public String getDescription() {
return description;
}
/**
* String ident key in forge.preferences.
* @return {@link java.lang.String}
*/
public FPref getPrefKey() {
return prefkeys;
}
/** */
public void attach() {
detach();
str = FModel.getPreferences().getPref(prefkeys);
if (!str.isEmpty()) {
key = assembleKeystrokes(str.split(" "));
// Attach key stroke to input map...
inputMap.put(key, str);
// ...then attach actionListener to action map
actionMap.put(str, handler);
}
}
/** */
public void detach() {
inputMap.remove(key);
actionMap.remove(str);
}
} // End class Shortcut
private static KeyStroke assembleKeystrokes(final String[] keys0) {
int[] inputEvents = new int[2];
int modifier = 0;
int keyEvent = 0;
inputEvents[0] = 0;
inputEvents[1] = 0;
// If CTRL or SHIFT is pressed, it must be passed as a modifier,
// in the form of an input event object. So, first test if these were pressed.
// ALT shortcuts will be ignored.
for (final String s : keys0) {
if (s.equals("16")) { inputEvents[0] = 16; }
else if (s.equals("17")) { inputEvents[1] = 17; }
else {
keyEvent = Integer.valueOf(s);
}
}
// Then, convert to InputEvent.
if (inputEvents[0] == 16 && inputEvents[1] != 17) {
modifier = InputEvent.SHIFT_DOWN_MASK;
}
else if (inputEvents[0] != 16 && inputEvents[1] == 17) {
modifier = InputEvent.CTRL_DOWN_MASK;
}
else if (inputEvents[0] != 0 && inputEvents[1] != 0) {
modifier = InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK;
}
return KeyStroke.getKeyStroke(keyEvent, modifier);
}
/**
* - Adds keycode to list stored in name of a text field.
* - Code is not added if already in list.
* - Backspace removes last code in list.
* - Sets text of text field with character equivalent of keycodes.
*
* @param e &emsp; KeyEvent
*/
public static void addKeyCode(final KeyEvent e) {
final KeyboardShortcutField ksf = (KeyboardShortcutField) e.getSource();
final String newCode = Integer.toString(e.getKeyCode());
final String codestring = ksf.getCodeString();
List<String> existingCodes;
if (codestring != null) {
existingCodes = new ArrayList<String>(Arrays.asList(codestring.split(" ")));
} else {
existingCodes = new ArrayList<String>();
}
// Backspace (8) will remove last code from list.
if (e.getKeyCode() == 8) {
existingCodes.remove(existingCodes.size() - 1);
} else if (!existingCodes.contains(newCode)) {
existingCodes.add(newCode);
}
ksf.setCodeString(StringUtils.join(existingCodes, ' '));
}
}

View File

@@ -0,0 +1,85 @@
package forge.control;
import forge.Singletons;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.List;
/**
* Restarts a java app.
* Credit: http://leolewis.website.org/wordpress/2011/07/06/programmatically-restart-a-java-application/
*/
public class RestartUtil {
/**
* Sun property pointing the main class and its arguments.
* Might not be defined on non Hotspot VM implementations.
*/
public static final String SUN_JAVA_COMMAND = "sun.java.command";
/**
* Restart the current Java application.
* @param runBeforeRestart some custom code to be run before restarting
*/
public static void restartApplication(final Runnable runBeforeRestart) {
if (!Singletons.getControl().canExitForge(true)) {
return;
}
try {
// java binary
final String java = System.getProperty("java.home")
+ File.separator + "bin" + File.separator + "java";
// vm arguments
final List<String> vmArguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
final StringBuffer vmArgsOneLine = new StringBuffer();
for (final String arg : vmArguments) {
// if it's the agent argument : we ignore it otherwise the
// address of the old application and the new one will be in conflict
if (!arg.contains("-agentlib")) {
vmArgsOneLine.append(arg);
vmArgsOneLine.append(" ");
}
}
// init the command to execute, add the vm args
final StringBuffer cmd = new StringBuffer("\"" + java + "\" " + vmArgsOneLine);
// program main and program arguments
final String[] mainCommand = System.getProperty(SUN_JAVA_COMMAND).split(" ");
// program main is a jar
if (mainCommand[0].endsWith(".jar")) {
// if it's a jar, add -jar mainJar
cmd.append("-jar " + new File(mainCommand[0]).getPath());
} else {
// else it's a .class, add the classpath and mainClass
cmd.append("-cp \"" + System.getProperty("java.class.path") + "\" " + mainCommand[0]);
}
// finally add program arguments
for (int i = 1; i < mainCommand.length; i++) {
cmd.append(" ");
cmd.append(mainCommand[i]);
}
// execute the command in a shutdown hook, to be sure that all the
// resources have been disposed before restarting the application
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
Runtime.getRuntime().exec(cmd.toString());
} catch (final IOException e) {
//e.printStackTrace();
}
}
});
// execute some custom code before restarting
if (runBeforeRestart != null) {
runBeforeRestart.run();
}
// exit
System.exit(0);
} catch (final Exception ex) {
//ErrorViewer.showError(ex, "Restart \"%s\" exception", "");
}
}
}

View File

@@ -0,0 +1,3 @@
/** Controller (as in model-view-controller) for Forge. */
package forge.control;

View File

@@ -0,0 +1,71 @@
package forge.deckchooser;
import forge.deck.DeckType;
import forge.gui.MouseUtil;
import forge.toolbox.FComboBoxWrapper;
import forge.toolbox.FSkin;
import forge.toolbox.FComboBox.TextAlignment;
import javax.swing.*;
import java.awt.Cursor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
public class DecksComboBox extends FComboBoxWrapper<DeckType> {
private List<IDecksComboBoxListener> _listeners = new ArrayList<>();
private DeckType selectedDeckType = null;
public DecksComboBox() {
setSkinFont(FSkin.getBoldFont(14));
setTextAlignment(TextAlignment.CENTER);
addActionListener(getDeckTypeComboListener());
}
public void refresh(DeckType deckType) {
setModel(new DefaultComboBoxModel<DeckType>(DeckType.values()));
setSelectedItem(deckType);
}
private ActionListener getDeckTypeComboListener() {
return new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
MouseUtil.setCursor(Cursor.WAIT_CURSOR);
DeckType newDeckType = (DeckType)getSelectedItem();
if (newDeckType != selectedDeckType) {
notifyDeckTypeSelected(newDeckType);
selectedDeckType = newDeckType;
}
MouseUtil.resetCursor();
}
};
}
public synchronized void addListener(IDecksComboBoxListener obj) {
_listeners.add(obj);
}
public synchronized void removeListener(IDecksComboBoxListener obj) {
_listeners.remove(obj);
}
private synchronized void notifyDeckTypeSelected(DeckType deckType) {
if (deckType != null) {
for (IDecksComboBoxListener listener : _listeners) {
listener.deckTypeSelected(new DecksComboBoxEvent(this, deckType));
}
}
}
public DeckType getDeckType() {
return selectedDeckType;
}
public void setDeckType(DeckType valueOf) {
selectedDeckType = valueOf;
setSelectedItem(selectedDeckType);
}
}

View File

@@ -0,0 +1,19 @@
package forge.deckchooser;
import java.util.EventObject;
import forge.deck.DeckType;
@SuppressWarnings("serial")
public class DecksComboBoxEvent extends EventObject {
private final DeckType deckType;
public DecksComboBoxEvent(Object source, DeckType deckType0) {
super(source);
deckType = deckType0;
}
public DeckType getDeckType() {
return deckType;
}
}

View File

@@ -0,0 +1,394 @@
package forge.deckchooser;
import forge.UiCommand;
import forge.deck.Deck;
import forge.deck.DeckProxy;
import forge.deck.DeckType;
import forge.deck.DeckgenUtil;
import forge.game.GameType;
import forge.game.player.RegisteredPlayer;
import forge.itemmanager.DeckManager;
import forge.itemmanager.ItemManagerConfig;
import forge.itemmanager.ItemManagerContainer;
import forge.model.FModel;
import forge.properties.ForgePreferences;
import forge.properties.ForgePreferences.FPref;
import forge.quest.QuestController;
import forge.quest.QuestEvent;
import forge.quest.QuestEventChallenge;
import forge.quest.QuestUtil;
import forge.toolbox.FLabel;
import net.miginfocom.swing.MigLayout;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@SuppressWarnings("serial")
public class FDeckChooser extends JPanel implements IDecksComboBoxListener {
private DecksComboBox decksComboBox;
private DeckType selectedDeckType;
private ItemManagerContainer lstDecksContainer;
private final DeckManager lstDecks = new DeckManager(GameType.Constructed);
private final FLabel btnViewDeck = new FLabel.ButtonBuilder().text("View Deck").fontSize(14).build();
private final FLabel btnRandom = new FLabel.ButtonBuilder().fontSize(14).build();
private boolean isAi;
private final ForgePreferences prefs = FModel.getPreferences();
private FPref stateSetting = null;
public FDeckChooser(boolean forAi) {
setOpaque(false);
isAi = forAi;
UiCommand cmdViewDeck = new UiCommand() {
@Override
public void run() {
if (selectedDeckType != DeckType.COLOR_DECK && selectedDeckType != DeckType.THEME_DECK) {
FDeckViewer.show(getDeck());
}
}
};
lstDecks.setItemActivateCommand(cmdViewDeck);
btnViewDeck.setCommand(cmdViewDeck);
}
public void initialize() {
initialize(DeckType.COLOR_DECK);
}
public void initialize(DeckType defaultDeckType) {
initialize(null, defaultDeckType);
}
public void initialize(FPref savedStateSetting, DeckType defaultDeckType) {
stateSetting = savedStateSetting;
selectedDeckType = defaultDeckType;
}
public DeckType getSelectedDeckType() { return selectedDeckType; }
public void setSelectedDeckType(DeckType selectedDeckType0) {
refreshDecksList(selectedDeckType0, false, null);
}
public DeckManager getLstDecks() { return lstDecks; }
private void updateCustom() {
lstDecks.setAllowMultipleSelections(false);
lstDecks.setPool(DeckProxy.getAllConstructedDecks(FModel.getDecks().getConstructed()));
lstDecks.setup(ItemManagerConfig.CONSTRUCTED_DECKS);
btnRandom.setText("Random Deck");
btnRandom.setCommand(new UiCommand() {
@Override
public void run() {
DeckgenUtil.randomSelect(lstDecks);
}
});
lstDecks.setSelectedIndex(0);
}
private class ColorDeckGenerator extends DeckProxy implements Comparable<ColorDeckGenerator> {
private String name;
private int index;
public ColorDeckGenerator(String name0, int index0) {
super();
name = name0;
this.index = index0;
}
@Override
public String getName() {
return name;
}
@Override
public String toString() {
return name;
}
@Override
public int compareTo(final ColorDeckGenerator d) {
return d instanceof ColorDeckGenerator ? Integer.compare(this.index, ((ColorDeckGenerator)d).index) : 1;
}
@Override
public Deck getDeck() {
List<String> selection = new ArrayList<String>();
for (DeckProxy deck : lstDecks.getSelectedItems()) {
selection.add(deck.getName());
}
if (DeckgenUtil.colorCheck(selection)) {
return DeckgenUtil.buildColorDeck(selection, isAi);
}
return null;
}
@Override
public boolean isGeneratedDeck() {
return true;
}
}
private void updateColors() {
lstDecks.setAllowMultipleSelections(true);
String[] colors = new String[] { "Random 1", "Random 2", "Random 3",
"White", "Blue", "Black", "Red", "Green" };
ArrayList<DeckProxy> decks = new ArrayList<DeckProxy>();
for (int i = 0; i < colors.length; i++) {
decks.add(new ColorDeckGenerator(colors[i], i));
}
lstDecks.setPool(decks);
lstDecks.setup(ItemManagerConfig.STRING_ONLY);
btnRandom.setText("Random Colors");
btnRandom.setCommand(new UiCommand() {
@Override
public void run() {
DeckgenUtil.randomSelectColors(lstDecks);
}
});
// default selection = basic two color deck
lstDecks.setSelectedIndices(new Integer[]{0, 1});
}
private void updateThemes() {
lstDecks.setAllowMultipleSelections(false);
lstDecks.setPool(DeckProxy.getAllThemeDecks());
lstDecks.setup(ItemManagerConfig.STRING_ONLY);
btnRandom.setText("Random Deck");
btnRandom.setCommand(new UiCommand() {
@Override
public void run() {
DeckgenUtil.randomSelect(lstDecks);
}
});
lstDecks.setSelectedIndex(0);
}
private void updatePrecons() {
lstDecks.setAllowMultipleSelections(false);
lstDecks.setPool(DeckProxy.getAllPreconstructedDecks(QuestController.getPrecons()));
lstDecks.setup(ItemManagerConfig.PRECON_DECKS);
btnRandom.setText("Random Deck");
btnRandom.setCommand(new UiCommand() {
@Override
public void run() {
DeckgenUtil.randomSelect(lstDecks);
}
});
lstDecks.setSelectedIndex(0);
}
private void updateQuestEvents() {
lstDecks.setAllowMultipleSelections(false);
lstDecks.setPool(DeckProxy.getAllQuestEventAndChallenges());
lstDecks.setup(ItemManagerConfig.QUEST_EVENT_DECKS);
btnRandom.setText("Random Deck");
btnRandom.setCommand(new UiCommand() {
@Override
public void run() {
DeckgenUtil.randomSelect(lstDecks);
}
});
lstDecks.setSelectedIndex(0);
}
public Deck getDeck() {
DeckProxy proxy = lstDecks.getSelectedItem();
return proxy.getDeck();
}
/** Generates deck from current list selection(s). */
public RegisteredPlayer getPlayer() {
if (lstDecks.getSelectedIndex() < 0) { return null; }
// Special branch for quest events
if (selectedDeckType == DeckType.QUEST_OPPONENT_DECK) {
QuestEvent event = DeckgenUtil.getQuestEvent(lstDecks.getSelectedItem().getName());
RegisteredPlayer result = new RegisteredPlayer(event.getEventDeck());
if (event instanceof QuestEventChallenge) {
result.setStartingLife(((QuestEventChallenge) event).getAiLife());
}
result.setCardsOnBattlefield(QuestUtil.getComputerStartingCards(event));
return result;
}
return new RegisteredPlayer(getDeck());
}
public void populate() {
if (decksComboBox == null) { //initialize components with delayed initialization the first time this is populated
decksComboBox = new DecksComboBox();
lstDecksContainer = new ItemManagerContainer(lstDecks);
restoreSavedState();
decksComboBox.addListener(this);
}
else {
removeAll();
restoreSavedState(); //ensure decks refreshed and state restored in case any deleted or added since last loaded
}
this.setLayout(new MigLayout("insets 0, gap 0"));
decksComboBox.addTo(this, "w 100%, h 30px!, gapbottom 5px, spanx 2, wrap");
this.add(lstDecksContainer, "w 100%, growy, pushy, spanx 2, wrap");
this.add(btnViewDeck, "w 50%-3px, h 30px!, gaptop 5px, gapright 6px");
this.add(btnRandom, "w 50%-3px, h 30px!, gaptop 5px");
if (isShowing()) {
revalidate();
repaint();
}
}
public final boolean isAi() {
return isAi;
}
public void setIsAi(boolean isAiDeck) {
this.isAi = isAiDeck;
}
/* (non-Javadoc)
* @see forge.gui.deckchooser.IDecksComboBoxListener#deckTypeSelected(forge.gui.deckchooser.DecksComboBoxEvent)
*/
@Override
public void deckTypeSelected(DecksComboBoxEvent ev) {
refreshDecksList(ev.getDeckType(), false, ev);
}
private void refreshDecksList(DeckType deckType, boolean forceRefresh, DecksComboBoxEvent ev) {
if (selectedDeckType == deckType && !forceRefresh) { return; }
selectedDeckType = deckType;
if (ev == null) {
decksComboBox.refresh(deckType);
}
lstDecks.setCaption(deckType.toString());
switch (deckType) {
case CUSTOM_DECK:
updateCustom();
break;
case COLOR_DECK:
updateColors();
break;
case THEME_DECK:
updateThemes();
break;
case QUEST_OPPONENT_DECK:
updateQuestEvents();
break;
case PRECONSTRUCTED_DECK:
updatePrecons();
break;
}
}
private final String SELECTED_DECK_DELIMITER = "::";
public void saveState() {
if (stateSetting == null) {
throw new NullPointerException("State setting missing. Specify first using the initialize() method.");
}
prefs.setPref(stateSetting, getState());
prefs.save();
}
private String getState() {
String deckType = decksComboBox.getDeckType().name();
StringBuilder state = new StringBuilder(deckType);
state.append(";");
joinSelectedDecks(state, SELECTED_DECK_DELIMITER);
return state.toString();
}
private void joinSelectedDecks(StringBuilder state, String delimiter) {
Iterable<DeckProxy> selectedDecks = lstDecks.getSelectedItems();
boolean isFirst = true;
if (selectedDecks != null) {
for (DeckProxy deck : selectedDecks) {
if (isFirst) {
isFirst = false;
}
else {
state.append(delimiter);
}
state.append(deck.toString());
}
}
}
/** Returns a clean name from the state that can be used for labels. */
public final String getStateForLabel() {
String deckType = decksComboBox.getDeckType().toString();
StringBuilder state = new StringBuilder(deckType);
state.append(": ");
joinSelectedDecks(state, ", ");
return state.toString();
}
private void restoreSavedState() {
if (stateSetting == null) {
//if can't restore saved state, just refresh deck list
refreshDecksList(selectedDeckType, true, null);
return;
}
String savedState = prefs.getPref(stateSetting);
refreshDecksList(getDeckTypeFromSavedState(savedState), true, null);
lstDecks.setSelectedStrings(getSelectedDecksFromSavedState(savedState));
}
private DeckType getDeckTypeFromSavedState(String savedState) {
try {
if (StringUtils.isBlank(savedState)) {
return selectedDeckType;
}
else {
return DeckType.valueOf(savedState.split(";")[0]);
}
}
catch (IllegalArgumentException ex) {
System.err.println(ex.getMessage() + ". Using default : " + selectedDeckType);
return selectedDeckType;
}
}
private List<String> getSelectedDecksFromSavedState(String savedState) {
try {
if (StringUtils.isBlank(savedState)) {
return new ArrayList<String>();
}
else {
return Arrays.asList(savedState.split(";")[1].split(SELECTED_DECK_DELIMITER));
}
}
catch (Exception ex) {
System.err.println(ex + " [savedState=" + savedState + "]");
return new ArrayList<String>();
}
}
public DecksComboBox getDecksComboBox() {
return decksComboBox;
}
}

View File

@@ -0,0 +1,185 @@
package forge.deckchooser;
import forge.deck.CardPool;
import forge.deck.Deck;
import forge.deck.DeckSection;
import forge.game.card.Card;
import forge.gui.CardDetailPanel;
import forge.gui.CardPicturePanel;
import forge.item.PaperCard;
import forge.itemmanager.CardManager;
import forge.itemmanager.ItemManagerConfig;
import forge.itemmanager.ItemManagerContainer;
import forge.itemmanager.ItemManagerModel;
import forge.itemmanager.views.*;
import forge.toolbox.FButton;
import forge.toolbox.FOptionPane;
import forge.view.FDialog;
import net.miginfocom.swing.MigLayout;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
@SuppressWarnings("serial")
public class FDeckViewer extends FDialog {
private final Deck deck;
private final List<DeckSection> sections = new ArrayList<DeckSection>();
private final CardManager cardManager;
private DeckSection currentSection;
private final CardDetailPanel cardDetail = new CardDetailPanel(null);
private final CardPicturePanel cardPicture = new CardPicturePanel();
private final FButton btnCopyToClipboard = new FButton("Copy to Clipboard");
private final FButton btnChangeSection = new FButton("Change Section");
private final FButton btnClose = new FButton("Close");
public static void show(final Deck deck) {
if (deck == null) { return; }
FDeckViewer deckViewer = new FDeckViewer(deck);
deckViewer.setVisible(true);
deckViewer.dispose();
}
private FDeckViewer(Deck deck0) {
this.deck = deck0;
this.setTitle(deck.getName());
this.cardManager = new CardManager(false) {
@Override //show hovered card in Image View in dialog instead of main Detail/Picture panes
protected ImageView<PaperCard> createImageView(final ItemManagerModel<PaperCard> model0) {
return new ImageView<PaperCard>(this, model0) {
@Override
protected void showHoveredItem(PaperCard item) {
Card card = Card.getCardForUi(item);
if (card == null) { return; }
cardDetail.setCard(card);
cardPicture.setCard(card, true);
}
};
}
};
this.cardManager.setPool(deck.getMain());
this.cardManager.addSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
PaperCard paperCard = cardManager.getSelectedItem();
if (paperCard == null) { return; }
Card card = Card.getCardForUi(paperCard);
if (card == null) { return; }
cardDetail.setCard(card);
cardPicture.setCard(card, true);
}
});
for (Entry<DeckSection, CardPool> entry : deck) {
this.sections.add(entry.getKey());
}
this.currentSection = DeckSection.Main;
updateCaption();
this.btnCopyToClipboard.setFocusable(false);
this.btnCopyToClipboard.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
FDeckViewer.this.copyToClipboard();
}
});
this.btnChangeSection.setFocusable(false);
if (this.sections.size() > 1) {
this.btnChangeSection.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
FDeckViewer.this.changeSection();
}
});
}
else {
this.btnChangeSection.setEnabled(false);
}
this.btnClose.setFocusable(false);
this.btnClose.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
FDeckViewer.this.setVisible(false);
}
});
final int width = 800;
final int height = 600;
this.setPreferredSize(new Dimension(width, height));
this.setSize(width, height);
this.cardPicture.setOpaque(false);
JPanel cardPanel = new JPanel(new MigLayout("insets 0, gap 0, wrap"));
cardPanel.setOpaque(false);
cardPanel.add(this.cardDetail, "w 225px, h 240px, gapbottom 10px");
cardPanel.add(this.cardPicture, "w 225px, h 350px, gapbottom 10px");
JPanel buttonPanel = new JPanel(new MigLayout("insets 0, gap 0"));
buttonPanel.setOpaque(false);
buttonPanel.add(this.btnCopyToClipboard, "w 200px!, h 26px!, gapright 5px");
buttonPanel.add(this.btnChangeSection, "w 200px!, h 26px!");
this.add(new ItemManagerContainer(this.cardManager), "push, grow, gapright 10px, gapbottom 10px");
this.add(cardPanel, "wrap");
this.add(buttonPanel);
this.add(this.btnClose, "w 120px!, h 26px!, ax right");
this.cardManager.setup(ItemManagerConfig.DECK_VIEWER);
this.setDefaultFocus(this.cardManager.getCurrentView().getComponent());
}
private void changeSection() {
int index = sections.indexOf(currentSection);
index = (index + 1) % sections.size();
currentSection = sections.get(index);
this.cardManager.setPool(this.deck.get(currentSection));
updateCaption();
}
private void updateCaption() {
this.cardManager.setCaption(deck.getName() + " - " + currentSection.name());
}
private void copyToClipboard() {
final String nl = System.getProperty("line.separator");
final StringBuilder deckList = new StringBuilder();
final String dName = deck.getName();
deckList.append(dName == null ? "" : dName + nl + nl);
for (DeckSection s : DeckSection.values()){
CardPool cp = deck.get(s);
if (cp == null || cp.isEmpty()) {
continue;
}
deckList.append(s.toString()).append(": ");
if (s.isSingleCard()) {
deckList.append(cp.get(0).getName()).append(nl);
}
else {
deckList.append(nl);
for (final Entry<PaperCard, Integer> ev : cp) {
deckList.append(ev.getValue()).append(" ").append(ev.getKey()).append(nl);
}
}
deckList.append(nl);
}
final StringSelection ss = new StringSelection(deckList.toString());
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
FOptionPane.showMessageDialog("Deck list for '" + deck.getName() + "' copied to clipboard.");
}
}

View File

@@ -0,0 +1,5 @@
package forge.deckchooser;
public interface IDecksComboBoxListener {
public void deckTypeSelected(DecksComboBoxEvent ev);
}

View File

@@ -0,0 +1,92 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.download;
import forge.assets.ImageUtil;
import forge.card.CardRules;
import forge.item.PaperCard;
import forge.model.FModel;
import forge.properties.ForgeConstants;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.util.Map;
import java.util.TreeMap;
@SuppressWarnings("serial")
public class GuiDownloadPicturesLQ extends GuiDownloader {
public GuiDownloadPicturesLQ() {
super();
}
@Override
protected final Map<String, String> getNeededImages() {
Map<String, String> downloads = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
for (PaperCard c : FModel.getMagicDb().getCommonCards().getAllCards()) {
addDLObject(c, downloads, false);
if (ImageUtil.hasBackFacePicture(c)) {
addDLObject(c, downloads, true);
}
}
for (PaperCard c : FModel.getMagicDb().getVariantCards().getAllCards()) {
addDLObject(c, downloads, false);
}
// Add missing tokens to the list of things to download.
addMissingItems(downloads, ForgeConstants.IMAGE_LIST_TOKENS_FILE, ForgeConstants.CACHE_TOKEN_PICS_DIR);
return downloads;
}
private void addDLObject(PaperCard c, Map<String, String> downloads, boolean backFace) {
CardRules cardRules = c.getRules();
String urls = cardRules.getPictureUrl(backFace);
if (StringUtils.isEmpty(urls)) {
return;
}
String filename = ImageUtil.getImageKey(c, backFace, false);
File destFile = new File(ForgeConstants.CACHE_CARD_PICS_DIR, filename + ".jpg");
if (destFile.exists())
return;
filename = destFile.getAbsolutePath();
if (downloads.containsKey(filename)) {
return;
}
final String urlToDownload;
int urlIndex = 0;
int allUrlsLen = 1;
if (urls.indexOf("\\") < 0)
urlToDownload = urls;
else {
String[] allUrls = urls.split("\\\\");
allUrlsLen = allUrls.length;
urlIndex = (c.getArtIndex()-1) % allUrlsLen;
urlToDownload = allUrls[urlIndex];
}
//System.out.println(c.getName() + "|" + c.getEdition() + " - " + c.getArtIndex() + " -> " + urlIndex + "/" + allUrlsLen + " === " + filename + " <<< " + urlToDownload);
downloads.put(destFile.getAbsolutePath(), urlToDownload);
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.download;
import forge.properties.ForgeConstants;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("serial")
public class GuiDownloadPrices extends GuiDownloader {
public GuiDownloadPrices() {
super();
}
@Override
protected Map<String, String> getNeededImages() {
Map<String, String> result = new HashMap<String, String>();
result.put(ForgeConstants.QUEST_CARD_PRICE_FILE, ForgeConstants.URL_PRICE_DOWNLOAD);
return result;
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.download;
import forge.properties.ForgeConstants;
import java.util.Map;
import java.util.TreeMap;
/** */
@SuppressWarnings("serial")
public class GuiDownloadQuestImages extends GuiDownloader {
/**
* <p>
* Constructor for GuiDownloadQuestImages.
* </p>
*/
public GuiDownloadQuestImages() {
super();
}
/**
* <p>
* getNeededCards.
* </p>
*
* @return an array of {@link forge.download.GuiDownloadSetPicturesLQ} objects.
*/
@Override
protected final Map<String, String> getNeededImages() {
// read all card names and urls
final Map<String, String> urls = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
addMissingItems(urls, ForgeConstants.IMAGE_LIST_QUEST_OPPONENT_ICONS_FILE, ForgeConstants.CACHE_ICON_PICS_DIR);
addMissingItems(urls, ForgeConstants.IMAGE_LIST_QUEST_PET_SHOP_ICONS_FILE, ForgeConstants.CACHE_ICON_PICS_DIR);
addMissingItems(urls, ForgeConstants.IMAGE_LIST_QUEST_BOOSTERS_FILE, ForgeConstants.CACHE_BOOSTER_PICS_DIR);
addMissingItems(urls, ForgeConstants.IMAGE_LIST_QUEST_FATPACKS_FILE, ForgeConstants.CACHE_FATPACK_PICS_DIR);
addMissingItems(urls, ForgeConstants.IMAGE_LIST_QUEST_PRECONS_FILE, ForgeConstants.CACHE_PRECON_PICS_DIR);
addMissingItems(urls, ForgeConstants.IMAGE_LIST_QUEST_TOURNAMENTPACKS_FILE, ForgeConstants.CACHE_TOURNAMENTPACK_PICS_DIR);
addMissingItems(urls, ForgeConstants.IMAGE_LIST_QUEST_TOKENS_FILE, ForgeConstants.CACHE_TOKEN_PICS_DIR);
return urls;
}
}

View File

@@ -0,0 +1,70 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.download;
import com.google.common.collect.Iterables;
import forge.assets.ImageUtil;
import forge.card.CardEdition;
import forge.item.PaperCard;
import forge.model.FModel;
import forge.properties.ForgeConstants;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.util.Map;
import java.util.TreeMap;
@SuppressWarnings("serial")
public class GuiDownloadSetPicturesLQ extends GuiDownloader {
public GuiDownloadSetPicturesLQ() {
super();
}
@Override
protected final Map<String, String> getNeededImages() {
Map<String, String> downloads = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
for (final PaperCard c : Iterables.concat(FModel.getMagicDb().getCommonCards().getAllCards(), FModel.getMagicDb().getVariantCards().getAllCards())) {
final String setCode3 = c.getEdition();
if (StringUtils.isBlank(setCode3) || CardEdition.UNKNOWN.getCode().equals(setCode3)) {
// we don't want cards from unknown sets
continue;
}
addDLObject(ImageUtil.getDownloadUrl(c, false), ImageUtil.getImageKey(c, false, true), downloads);
if (ImageUtil.hasBackFacePicture(c)) {
addDLObject(ImageUtil.getDownloadUrl(c, true), ImageUtil.getImageKey(c, true, true), downloads);
}
}
// Add missing tokens to the list of things to download.
addMissingItems(downloads, ForgeConstants.IMAGE_LIST_TOKENS_FILE, ForgeConstants.CACHE_TOKEN_PICS_DIR);
return downloads;
}
private void addDLObject(String urlPath, String filename, Map<String, String> downloads) {
File destFile = new File(ForgeConstants.CACHE_CARD_PICS_DIR, filename + ".jpg");
// System.out.println(filename);
if (!destFile.exists()) {
downloads.put(destFile.getAbsolutePath(), ForgeConstants.URL_PIC_DOWNLOAD + urlPath);
}
}
}

View File

@@ -0,0 +1,363 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.download;
import com.esotericsoftware.minlog.Log;
import forge.UiCommand;
import forge.ImageCache;
import forge.assets.FSkinProp;
import forge.error.BugReporter;
import forge.gui.SOverlayUtils;
import forge.toolbox.*;
import forge.util.FileUtil;
import forge.util.MyRandom;
import net.miginfocom.swing.MigLayout;
import org.apache.commons.lang3.tuple.Pair;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.*;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
@SuppressWarnings("serial")
public abstract class GuiDownloader extends DefaultBoundedRangeModel implements Runnable {
public static final Proxy.Type[] TYPES = Proxy.Type.values();
// Actions and commands
private final ActionListener actStartDownload = new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
// invalidate image cache so newly downloaded images will be loaded
ImageCache.clear();
new Thread(GuiDownloader.this).start();
btnStart.setEnabled(false);
}
};
private final ActionListener actOK = new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
close();
}
};
private final UiCommand cmdClose = new UiCommand() { @Override
public void run() { close(); } };
// Swing components
private final FPanel pnlDialog = new FPanel(new MigLayout("insets 0, gap 0, wrap, ax center, ay center"));
private final FProgressBar barProgress = new FProgressBar();
private final FButton btnStart = new FButton("Start");
private final JTextField txfAddr = new JTextField("Proxy Address");
private final JTextField txfPort = new JTextField("Proxy Port");
private final FLabel btnClose = new FLabel.Builder().text("X")
.hoverable(true).fontAlign(SwingConstants.CENTER).cmdClick(cmdClose).build();
private final JRadioButton radProxyNone = new FRadioButton("No Proxy");
private final JRadioButton radProxySocks = new FRadioButton("SOCKS Proxy");
private final JRadioButton radProxyHTTP = new FRadioButton("HTTP Proxy");
// Proxy info
private int type;
// Progress variables
private Map<String, String> cards; // local path -> url
private boolean cancel;
private final long[] times = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
private int tptr = 0;
private int skipped = 0;
private long lTime = System.currentTimeMillis();
protected GuiDownloader() {
String radConstraints = "w 100%!, h 30px!, gap 2% 0 0 10px";
JXButtonPanel grpPanel = new JXButtonPanel();
grpPanel.add(radProxyNone, radConstraints);
grpPanel.add(radProxyHTTP, radConstraints);
grpPanel.add(radProxySocks, radConstraints);
radProxyNone.addChangeListener(new ProxyHandler(0));
radProxyHTTP.addChangeListener(new ProxyHandler(1));
radProxySocks.addChangeListener(new ProxyHandler(2));
radProxyNone.setSelected(true);
btnClose.setBorder(new FSkin.LineSkinBorder(FSkin.getColor(FSkin.Colors.CLR_TEXT)));
btnStart.setFont(FSkin.getFont(18));
btnStart.setVisible(false);
barProgress.reset();
barProgress.setString("Scanning for existing items...");
pnlDialog.setBackgroundTexture(FSkin.getIcon(FSkinProp.BG_TEXTURE));
// Layout
pnlDialog.add(grpPanel, "w 50%!");
pnlDialog.add(txfAddr, "w 95%!, h 30px!, gap 2% 0 0 10px");
pnlDialog.add(txfPort, "w 95%!, h 30px!, gap 2% 0 0 10px");
pnlDialog.add(barProgress, "w 95%!, h 40px!, gap 2% 0 20px 0");
pnlDialog.add(btnStart, "w 200px!, h 40px!, gap 0 0 20px 0, ax center");
pnlDialog.add(btnClose, "w 20px!, h 20px!, pos 370px 10px");
final JPanel pnl = FOverlay.SINGLETON_INSTANCE.getPanel();
pnl.removeAll();
pnl.setLayout(new MigLayout("insets 0, gap 0, wrap, ax center, ay center"));
pnl.add(pnlDialog, "w 400px!, h 350px!, ax center, ay center");
SOverlayUtils.showOverlay();
// Free up the EDT by assembling card list in the background
SwingWorker<Void, Void> thrGetImages = new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
try {
GuiDownloader.this.cards = GuiDownloader.this.getNeededImages();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void done() {
GuiDownloader.this.readyToStart();
}
};
thrGetImages.execute();
}
private void readyToStart() {
if (this.cards.isEmpty()) {
barProgress.setString("All items have been downloaded.");
btnStart.setText("OK");
btnStart.addActionListener(actOK);
} else {
barProgress.setMaximum(this.cards.size());
barProgress.setString(this.cards.size() == 1 ? "1 item found." : this.cards.size() + " items found.");
//for(Entry<String, String> kv : cards.entrySet()) System.out.printf("Will get %s from %s%n", kv.getKey(), kv.getValue());
btnStart.addActionListener(actStartDownload);
}
btnStart.setVisible(true);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
btnStart.requestFocusInWindow();
}
});
}
private void setCancel(final boolean cancel) {
this.cancel = cancel;
}
private void close() {
setCancel(true);
// Kill overlay
SOverlayUtils.hideOverlay();
}
protected final int getAverageTimePerObject() {
int numNonzero = 10;
if (this.tptr > 9) {
this.tptr = 0;
}
this.times[this.tptr] = System.currentTimeMillis() - this.lTime;
this.lTime = System.currentTimeMillis();
int tTime = 0;
for (int i = 0; i < 10; i++) {
tTime += this.times[i];
if (this.times[i] == 0) {
numNonzero--;
}
}
this.tptr++;
return tTime / Math.max(1, numNonzero);
}
private void update(final int card, final File dest) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
GuiDownloader.this.fireStateChanged();
final StringBuilder sb = new StringBuilder();
final int a = GuiDownloader.this.getAverageTimePerObject();
if (card != GuiDownloader.this.cards.size()) {
sb.append(card + "/" + GuiDownloader.this.cards.size() + " - ");
long t2Go = (GuiDownloader.this.cards.size() - card) * a;
if (t2Go > 3600000) {
sb.append(String.format("%02d:", t2Go / 3600000));
t2Go = t2Go % 3600000;
}
if (t2Go > 60000) {
sb.append(String.format("%02d:", t2Go / 60000));
t2Go = t2Go % 60000;
} else {
sb.append("00:");
}
sb.append(String.format("%02d remaining.", t2Go / 1000));
} else {
sb.append(String.format("%d of %d items finished! Skipped " + skipped + " items. Please close!",
card, GuiDownloader.this.cards.size()));
btnStart.setText("OK");
btnStart.addActionListener(actOK);
btnStart.setEnabled(true);
btnStart.requestFocusInWindow();
}
GuiDownloader.this.barProgress.setString(sb.toString());
System.out.println(card + "/" + GuiDownloader.this.cards.size() + " - " + dest);
}
});
}
@Override
public final void run() {
final Random r = MyRandom.getRandom();
Proxy p = null;
if (this.type == 0) {
p = Proxy.NO_PROXY;
} else {
try {
p = new Proxy(GuiDownloader.TYPES[this.type], new InetSocketAddress(this.txfAddr.getText(),
Integer.parseInt(this.txfPort.getText())));
} catch (final Exception ex) {
BugReporter.reportException(ex,
"Proxy connection could not be established!\nProxy address: %s\nProxy port: %s",
this.txfAddr.getText(), this.txfPort.getText());
return;
}
}
int iCard = 0;
for(Entry<String, String> kv : cards.entrySet()) {
if( cancel )
break;
String url = kv.getValue();
final File fileDest = new File(kv.getKey());
final File base = fileDest.getParentFile();
ReadableByteChannel rbc = null;
FileOutputStream fos = null;
try {
// test for folder existence
if (!base.exists() && !base.mkdir()) { // create folder if not found
System.out.println("Can't create folder" + base.getAbsolutePath());
}
URL imageUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection) imageUrl.openConnection(p);
// don't allow redirections here -- they indicate 'file not found' on the server
conn.setInstanceFollowRedirects(false);
conn.connect();
if (conn.getResponseCode() != 200) {
conn.disconnect();
System.out.println("Skipped Download for: " + fileDest.getPath());
update(++iCard, fileDest);
skipped++;
continue;
}
rbc = Channels.newChannel(conn.getInputStream());
fos = new FileOutputStream(fileDest);
fos.getChannel().transferFrom(rbc, 0, 1 << 24);
} catch (final ConnectException ce) {
System.out.println("Connection refused for url: " + url);
} catch (final MalformedURLException mURLe) {
System.out.println("Error - possibly missing URL for: " + fileDest.getName());
} catch (final FileNotFoundException fnfe) {
String formatStr = "Error - the LQ picture %s could not be found on the server. [%s] - %s";
System.out.println(String.format(formatStr, fileDest.getName(), url, fnfe.getMessage()));
} catch (final Exception ex) {
Log.error("LQ Pictures", "Error downloading pictures", ex);
} finally {
if (null != rbc) {
try { rbc.close(); } catch (IOException e) { System.out.println("error closing input stream"); }
}
if (null != fos) {
try { fos.close(); } catch (IOException e) { System.out.println("error closing output stream"); }
}
}
update(++iCard, fileDest);
// throttle to reduce load on the server
try {
Thread.sleep(r.nextInt(50) + 50);
} catch (final InterruptedException e) {
Log.error("GuiDownloader", "Sleep Error", e);
}
} // for
}
protected abstract Map<String, String> getNeededImages();
protected static void addMissingItems(Map<String, String> list, String nameUrlFile, String dir) {
for (Pair<String, String> nameUrlPair : FileUtil.readNameUrlFile(nameUrlFile)) {
File f = new File(dir, nameUrlPair.getLeft());
//System.out.println(f.getAbsolutePath());
if (!f.exists()) {
list.put(f.getAbsolutePath(), nameUrlPair.getRight());
}
}
}
protected class ProxyHandler implements ChangeListener {
private final int type;
public ProxyHandler(final int type) {
this.type = type;
}
@Override
public final void stateChanged(final ChangeEvent e) {
if (((AbstractButton) e.getSource()).isSelected()) {
GuiDownloader.this.type = this.type;
GuiDownloader.this.txfAddr.setEnabled(this.type != 0);
GuiDownloader.this.txfPort.setEnabled(this.type != 0);
}
}
}
}

View File

@@ -0,0 +1,3 @@
/** Forge Card Game. */
package forge.download;

View File

@@ -0,0 +1,324 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.error;
import forge.FThreads;
import forge.gui.WrapLayout;
import forge.toolbox.FHyperlink;
import forge.toolbox.FLabel;
import forge.toolbox.FOptionPane;
import forge.util.BuildInfo;
import net.miginfocom.swing.MigLayout;
import org.apache.commons.lang3.StringUtils;
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.ArrayList;
import java.util.Map;
import java.util.Map.Entry;
/**
* The class ErrorViewer. Enables showing and saving error messages that
* occurred in forge.
*
* @author Clemens Koza
* @version V1.0 02.08.2009
*/
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;
}
if (message != null) {
System.err.printf("%s > %s%n", FThreads.debugGetCurrThreadId(), message);
}
System.err.print( FThreads.debugGetCurrThreadId() + " > " );
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(FThreads.debugGetCurrThreadId()).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<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces();
for (final Entry<Thread, StackTraceElement[]> 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 = "<html>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.</html>";
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("<html>" + word + "</html>").useSkinColors(false).build());
}
helpPanel.add(new FHyperlink.Builder().url(helpUrl).text("<html>this post</html>").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<Object> options = new ArrayList<Object>();
options.add(new JButton(new _CopyAndGo(url, area)));
options.add(new JButton(new _SaveAction(area)));
options.add("Close");
if (showExitAppBtn) {
options.add(new JButton(new _ExitAction()));
}
JOptionPane pane = new JOptionPane(p, JOptionPane.PLAIN_MESSAGE,
JOptionPane.DEFAULT_OPTION, null, options.toArray(), options.get(0));
JDialog dlg = pane.createDialog(JOptionPane.getRootFrame(), title);
dlg.setSize(showExitAppBtn ? 780 : 600, 400);
dlg.setResizable(true);
dialogShown = true;
dlg.setVisible(true);
dlg.dispose();
dialogShown = false;
}
@SuppressWarnings("serial")
private static class _CopyAndGo extends AbstractAction {
private final String url;
private final JTextArea text;
public _CopyAndGo(String url, JTextArea text) {
super("Copy and go to forum");
this.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
this.url = url;
this.text = text;
}
@Override
public void actionPerformed(final ActionEvent e) {
try {
// copy text to clipboard
StringSelection ss = new StringSelection(text.getText());
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
// browse to url
Desktop.getDesktop().browse(new URI(url));
}
catch (Exception ex) {
FOptionPane.showMessageDialog("Sorry, a problem occurred while opening the forum in your default browser.",
"A problem occurred", FOptionPane.ERROR_ICON);
}
}
}
@SuppressWarnings("serial")
private static class _SaveAction extends AbstractAction {
private static JFileChooser c;
private final JTextArea area;
public _SaveAction(final JTextArea areaParam) {
super("Save to file");
this.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
this.area = areaParam;
}
@Override
public void actionPerformed(final ActionEvent e) {
if (c == null) {
c = new JFileChooser();
}
File f;
long curTime = System.currentTimeMillis();
for (int i = 0;; i++) {
final String name = String.format("%TF-%02d.txt", curTime, i);
f = new File(name);
if (!f.exists()) {
break;
}
}
c.setSelectedFile(f);
c.showSaveDialog(null);
f = c.getSelectedFile();
try {
final BufferedWriter bw = new BufferedWriter(new FileWriter(f));
bw.write(this.area.getText());
bw.close();
}
catch (final IOException ex) {
FOptionPane.showMessageDialog("There was an error during saving. Sorry!\n" + ex,
"Error saving file", FOptionPane.ERROR_ICON);
}
}
}
@SuppressWarnings("serial")
private static class _ExitAction extends AbstractAction {
public _ExitAction() {
super("Exit application");
this.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_X, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
}
@Override
public void actionPerformed(final ActionEvent e) {
System.exit(0);
}
}
// disable instantiation
private BugReporter() { }
}

View File

@@ -0,0 +1,3 @@
/** Forge Card Game. */
package forge.error;

View File

@@ -0,0 +1,50 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.gui;
import forge.game.card.Card;
/**
* The class CardContainer. A card container is an object that references a
* card.
*
* @author Clemens Koza
* @version V0.0 17.02.2010
*/
public interface CardContainer {
/**
* <p>
* setCard.
* </p>
*
* @param card
* a {@link forge.game.card.Card} object.
*/
void setCard(Card card);
/**
* <p>
* getCard.
* </p>
*
* @return a {@link forge.game.card.Card} object.
*/
Card getCard();
}

View File

@@ -0,0 +1,700 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.gui;
import forge.Singletons;
import forge.card.CardCharacteristicName;
import forge.card.CardEdition;
import forge.card.CardType;
import forge.card.ColorSet;
import forge.game.GameEntity;
import forge.game.card.Card;
import forge.game.card.CardUtil;
import forge.game.card.CounterType;
import forge.game.player.Player;
import forge.game.zone.ZoneType;
import forge.item.IPaperCard;
import forge.item.InventoryItemFromSet;
import forge.item.PreconDeck;
import forge.item.SealedProduct;
import forge.model.FModel;
import forge.toolbox.*;
import forge.toolbox.FSkin.SkinnedPanel;
import forge.util.Lang;
import forge.view.FDialog;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.util.ArrayList;
import java.util.Iterator;
/**
* The class CardDetailPanel. Shows the details of a card.
*
* @author Clemens Koza
* @version V0.0 17.02.2010
*/
public class CardDetailPanel extends SkinnedPanel {
/** Constant <code>serialVersionUID=-8461473263764812323L</code>. */
private static final long serialVersionUID = -8461473263764812323L;
private static Color purple = new Color(14381203);
private final FLabel nameCostLabel;
private final FLabel typeLabel;
private final FLabel powerToughnessLabel;
private final FLabel idLabel;
private final JLabel setInfoLabel;
private final FHtmlViewer cdArea;
private final FScrollPane scrArea;
public CardDetailPanel(final Card card) {
super();
this.setLayout(null);
this.setOpaque(false);
this.nameCostLabel = new FLabel.Builder().fontAlign(SwingConstants.CENTER).build();
this.typeLabel = new FLabel.Builder().fontAlign(SwingConstants.CENTER).build();
this.idLabel = new FLabel.Builder().fontAlign(SwingConstants.LEFT).tooltip("Card ID").build();
this.powerToughnessLabel = new FLabel.Builder().fontAlign(SwingConstants.CENTER).build();
this.setInfoLabel = new JLabel();
this.setInfoLabel.setHorizontalAlignment(SwingConstants.CENTER);
Font font = new Font("Dialog", 0, 14);
this.nameCostLabel.setFont(font);
this.typeLabel.setFont(font);
this.idLabel.setFont(font);
this.powerToughnessLabel.setFont(font);
this.cdArea = new FHtmlViewer();
this.cdArea.setBorder(new EmptyBorder(2, 6, 2, 6));
this.cdArea.setOpaque(false);
this.scrArea = new FScrollPane(this.cdArea, false);
this.add(this.nameCostLabel);
this.add(this.typeLabel);
this.add(this.idLabel);
this.add(this.powerToughnessLabel);
this.add(this.setInfoLabel);
this.add(this.scrArea);
this.setCard(card);
}
@Override
public void doLayout() {
int insets = 3;
int setInfoWidth = 40;
int x = insets;
int y = insets;
int lineWidth = getWidth() - 2 * insets;
int lineHeight = this.nameCostLabel.getPreferredSize().height;
int dy = lineHeight + 1;
this.nameCostLabel.setBounds(x, y, lineWidth, lineHeight);
y += dy;
this.typeLabel.setBounds(x, y, lineWidth, lineHeight);
y += dy;
this.idLabel.setBounds(x, y, this.idLabel.getAutoSizeWidth(), lineHeight);
this.powerToughnessLabel.setBounds(x, y, lineWidth, lineHeight);
//+1 to x,y so set info label right up against border and the baseline matches ID and P/T
this.setInfoLabel.setBounds(x + lineWidth - setInfoWidth + 1, y + 1, setInfoWidth, lineHeight);
y += dy;
this.scrArea.setBounds(0, y, getWidth(), getHeight() - y);
}
public String getItemDescription(InventoryItemFromSet i) {
if( i instanceof SealedProduct )
return ((SealedProduct)i).getDescription();
if( i instanceof PreconDeck)
return ((PreconDeck) i).getDescription();
return i.getName();
}
public final void setItem(InventoryItemFromSet item) {
nameCostLabel.setText(item.getName());
typeLabel.setVisible(false);
powerToughnessLabel.setVisible(false);
idLabel.setText("");
cdArea.setText(getItemDescription(item));
this.updateBorder(item instanceof IPaperCard ? ((IPaperCard)item).getRules().getColor() : null, false);
String set = item.getEdition();
setInfoLabel.setText(set);
setInfoLabel.setToolTipText("");
if (StringUtils.isEmpty(set)) {
setInfoLabel.setOpaque(false);
setInfoLabel.setBorder(null);
} else {
CardEdition edition = FModel.getMagicDb().getEditions().get(set);
if (null != edition) {
setInfoLabel.setToolTipText(edition.getName());
}
this.setInfoLabel.setOpaque(true);
this.setInfoLabel.setBackground(Color.BLACK);
this.setInfoLabel.setForeground(Color.WHITE);
this.setInfoLabel.setBorder(BorderFactory.createLineBorder(Color.WHITE));
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
scrArea.getVerticalScrollBar().setValue(scrArea.getVerticalScrollBar().getMinimum());
}
});
}
/** {@inheritDoc} */
public final void setCard(final Card card) {
this.nameCostLabel.setText("");
this.typeLabel.setVisible(true);
this.typeLabel.setText("");
this.powerToughnessLabel.setVisible(true);
this.powerToughnessLabel.setText("");
this.idLabel.setText("");
this.setInfoLabel.setText("");
this.setInfoLabel.setToolTipText("");
this.setInfoLabel.setOpaque(false);
this.setInfoLabel.setBorder(null);
this.cdArea.setText("");
if( card == null ) {
this.updateBorder(null, false);
return;
}
boolean canShowThis = false;
if (card.isFaceDown()) {
if (card.isInZone(ZoneType.Battlefield)) {
this.nameCostLabel.setText("Morph");
this.typeLabel.setText("Creature");
}
}
else if (Singletons.getControl().mayShowCard(card) || FDialog.isModalOpen()) { //allow showing cards while modal open to account for revealing, picking, and ordering cards
canShowThis = true;
if (card.getManaCost().isNoCost()) {
this.nameCostLabel.setText(card.getName());
}
else {
String manaCost = card.getManaCost().toString();
if ( card.isSplitCard() && card.getCurState() == CardCharacteristicName.Original) {
manaCost = card.getRules().getMainPart().getManaCost().toString() + " // " + card.getRules().getOtherPart().getManaCost().toString();
}
this.nameCostLabel.setText(FSkin.encodeSymbols(card.getName() + " - " + manaCost, true));
}
this.typeLabel.setText(formatCardType(card));
String set = card.getCurSetCode();
this.setInfoLabel.setText(set);
if (null != set && !set.isEmpty()) {
CardEdition edition = FModel.getMagicDb().getEditions().get(set);
if (null == edition) {
setInfoLabel.setToolTipText(card.getRarity().name());
}
else {
setInfoLabel.setToolTipText(String.format("%s (%s)", edition.getName(), card.getRarity().name()));
}
this.setInfoLabel.setOpaque(true);
switch(card.getRarity()) {
case Uncommon:
this.setInfoLabel.setBackground(Color.LIGHT_GRAY);
this.setInfoLabel.setForeground(Color.BLACK);
this.setInfoLabel.setBorder(BorderFactory.createLineBorder(Color.BLACK));
break;
case Rare:
this.setInfoLabel.setBackground(Color.YELLOW);
this.setInfoLabel.setForeground(Color.BLACK);
this.setInfoLabel.setBorder(BorderFactory.createLineBorder(Color.BLACK));
break;
case MythicRare:
this.setInfoLabel.setBackground(Color.RED);
this.setInfoLabel.setForeground(Color.BLACK);
this.setInfoLabel.setBorder(BorderFactory.createLineBorder(Color.BLACK));
break;
case Special:
// "Timeshifted" or other Special Rarity Cards
this.setInfoLabel.setBackground(CardDetailPanel.purple);
this.setInfoLabel.setForeground(Color.BLACK);
this.setInfoLabel.setBorder(BorderFactory.createLineBorder(Color.BLACK));
break;
default: //case BasicLand: + case Common:
this.setInfoLabel.setBackground(Color.BLACK);
this.setInfoLabel.setForeground(Color.WHITE);
this.setInfoLabel.setBorder(BorderFactory.createLineBorder(Color.WHITE));
break;
}
}
}
this.updateBorder(CardUtil.getColors(card), canShowThis);
StringBuilder ptText = new StringBuilder();
if (card.isCreature()) {
ptText.append(card.getNetAttack()).append(" / ").append(card.getNetDefense());
}
if (card.isPlaneswalker()) {
if (ptText.length() > 0) {
ptText.insert(0, "P/T: ");
ptText.append(" - ").append("Loy: ");
} else {
ptText.append("Loyalty: ");
}
int loyalty = card.getCounters(CounterType.LOYALTY);
if (loyalty == 0) {
loyalty = card.getBaseLoyalty();
}
ptText.append(loyalty);
}
this.powerToughnessLabel.setText(ptText.toString());
this.idLabel.setText(card.getUniqueNumber() > 0 ? "[" + card.getUniqueNumber() + "]" : "");
// fill the card text
this.cdArea.setText(composeCardText(card, canShowThis));
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
scrArea.getVerticalScrollBar().setValue(scrArea.getVerticalScrollBar().getMinimum());
}
});
}
private String composeCardText(final Card card, final boolean canShow) {
final StringBuilder area = new StringBuilder();
// Token
if (card.isToken()) {
area.append("Token");
}
if (canShow) {
// card text
if (area.length() != 0) {
area.append("\n");
}
String text = card.getText();
// LEVEL [0-9]+-[0-9]+
// LEVEL [0-9]+\+
String regex = "LEVEL [0-9]+-[0-9]+ ";
text = text.replaceAll(regex, "$0\r\n");
regex = "LEVEL [0-9]+\\+ ";
text = text.replaceAll(regex, "\r\n$0\r\n");
// displays keywords that have dots in them a little better:
regex = "\\., ";
text = text.replaceAll(regex, ".\r\n");
area.append(text);
}
if (card.isPhasedOut()) {
if (area.length() != 0) {
area.append("\n");
}
area.append("Phased Out");
}
// counter text
final CounterType[] counters = CounterType.values();
for (final CounterType counter : counters) {
if (card.getCounters(counter) != 0) {
if (area.length() != 0) {
area.append("\n");
}
area.append(counter.getName() + " counters: ");
area.append(card.getCounters(counter));
}
}
if (card.isCreature()) {
int damage = card.getDamage();
if (damage > 0) {
if (area.length() != 0) {
area.append("\n");
}
area.append("Damage: " + damage);
}
int assigned = card.getTotalAssignedDamage();
if (assigned > 0) {
if (area.length() != 0) {
area.append("\n");
}
area.append("Assigned Damage: " + assigned);
}
}
if (card.isPlaneswalker()) {
int assigned = card.getTotalAssignedDamage();
if (assigned > 0) {
if (area.length() != 0) {
area.append("\n");
}
area.append("Assigned Damage: " + assigned);
}
}
// Regeneration Shields
final int regenShields = card.getShield().size();
if (regenShields > 0) {
if (area.length() != 0) {
area.append("\n");
}
area.append("Regeneration Shield(s): ").append(regenShields);
}
// Damage Prevention
final int preventNextDamage = card.getPreventNextDamageTotalShields();
if (preventNextDamage > 0) {
area.append("\n");
area.append("Prevent the next ").append(preventNextDamage).append(" damage that would be dealt to ");
area.append(card.getName()).append(" this turn.");
}
// top revealed
if ((card.hasKeyword("Play with the top card of your library revealed.") || card
.hasKeyword("Players play with the top card of their libraries revealed."))
&& card.getController() != null
&& (card.isInZone(ZoneType.Battlefield) || (card.isInZone(ZoneType.Command) && !card.isCommander()))
&& !card.getController().getZone(ZoneType.Library).isEmpty()) {
area.append("\r\nTop card of your library: ");
area.append(card.getController().getCardsIn(ZoneType.Library, 1));
if (card.hasKeyword("Players play with the top card of their libraries revealed.")) {
for (final Player p : card.getController().getAllOtherPlayers()) {
if (p.getZone(ZoneType.Library).isEmpty()) {
area.append(p.getName());
area.append("'s library is empty.");
} else {
area.append("\r\nTop card of ");
area.append(p.getName());
area.append("'s library: ");
area.append(p.getCardsIn(ZoneType.Library, 1));
}
}
}
}
// chosen type
if (!card.getChosenType().equals("")) {
if (area.length() != 0) {
area.append("\n");
}
area.append("(chosen type: ");
area.append(card.getChosenType());
area.append(")");
}
// chosen color
if (!card.getChosenColor().isEmpty()) {
if (area.length() != 0) {
area.append("\n");
}
area.append("(chosen colors: ");
area.append(card.getChosenColor());
area.append(")");
}
// chosen player
if (card.getChosenPlayer() != null) {
if (area.length() != 0) {
area.append("\n");
}
area.append("(chosen player: " + card.getChosenPlayer() + ")");
}
// named card
if (!card.getNamedCard().equals("")) {
if (area.length() != 0) {
area.append("\n");
}
area.append("(named card: ");
area.append(card.getNamedCard());
area.append(")");
}
// equipping
if (!card.getEquipping().isEmpty()) {
if (area.length() != 0) {
area.append("\n");
}
area.append("=Equipping ");
area.append(card.getEquipping().get(0));
area.append("=");
}
// equipped by
if (!card.getEquippedBy().isEmpty()) {
if (area.length() != 0) {
area.append("\n");
}
area.append("=Equipped by ");
for (final Iterator<Card> it = card.getEquippedBy().iterator(); it.hasNext();) {
area.append(it.next());
if (it.hasNext()) {
area.append(", ");
}
}
area.append("=");
}
// enchanting
final GameEntity entity = card.getEnchanting();
if (entity != null) {
if (area.length() != 0) {
area.append("\n");
}
area.append("*Enchanting ");
if (entity instanceof Card) {
final Card c = (Card) entity;
if (!Singletons.getControl().mayShowCard(c)) {
area.append("Morph (");
area.append(card.getUniqueNumber());
area.append(")");
} else {
area.append(entity);
}
} else {
area.append(entity);
}
area.append("*");
}
// enchanted by
if (!card.getEnchantedBy().isEmpty()) {
if (area.length() != 0) {
area.append("\n");
}
area.append("*Enchanted by ");
for (final Iterator<Card> it = card.getEnchantedBy().iterator(); it.hasNext();) {
area.append(it.next());
if (it.hasNext()) {
area.append(", ");
}
}
area.append("*");
}
// controlling
if (card.getGainControlTargets().size() > 0) {
if (area.length() != 0) {
area.append("\n");
}
area.append("+Controlling: ");
for (final Iterator<Card> it = card.getGainControlTargets().iterator(); it.hasNext();) {
area.append(it.next());
if (it.hasNext()) {
area.append(", ");
}
}
area.append("+");
}
// cloned via
if (card.getCloneOrigin() != null) {
if (area.length() != 0) {
area.append("\n");
}
area.append("^Cloned via: ");
area.append(card.getCloneOrigin().getName());
area.append("^");
}
// Imprint
if (!card.getImprinted().isEmpty()) {
if (area.length() != 0) {
area.append("\n");
}
area.append("Imprinting: ");
for (final Iterator<Card> it = card.getImprinted().iterator(); it.hasNext();) {
area.append(it.next());
if (it.hasNext()) {
area.append(", ");
}
}
}
// Haunt
if (!card.getHauntedBy().isEmpty()) {
if (area.length() != 0) {
area.append("\n");
}
area.append("Haunted by: ");
for (final Iterator<Card> it = card.getHauntedBy().iterator(); it.hasNext();) {
area.append(it.next());
if (it.hasNext()) {
area.append(", ");
}
}
}
if (card.getHaunting() != null) {
if (area.length() != 0) {
area.append("\n");
}
area.append("Haunting " + card.getHaunting());
}
// must block
if (card.getMustBlockCards() != null) {
if (area.length() != 0) {
area.append("\n");
}
String mustBlockThese = Lang.joinHomogenous(card.getMustBlockCards());
area.append("Must block " + mustBlockThese);
}
return FSkin.encodeSymbols(area.toString(), true);
}
/** @return FLabel */
public FLabel getNameCostLabel() {
return this.nameCostLabel;
}
/** @return FLabel */
public FLabel getTypeLabel() {
return this.typeLabel;
}
/** @return FLabel */
public FLabel getPowerToughnessLabel() {
return this.powerToughnessLabel;
}
/** @return JLabel */
public JLabel getSetInfoLabel() {
return this.setInfoLabel;
}
/** @return FHtmlViewer */
public FHtmlViewer getCDArea() {
return this.cdArea;
}
public static String formatCardType(final Card card) {
final ArrayList<String> list = card.getType();
final StringBuilder sb = new StringBuilder();
final ArrayList<String> superTypes = new ArrayList<String>();
final ArrayList<String> cardTypes = new ArrayList<String>();
final ArrayList<String> subTypes = new ArrayList<String>();
final boolean allCreatureTypes = list.contains("AllCreatureTypes");
for (final String t : list) {
if (allCreatureTypes && t.equals("AllCreatureTypes")) {
continue;
}
if (CardType.isASuperType(t) && !superTypes.contains(t)) {
superTypes.add(t);
}
if (CardType.isACardType(t) && !cardTypes.contains(t)) {
cardTypes.add(t);
}
if (CardType.isASubType(t) && !subTypes.contains(t) && (!allCreatureTypes || !CardType.isACreatureType(t))) {
subTypes.add(t);
}
}
for (final String type : superTypes) {
sb.append(type).append(" ");
}
for (final String type : cardTypes) {
sb.append(type).append(" ");
}
if (!subTypes.isEmpty() || allCreatureTypes) {
sb.append("- ");
}
if (allCreatureTypes) {
sb.append("All creature types ");
}
for (final String type : subTypes) {
sb.append(type).append(" ");
}
return sb.toString();
}
private void updateBorder(ColorSet list, final boolean canShow) {
// color info
if (list == null) {
this.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
scrArea.setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0));
return;
}
Color color;
if (!canShow) {
color = Color.gray;
} else if (list.isMulticolor()) {
color = Color.orange;
} else if (list.hasBlack()) {
color = Color.black;
} else if (list.hasGreen()) {
color = new Color(0, 220, 39);
} else if (list.hasWhite()) {
color = Color.white;
} else if (list.hasRed()) {
color = Color.red;
} else if (list.hasBlue()) {
color = Color.blue;
} else if (list.isColorless()) {
color = Color.gray;
} else {
color = new Color(200, 0, 230); // If your card has a violet border, something is wrong
}
if (color != Color.gray) {
int r = color.getRed();
int g = color.getGreen();
int b = color.getBlue();
final int shade = 10;
r -= shade;
g -= shade;
b -= shade;
r = Math.max(0, r);
g = Math.max(0, g);
b = Math.max(0, b);
color = new Color(r, g, b);
}
this.setBorder(BorderFactory.createLineBorder(color, 2));
scrArea.setBorder(BorderFactory.createMatteBorder(2, 0, 0, 0, color));
}
}

View File

@@ -0,0 +1,162 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.gui;
import forge.game.card.Card;
import forge.item.PaperCard;
import forge.toolbox.FButton;
import forge.toolbox.FLabel;
import forge.toolbox.FScrollPane;
import forge.view.FDialog;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.util.Collections;
import java.util.List;
/**
* A simple class that shows a list of cards in a dialog with preview in its
* right part.
*
* @author Forge
* @version $Id: ListChooser.java 9708 2011-08-09 19:34:12Z jendave $
*/
@SuppressWarnings("serial")
public class CardListViewer extends FDialog {
// Data and number of choices for the list
private final List<PaperCard> list;
// initialized before; listeners may be added to it
private final JList<PaperCard> jList;
private final CardDetailPanel detail;
private final CardPicturePanel picture;
/**
* Instantiates a new card list viewer.
*
* @param title
* the title
* @param list
* the list
*/
public CardListViewer(final String title, final List<PaperCard> list) {
this(title, "", list, null);
}
/**
* Instantiates a new card list viewer.
*
* @param title
* the title
* @param message
* the message
* @param list
* the list
*/
public CardListViewer(final String title, final String message, final List<PaperCard> list) {
this(title, message, list, null);
}
/**
* Instantiates a new card list viewer.
*
* @param title
* the title
* @param message
* the message
* @param list
* the list
* @param dialogIcon
* the dialog icon
*/
public CardListViewer(final String title, final String message, final List<PaperCard> list, final Icon dialogIcon) {
this.list = Collections.unmodifiableList(list);
this.jList = new JList<PaperCard>(new ChooserListModel());
this.detail = new CardDetailPanel(null);
this.picture = new CardPicturePanel();
this.picture.setOpaque(false);
this.setTitle(title);
this.setSize(720, 360);
this.addWindowFocusListener(new CardListFocuser());
FButton btnOK = new FButton("OK");
btnOK.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
CardListViewer.this.processWindowEvent(new WindowEvent(CardListViewer.this, WindowEvent.WINDOW_CLOSING));
}
});
this.add(new FLabel.Builder().text(message).build(), "cell 0 0, spanx 3, gapbottom 4");
this.add(new FScrollPane(this.jList, true), "cell 0 1, w 225, growy, pushy, ax c");
this.add(this.picture, "cell 1 1, w 225, growy, pushy, ax c");
this.add(this.detail, "cell 2 1, w 225, growy, pushy, ax c");
this.add(btnOK, "cell 1 2, w 150, h 26, ax c, gaptop 6");
// selection is here
this.jList.getSelectionModel().addListSelectionListener(new SelListener());
this.jList.setSelectedIndex(0);
}
private class ChooserListModel extends AbstractListModel<PaperCard> {
private static final long serialVersionUID = 3871965346333840556L;
@Override
public int getSize() {
return CardListViewer.this.list.size();
}
@Override
public PaperCard getElementAt(final int index) {
return CardListViewer.this.list.get(index);
}
}
private class CardListFocuser implements WindowFocusListener {
@Override
public void windowGainedFocus(final WindowEvent e) {
CardListViewer.this.jList.grabFocus();
}
@Override
public void windowLostFocus(final WindowEvent e) {
}
}
private class SelListener implements ListSelectionListener {
@Override
public void valueChanged(final ListSelectionEvent e) {
final int row = CardListViewer.this.jList.getSelectedIndex();
// (String) jList.getSelectedValue();
if ((row >= 0) && (row < CardListViewer.this.list.size())) {
final PaperCard cp = CardListViewer.this.list.get(row);
CardListViewer.this.detail.setCard(Card.getCardForUi(cp));
CardListViewer.this.picture.setCard(cp);
}
}
}
}

View File

@@ -0,0 +1,111 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.gui;
import forge.ImageCache;
import forge.ImageKeys;
import forge.card.CardCharacteristicName;
import forge.game.card.Card;
import forge.item.InventoryItem;
import forge.model.FModel;
import forge.properties.ForgePreferences.FPref;
import forge.toolbox.imaging.FImagePanel;
import forge.toolbox.imaging.FImageUtil;
import forge.toolbox.imaging.FImagePanel.AutoSizeImageMode;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
/**
* Displays image associated with a card or inventory item.
*
* @version $Id: CardPicturePanel.java 25265 2014-03-27 02:18:47Z drdev $
*
*/
public final class CardPicturePanel extends JPanel {
/** Constant <code>serialVersionUID=-3160874016387273383L</code>. */
private static final long serialVersionUID = -3160874016387273383L;
private Object displayed;
private final FImagePanel panel;
private BufferedImage currentImage;
private boolean mayShowCard;
public CardPicturePanel() {
super(new BorderLayout());
this.panel = new FImagePanel();
this.add(this.panel);
}
public void setCard(final InventoryItem cp) {
this.displayed = cp;
this.mayShowCard = true;
this.setImage();
}
//@Override
public void setCard(final Card c, boolean mayShowCard) {
this.displayed = c;
this.mayShowCard = mayShowCard;
this.setImage();
}
public void setCardImage(CardCharacteristicName flipState) {
BufferedImage image = FImageUtil.getImage((Card)displayed, flipState);
if (image != null && image != this.currentImage) {
this.currentImage = image;
this.panel.setImage(image, getAutoSizeImageMode());
}
}
public void setImage() {
BufferedImage image = getImage();
if (image != null && image != this.currentImage) {
this.currentImage = image;
this.panel.setImage(image, getAutoSizeImageMode());
}
}
public BufferedImage getImage() {
if (displayed instanceof InventoryItem) {
InventoryItem item = (InventoryItem) displayed;
return ImageCache.getOriginalImage(ImageKeys.getImageKey(item, false), true);
}
else if (displayed instanceof Card) {
if (mayShowCard) {
return FImageUtil.getImage((Card)displayed);
}
return ImageCache.getOriginalImage(ImageKeys.TOKEN_PREFIX + ImageKeys.MORPH_IMAGE, true);
}
return null;
}
private AutoSizeImageMode getAutoSizeImageMode() {
return (isUIScaleLarger() ? AutoSizeImageMode.PANEL : AutoSizeImageMode.SOURCE);
}
private boolean isUIScaleLarger() {
return FModel.getPreferences().getPrefBoolean(FPref.UI_SCALE_LARGER);
}
}

View File

@@ -0,0 +1,451 @@
package forge.gui;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
import forge.item.IPaperCard;
import forge.item.PaperCard;
import forge.screens.match.CMatchUI;
import forge.toolbox.*;
import forge.view.FDialog;
import javax.swing.*;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
// An input box for handling the order of choices.
// Left box has the original choices
// Right box has the final order
// Top string will be like Top of the Stack or Top of the Library
// Bottom string will be like Bottom of the Stack or Bottom of the Library
// Single Arrows in between left box and right box for ordering
// Multi Arrows for moving everything in order
// Up/down arrows on the right of the right box for swapping
// Single ok button, disabled until left box has specified number of items remaining
@SuppressWarnings("serial")
public class DualListBox<T> extends FDialog {
private final FList<T> sourceList;
private final UnsortedListModel<T> sourceListModel;
private final FList<T> destList;
private final UnsortedListModel<T> destListModel;
private final FButton addButton;
private final FButton addAllButton;
private final FButton removeButton;
private final FButton removeAllButton;
private final FButton okButton;
private final FButton autoButton;
private final FLabel orderedLabel;
private final FLabel selectOrder;
private final int targetRemainingSourcesMin;
private final int targetRemainingSourcesMax;
private boolean sideboardingMode = false;
private boolean showCard = true;
public DualListBox(int remainingSources, List<T> sourceElements, List<T> destElements) {
this(remainingSources, remainingSources, sourceElements, destElements);
}
public DualListBox(int remainingSourcesMin, int remainingSourcesMax, List<T> sourceElements, List<T> destElements) {
targetRemainingSourcesMin = remainingSourcesMin;
targetRemainingSourcesMax = remainingSourcesMax;
sourceListModel = new UnsortedListModel<T>();
sourceList = new FList<T>(sourceListModel);
destListModel = new UnsortedListModel<T>();
destList = new FList<T>(destListModel);
final Runnable onAdd = new Runnable() {
@SuppressWarnings("unchecked")
@Override
public void run() {
if (!addButton.isEnabled()) { return; }
List<T> selected = new ArrayList<T>();
for (Object item : sourceList.getSelectedValuesList()) {
selected.add((T)item);
}
addDestinationElements(selected);
clearSourceSelected();
sourceList.validate();
_setButtonState();
}
};
final Runnable onRemove = new Runnable() {
@SuppressWarnings("unchecked")
@Override
public void run() {
if (!removeButton.isEnabled()) { return; }
List<T> selected = new ArrayList<T>();
for (Object item : destList.getSelectedValuesList()) {
selected.add((T)item);
}
clearDestinationSelected();
addSourceElements(selected);
_setButtonState();
}
};
sourceList.addKeyListener(new KeyAdapter() {
@Override public void keyPressed(final KeyEvent e) {
_handleListKey(e, onAdd, destList);
}
});
sourceList.addMouseListener(new MouseAdapter() {
@Override public void mouseClicked(MouseEvent e) {
if (MouseEvent.BUTTON1 == e.getButton() && 2 == e.getClickCount()) { onAdd.run(); }
}
});
destList.addKeyListener(new KeyAdapter() {
@Override public void keyPressed(final KeyEvent e) {
_handleListKey(e, onRemove, sourceList);
}
});
destList.addMouseListener(new MouseAdapter() {
@Override public void mouseClicked(MouseEvent e) {
if (MouseEvent.BUTTON1 == e.getButton() && 2 == e.getClickCount()) { onRemove.run(); }
}
});
// Dual List control buttons
addButton = new FButton(">");
addButton.addActionListener(new ActionListener() {@Override public void actionPerformed(ActionEvent e) { onAdd.run(); } });
addAllButton = new FButton(">>");
addAllButton.addActionListener(new ActionListener() {@Override public void actionPerformed(ActionEvent e) { _addAll(); } });
removeButton = new FButton("<");
removeButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { onRemove.run(); } });
removeAllButton = new FButton("<<");
removeAllButton.addActionListener(new ActionListener() {@Override public void actionPerformed(ActionEvent e) { _removeAll(); } });
// Dual List Complete Buttons
okButton = new FButton("OK");
okButton.addActionListener(new ActionListener() {@Override public void actionPerformed(ActionEvent e) { _finish(); } });
autoButton = new FButton("Auto");
autoButton.addActionListener(new ActionListener() {@Override public void actionPerformed(ActionEvent e) { _addAll(); _finish(); } });
FPanel leftPanel = new FPanel(new BorderLayout());
selectOrder = new FLabel.Builder().text("Select Order:").build();
leftPanel.add(selectOrder, BorderLayout.NORTH);
leftPanel.add(new FScrollPane(sourceList, true), BorderLayout.CENTER);
leftPanel.add(okButton, BorderLayout.SOUTH);
FPanel centerPanel = new FPanel(new GridLayout(6, 1));
centerPanel.setBorderToggle(false);
JPanel emptyPanel = new JPanel();
emptyPanel.setOpaque(false);
centerPanel.add(emptyPanel); // empty panel to take up the first slot
centerPanel.add(addButton);
centerPanel.add(addAllButton);
centerPanel.add(removeButton);
centerPanel.add(removeAllButton);
orderedLabel = new FLabel.Builder().build();
FPanel rightPanel = new FPanel(new BorderLayout());
rightPanel.add(orderedLabel, BorderLayout.NORTH);
rightPanel.add(new FScrollPane(destList, true), BorderLayout.CENTER);
rightPanel.add(autoButton, BorderLayout.SOUTH);
add(leftPanel, "w 250, h 300");
add(centerPanel, "w 100, h 300");
add(rightPanel, "w 250, h 300");
_addListListeners(sourceList);
_addListListeners(destList);
if (destElements != null && !destElements.isEmpty()) {
addDestinationElements(destElements);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
destList.setSelectedIndex(0);
}
});
}
if (sourceElements != null && !sourceElements.isEmpty()) {
addSourceElements(sourceElements);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
sourceList.setSelectedIndex(0);
}
});
}
_setButtonState();
if (remainingSourcesMin <= sourceElements.size() && remainingSourcesMax >= sourceElements.size()) {
//ensure OK button gets initial focus if remainingSources matches source list count
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
okButton.requestFocusInWindow();
}
});
}
}
public void setSecondColumnLabelText(String label) {
orderedLabel.setText(label);
}
public void setSideboardMode(boolean isSideboardMode) {
sideboardingMode = isSideboardMode;
if (sideboardingMode) {
addAllButton.setVisible(false);
removeAllButton.setVisible(false);
autoButton.setEnabled(false);
selectOrder.setText(String.format("Sideboard (%d):", sourceListModel.getSize()));
orderedLabel.setText(String.format("Main Deck (%d):", destListModel.getSize()));
}
}
private void _handleListKey(KeyEvent e, Runnable onSpace, FList<T> arrowFocusTarget) {
switch (e.getKeyCode()) {
case KeyEvent.VK_SPACE:
onSpace.run();
break;
case KeyEvent.VK_LEFT:
case KeyEvent.VK_RIGHT:
arrowFocusTarget.requestFocusInWindow();
break;
case KeyEvent.VK_ENTER:
if (okButton.isEnabled()) {
okButton.doClick();
}
else if (autoButton.isEnabled()) {
autoButton.doClick();
}
break;
default:
break;
}
}
public void clearSourceListModel() {
sourceListModel.clear();
}
public void clearDestinationListModel() {
destListModel.clear();
}
public void addSourceElements(ListModel<T> newValue) {
fillListModel(sourceListModel, newValue);
}
public void setSourceElements(ListModel<T> newValue) {
clearSourceListModel();
addSourceElements(newValue);
}
public void addDestinationElements(List<T> newValue) {
fillListModel(destListModel, newValue);
}
public void addDestinationElements(ListModel<T> newValue) {
fillListModel(destListModel, newValue);
}
private void fillListModel(UnsortedListModel<T> model, ListModel<T> newValues) {
model.addAll(newValues);
}
public void addSourceElements(List<T> newValue) {
fillListModel(sourceListModel, newValue);
}
public void setSourceElements(List<T> newValue) {
clearSourceListModel();
addSourceElements(newValue);
}
private void fillListModel(UnsortedListModel<T> model, List<T> newValues) {
model.addAll(newValues);
}
private void clearSourceSelected() {
int[] selected = sourceList.getSelectedIndices();
for (int i = selected.length - 1; i >= 0; --i) {
sourceListModel.removeElement(selected[i]);
}
}
private void clearDestinationSelected() {
int[] selected = destList.getSelectedIndices();
for (int i = selected.length - 1; i >= 0; --i) {
destListModel.removeElement(selected[i]);
}
}
public List<T> getOrderedList() {
this.setVisible(false);
return destListModel.model;
}
public List<T> getRemainingSourceList() {
return sourceListModel.model;
}
private void showSelectedCard(Object obj) {
if (!showCard || null == obj) {
return;
}
Card card = null;
if (obj instanceof Card) {
card = (Card) obj;
} else if (obj instanceof SpellAbility) {
card = ((SpellAbility) obj).getHostCard();
} else if (obj instanceof PaperCard) {
card = Card.getCardForUi((IPaperCard) obj);
}
GuiUtils.clearPanelSelections();
if (card != null) {
CMatchUI.SINGLETON_INSTANCE.setCard(card);
GuiUtils.setPanelSelection(card);
}
}
private void _addListListeners(final FList<T> list) {
list.getModel().addListDataListener(new ListDataListener() {
int callCount = 0;
@Override
public void intervalRemoved(final ListDataEvent e) {
final int callNum = ++callCount;
// invoke this later since the list is out of sync with the model
// at this moment.
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (callNum != callCount) {
// don't run stale callbacks
return;
}
ListModel<T> model = list.getModel();
if (0 == model.getSize()) {
// nothing left to show
return;
}
int cardIdx = e.getIndex0();
if (model.getSize() <= cardIdx) {
// the last element got removed, get the one above it
cardIdx = model.getSize() - 1;
}
showCard = false;
list.setSelectedIndex(cardIdx);
showCard = true;
showSelectedCard(model.getElementAt(cardIdx));
if (!okButton.isEnabled()) {
list.requestFocusInWindow();
}
}
});
}
@Override
public void intervalAdded(final ListDataEvent e) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// select just-added items so user can undo the add with a single click
int startIdx = Math.min(e.getIndex0(), e.getIndex1());
int endIdx = Math.max(e.getIndex0(), e.getIndex1());
int[] addedIndices = new int[endIdx - startIdx + 1];
for (int idx = startIdx; idx <= endIdx; ++idx) {
addedIndices[idx - startIdx] = idx;
}
// attempt to scroll to just-added item (setSelectedIndices does not scroll)
// this will scroll to the wrong item if there are other identical items previously in the list
showCard = false;
list.setSelectedValue(list.getModel().getElementAt(
Math.min(endIdx, startIdx + list.getVisibleRowCount())), true);
list.setSelectedIndices(addedIndices);
showCard = true;
}
});
}
@Override
public void contentsChanged(ListDataEvent e) {
}
});
list.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent ev) {
showSelectedCard(list.getSelectedValue());
}
});
list.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
showSelectedCard(list.getSelectedValue());
}
});
}
private void _addAll() {
addDestinationElements(sourceListModel);
clearSourceListModel();
_setButtonState();
}
private void _removeAll() {
addSourceElements(destListModel);
clearDestinationListModel();
_setButtonState();
}
private void _setButtonState() {
if (sideboardingMode) {
removeAllButton.setVisible(false);
addAllButton.setVisible(false);
selectOrder.setText(String.format("Sideboard (%d):", sourceListModel.getSize()));
orderedLabel.setText(String.format("Main Deck (%d):", destListModel.getSize()));
}
boolean anySize = targetRemainingSourcesMax < 0;
boolean canAdd = sourceListModel.getSize() != 0 && (anySize || targetRemainingSourcesMin <= sourceListModel.getSize());
boolean canRemove = destListModel.getSize() != 0;
boolean targetReached = anySize || targetRemainingSourcesMin <= sourceListModel.getSize() && targetRemainingSourcesMax >= sourceListModel.getSize();
autoButton.setEnabled(targetRemainingSourcesMax == 0 && !targetReached && !sideboardingMode);
addButton.setEnabled(canAdd);
addAllButton.setEnabled(canAdd);
removeButton.setEnabled(canRemove);
removeAllButton.setEnabled(canRemove);
okButton.setEnabled(targetReached);
if (targetReached) {
okButton.requestFocusInWindow(); //focus OK button if specific target reached
}
}
private void _finish() {
this.setVisible(false);
}
}

View File

@@ -0,0 +1,130 @@
package forge.gui;
import forge.control.ChatArea;
import forge.net.FServer;
import forge.net.Lobby;
import forge.toolbox.*;
import forge.toolbox.FSkin.SkinnedPanel;
import net.miginfocom.swing.MigLayout;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* TODO: Write javadoc for this type.
*
*/
public enum FNetOverlay {
SINGLETON_INSTANCE;
private final OverlayPanel pnl = new OverlayPanel();
/** @return {@link javax.swing.JPanel} */
public SkinnedPanel getPanel() {
return this.pnl;
}
private final FTextArea txtLog = new FTextArea();
private final FTextField txtInput = new FTextField.Builder().maxLength(60).build();
private final FLabel cmdSend = new FLabel.ButtonBuilder().text("Send").build();
//private boolean minimized = false;
private int height = 120;
private int width = 400;
private final ActionListener onSend = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String message = txtInput.getText();
txtInput.setText("");
if ( StringUtils.isBlank(message) )
return;
Lobby lobby = FServer.getLobby();
lobby.speak(ChatArea.Room, lobby.getGuiPlayer(), message);
}
};
//private final int minimizedHeight = 30;
/**
* Semi-transparent overlay panel. Should be used with layered panes.
*/
private FNetOverlay() {
pnl.setOpaque(false);
pnl.setVisible(false);
pnl.setBackground(FSkin.getColor(FSkin.Colors.CLR_ZEBRA));
pnl.setBorder(new FSkin.LineSkinBorder(FSkin.getColor(FSkin.Colors.CLR_BORDERS)));
pnl.setLayout(new MigLayout("insets 0, gap 0, ax center, wrap 2"));
// pnl.add(new FLabel.Builder().text("Loading new game...").fontSize(22).build(), "h 40px!, align center");
// Block all input events below the overlay
txtLog.setOpaque(true);
txtLog.setFocusable(true);
txtLog.setBackground(FSkin.getColor(FSkin.Colors.CLR_ZEBRA));
FScrollPane _operationLogScroller = new FScrollPane(txtLog, false);
_operationLogScroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
new SmartScroller(_operationLogScroller);
pnl.add(_operationLogScroller, "pushx, hmin 24, pushy, growy, growx, gap 2px 2px 2px 0, sx 2");
txtInput.setBorder(new FSkin.LineSkinBorder(FSkin.getColor(FSkin.Colors.CLR_BORDERS)));
pnl.add(txtInput, "pushx, growx, h 26px!, gap 2px 2px 2px 0");
pnl.add(cmdSend, "w 60px!, h 28px!, gap 0 0 2px 0");
txtInput.addActionListener(onSend);
cmdSend.setCommand(new Runnable() { @Override public void run() { onSend.actionPerformed(null); } });
}
public void showUp(String message) {
txtLog.setText(message);
pnl.setVisible(true);
}
private class OverlayPanel extends SkinnedPanel {
private static final long serialVersionUID = -5056220798272120558L;
/**
* For some reason, the alpha channel background doesn't work properly on
* Windows 7, so the paintComponent override is required for a
* semi-transparent overlay.
*
* @param g
* &emsp; Graphics object
*/
@Override
public void paintComponent(final Graphics g) {
super.paintComponent(g);
g.setColor(this.getBackground());
g.fillRect(0, 0, this.getWidth(), this.getHeight());
}
}
/**
* TODO: Write javadoc for this method.
* @param mainBounds
*/
public void containerResized(Rectangle mainBounds) {
int w = Math.max(width, (int)(mainBounds.width * 0.25f));
int x = mainBounds.width - w;
int y = mainBounds.height - height;
getPanel().setBounds(x, y, w, height);
getPanel().validate();
}
SimpleDateFormat inFormat = new SimpleDateFormat("HH:mm:ss");
public void addMessage(String origin, String message) {
String toAdd = String.format("%n[%s] %s: %s", inFormat.format(new Date()), origin, message);
txtLog.append(toAdd);
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.gui;
import javax.swing.*;
import forge.match.MatchConstants;
@SuppressWarnings("serial")
public abstract class ForgeAction extends AbstractAction {
public ForgeAction(MatchConstants property) {
super(property.button);
this.putValue("buttonText", property.button);
this.putValue("menuText", property.menu);
}
public <T extends AbstractButton> T setupButton(final T button) {
button.setAction(this);
button.setText((String) this.getValue(button instanceof JMenuItem ? "menuText" : "buttonText"));
return button;
}
}

View File

@@ -0,0 +1,358 @@
package forge.gui;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import forge.FThreads;
import forge.Singletons;
import forge.game.card.Card;
import forge.item.InventoryItem;
import forge.screens.match.CMatchUI;
import forge.toolbox.FOptionPane;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* TODO: Write javadoc for this type.
*
*/
public class GuiChoose {
/**
* Convenience for getChoices(message, 0, 1, choices).
*
* @param <T>
* is automatically inferred.
* @param message
* a {@link java.lang.String} object.
* @param choices
* a T object.
* @return null if choices is missing, empty, or if the users' choices are
* empty; otherwise, returns the first item in the List returned by
* getChoices.
* @see #getChoices(String, int, int, Object...)
*/
public static <T> T oneOrNone(final String message, final T[] choices) {
if ((choices == null) || (choices.length == 0)) {
return null;
}
final List<T> choice = GuiChoose.getChoices(message, 0, 1, choices);
return choice.isEmpty() ? null : choice.get(0);
} // getChoiceOptional(String,T...)
public static <T> T oneOrNone(final String message, final Collection<T> choices) {
if ((choices == null) || choices.isEmpty()) {
return null;
}
final List<T> choice = GuiChoose.getChoices(message, 0, 1, choices);
return choice.isEmpty() ? null : choice.get(0);
} // getChoiceOptional(String,T...)
// returned Object will never be null
/**
* <p>
* getChoice.
* </p>
*
* @param <T>
* a T object.
* @param message
* a {@link java.lang.String} object.
* @param choices
* a T object.
* @return a T object.
*/
public static <T> T one(final String message, final T[] choices) {
final List<T> choice = GuiChoose.getChoices(message, 1, 1, choices);
assert choice.size() == 1;
return choice.get(0);
}
public static <T> T one(final String message, final Collection<T> choices) {
if (choices == null || choices.isEmpty())
return null;
if( choices.size() == 1)
return Iterables.getFirst(choices, null);
final List<T> choice = GuiChoose.getChoices(message, 1, 1, choices);
assert choice.size() == 1;
return choice.get(0);
}
public static <T> List<T> noneOrMany(final String message, final Collection<T> choices) {
return GuiChoose.getChoices(message, 0, choices.size(), choices, null, null);
}
// Nothing to choose here. Code uses this to just reveal one or more items
public static <T> void reveal(final String message, final T item) {
List<T> items = new ArrayList<T>();
items.add(item);
reveal(message, items);
}
public static <T> void reveal(final String message, final T[] items) {
GuiChoose.getChoices(message, -1, -1, items);
}
public static <T> void reveal(final String message, final Collection<T> items) {
GuiChoose.getChoices(message, -1, -1, items);
}
// Get Integer in range
public static Integer getInteger(final String message) {
return getInteger(message, 0, Integer.MAX_VALUE);
}
public static Integer getInteger(final String message, int min) {
return getInteger(message, min, Integer.MAX_VALUE);
}
public static Integer getInteger(final String message, int min, int max) {
if (max <= min) { return min; } //just return min if max <= min
//force cutting off after 100 numbers at most
if (max == Integer.MAX_VALUE) {
return getInteger(message, min, max, min + 99);
}
int count = max - min + 1;
if (count > 100) {
return getInteger(message, min, max, min + 99);
}
final Integer[] choices = new Integer[count];
for (int i = 0; i < count; i++) {
choices[i] = Integer.valueOf(i + min);
}
return GuiChoose.oneOrNone(message, choices);
}
public static Integer getInteger(final String message, int min, int max, int cutoff) {
if (max <= min || cutoff < min) { return min; } //just return min if max <= min or cutoff < min
if (cutoff >= max) { //fallback to regular integer prompt if cutoff at or after max
return getInteger(message, min, max);
}
List<Object> choices = new ArrayList<Object>();
for (int i = min; i <= cutoff; i++) {
choices.add(Integer.valueOf(i));
}
choices.add("Other...");
Object choice = GuiChoose.oneOrNone(message, choices);
if (choice instanceof Integer || choice == null) {
return (Integer)choice;
}
//if Other option picked, prompt for number input
String prompt = "Enter a number";
if (min != Integer.MIN_VALUE) {
if (max != Integer.MAX_VALUE) {
prompt += " between " + min + " and " + max;
}
else {
prompt += " greater than or equal to " + min;
}
}
else if (max != Integer.MAX_VALUE) {
prompt += " less than or equal to " + max;
}
prompt += ":";
while (true) {
String str = FOptionPane.showInputDialog(prompt, message);
if (str == null) { return null; } // that is 'cancel'
if (StringUtils.isNumeric(str)) {
Integer val = Integer.valueOf(str);
if (val >= min && val <= max) {
return val;
}
}
}
}
// returned Object will never be null
public static <T> List<T> getChoices(final String message, final int min, final int max, final T[] choices) {
return getChoices(message, min, max, Arrays.asList(choices), null, null);
}
public static <T> List<T> getChoices(final String message, final int min, final int max, final Collection<T> choices) {
return getChoices(message, min, max, choices, null, null);
}
public static <T> List<T> getChoices(final String message, final int min, final int max, final Collection<T> choices, final T selected, final Function<T, String> display) {
if (choices == null || choices.isEmpty()) {
if (min == 0) {
return new ArrayList<T>();
}
throw new RuntimeException("choice required from empty list");
}
Callable<List<T>> showChoice = new Callable<List<T>>() {
@Override
public List<T> call() {
ListChooser<T> c = new ListChooser<T>(message, min, max, choices, display);
final JList<T> list = c.getLstChoices();
list.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(final ListSelectionEvent ev) {
if (list.getSelectedValue() instanceof Card) {
Card card = (Card) list.getSelectedValue();
if (card.isFaceDown() && Singletons.getControl().mayShowCard(card)) {
CMatchUI.SINGLETON_INSTANCE.setCard(card, true);
}
else {
CMatchUI.SINGLETON_INSTANCE.setCard(card);
}
GuiUtils.clearPanelSelections();
GuiUtils.setPanelSelection(card);
}
if (list.getSelectedValue() instanceof InventoryItem) {
CMatchUI.SINGLETON_INSTANCE.setCard((InventoryItem) list.getSelectedValue());
}
}
});
if (selected != null) {
c.show(selected);
}
else {
c.show();
}
GuiUtils.clearPanelSelections();
return c.getSelectedValues();
}
};
FutureTask<List<T>> future = new FutureTask<List<T>>(showChoice);
FThreads.invokeInEdtAndWait(future);
try {
return future.get();
} catch (Exception e) { // should be no exception here
e.printStackTrace();
}
return null;
}
public static <T> List<T> many(final String title, final String topCaption, int cnt, final List<T> sourceChoices, Card referenceCard) {
return order(title, topCaption, cnt, cnt, sourceChoices, null, referenceCard, false);
}
public static <T> List<T> many(final String title, final String topCaption, int min, int max, final List<T> sourceChoices, Card referenceCard) {
int m2 = min >= 0 ? sourceChoices.size() - min : -1;
int m1 = max >= 0 ? sourceChoices.size() - max : -1;
return order(title, topCaption, m1, m2, sourceChoices, null, referenceCard, false);
}
public static <T> List<T> order(final String title, final String top, final List<T> sourceChoices, Card referenceCard) {
return order(title, top, 0, 0, sourceChoices, null, referenceCard, false);
}
public static <T extends Comparable<? super T>> List<T> sideboard(List<T> sideboard, List<T> deck) {
Collections.sort(deck);
Collections.sort(sideboard);
return order("Sideboard", "Main Deck", -1, -1, sideboard, deck, null, true);
}
private static <T> List<T> order(final String title, final String top, final int remainingObjectsMin, final int remainingObjectsMax,
final List<T> sourceChoices, final List<T> destChoices, final Card referenceCard, final boolean sideboardingMode) {
// An input box for handling the order of choices.
Callable<List<T>> callable = new Callable<List<T>>() {
@Override
public List<T> call() throws Exception {
DualListBox<T> dual = new DualListBox<T>(remainingObjectsMin, remainingObjectsMax, sourceChoices, destChoices);
dual.setSecondColumnLabelText(top);
dual.setSideboardMode(sideboardingMode);
dual.setTitle(title);
dual.pack();
dual.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
if (referenceCard != null) {
CMatchUI.SINGLETON_INSTANCE.setCard(referenceCard);
// MARKED FOR UPDATE
}
dual.setVisible(true);
List<T> objects = dual.getOrderedList();
dual.dispose();
GuiUtils.clearPanelSelections();
return objects;
}
};
FutureTask<List<T>> ft = new FutureTask<List<T>>(callable);
FThreads.invokeInEdtAndWait(ft);
try {
return ft.get();
} catch (Exception e) { // we have waited enough
e.printStackTrace();
}
return null;
}
// If comparer is NULL, T has to be comparable. Otherwise you'll get an exception from inside the Arrays.sort() routine
public static <T> T sortedOneOrNone(final String message, final T[] choices, Comparator<T> comparer) {
if ((choices == null) || (choices.length == 0)) {
return null;
}
final List<T> choice = GuiChoose.sortedGetChoices(message, 0, 1, choices, comparer);
return choice.isEmpty() ? null : choice.get(0);
} // getChoiceOptional(String,T...)
// If comparer is NULL, T has to be comparable. Otherwise you'll get an exception from inside the Arrays.sort() routine
public static <T> T sortedOneOrNone(final String message, final List<T> choices, Comparator<T> comparer) {
if ((choices == null) || choices.isEmpty()) {
return null;
}
final List<T> choice = GuiChoose.sortedGetChoices(message, 0, 1, choices, comparer);
return choice.isEmpty() ? null : choice.get(0);
} // getChoiceOptional(String,T...)
// If comparer is NULL, T has to be comparable. Otherwise you'll get an exception from inside the Arrays.sort() routine
public static <T> T sortedOne(final String message, final T[] choices, Comparator<T> comparer) {
final List<T> choice = GuiChoose.sortedGetChoices(message, 1, 1, choices, comparer);
assert choice.size() == 1;
return choice.get(0);
} // getChoice()
// If comparer is NULL, T has to be comparable. Otherwise you'll get an exception from inside the Arrays.sort() routine
public static <T> T sortedOne(final String message, final List<T> choices, Comparator<T> comparer) {
if ((choices == null) || (choices.size() == 0)) {
return null;
}
final List<T> choice = GuiChoose.sortedGetChoices(message, 1, 1, choices, comparer);
assert choice.size() == 1;
return choice.get(0);
}
// If comparer is NULL, T has to be comparable. Otherwise you'll get an exception from inside the Arrays.sort() routine
public static <T> List<T> sortedNoneOrMany(final String message, final List<T> choices, Comparator<T> comparer) {
return GuiChoose.sortedGetChoices(message, 0, choices.size(), choices, comparer);
}
// If comparer is NULL, T has to be comparable. Otherwise you'll get an exception from inside the Arrays.sort() routine
public static <T> List<T> sortedGetChoices(final String message, final int min, final int max, final T[] choices, Comparator<T> comparer) {
// You may create a copy of source array if callers expect the collection to be unchanged
Arrays.sort(choices, comparer);
return getChoices(message, min, max, choices);
}
// If comparer is NULL, T has to be comparable. Otherwise you'll get an exception from inside the Arrays.sort() routine
public static <T> List<T> sortedGetChoices(final String message, final int min, final int max, final List<T> choices, Comparator<T> comparer) {
// You may create a copy of source list if callers expect the collection to be unchanged
Collections.sort(choices, comparer);
return getChoices(message, min, max, choices);
}
}

View File

@@ -0,0 +1,77 @@
package forge.gui;
import forge.FThreads;
import forge.game.card.Card;
import forge.screens.match.CMatchUI;
import forge.toolbox.FOptionPane;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* Holds player interactions using standard windows
*
*/
public class GuiDialog {
private static final String[] defaultConfirmOptions = { "Yes", "No" };
public static boolean confirm(final Card c, final String question) {
return GuiDialog.confirm(c, question, true, null);
}
public static boolean confirm(final Card c, final String question, final boolean defaultChoice) {
return GuiDialog.confirm(c, question, defaultChoice, null);
}
public static boolean confirm(final Card c, final String question, String[] options) {
return GuiDialog.confirm(c, question, true, options);
}
public static boolean confirm(final Card c, final String question, final boolean defaultIsYes, final String[] options) {
Callable<Boolean> confirmTask = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
if ( null != c )
CMatchUI.SINGLETON_INSTANCE.setCard(c);
final String title = c == null ? "Question" : c.getName() + " - Ability";
String questionToUse = StringUtils.isBlank(question) ? "Activate card's ability?" : question;
String[] opts = options == null ? defaultConfirmOptions : options;
int answer = FOptionPane.showOptionDialog(questionToUse, title, FOptionPane.QUESTION_ICON, opts, defaultIsYes ? 0 : 1);
return answer == 0;
}};
FutureTask<Boolean> future = new FutureTask<Boolean>(confirmTask);
FThreads.invokeInEdtAndWait(future);
try {
return future.get().booleanValue();
}
catch (Exception e) { // should be no exception here
e.printStackTrace();
}
return false;
}
/**
* <p>
* showInfoDialg.
* </p>
*
* @param message
* a {@link java.lang.String} object.
*/
public static void message(final String message) {
message(message, UIManager.getString("OptionPane.messageDialogTitle"));
}
public static void message(final String message, final String title) {
FThreads.invokeInEdtAndWait(new Runnable() {
@Override
public void run() {
FOptionPane.showMessageDialog(message, title, null);
}
});
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.gui;
import forge.view.FDialog;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
/**
* <p>
* Gui_ProgressBarWindow class.
* </p>
*
* @author Forge
* @version $Id: GuiProgressBarWindow.java 24769 2014-02-09 13:56:04Z Hellfish $
* @since 1.0.15
*/
public class GuiProgressBarWindow extends FDialog {
/**
*
*/
private static final long serialVersionUID = 5832740611050396643L;
private final JPanel contentPanel = new JPanel();
private final JProgressBar progressBar = new JProgressBar();
/**
* Create the dialog.
*/
public GuiProgressBarWindow() {
this.setResizable(false);
this.setTitle("Some Progress");
final Dimension screen = this.getToolkit().getScreenSize();
this.setBounds(screen.width / 3, 100, // position
450, 84); // size
this.getContentPane().setLayout(null);
this.contentPanel.setBounds(0, 0, 442, 58);
this.contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
this.getContentPane().add(this.contentPanel);
this.contentPanel.setLayout(null);
this.progressBar.setValue(50);
// progressBar.setBackground(Color.GRAY);
// progressBar.setForeground(Color.BLUE);
this.progressBar.setBounds(12, 12, 418, 32);
this.contentPanel.add(this.progressBar);
}
/**
* <p>
* setProgressRange.
* </p>
*
* @param min
* a int.
* @param max
* a int.
*/
public final void setProgressRange(final int min, final int max) {
this.progressBar.setMinimum(min);
this.progressBar.setMaximum(max);
}
/**
* <p>
* increment.
* </p>
*/
public final void increment() {
this.progressBar.setValue(this.progressBar.getValue() + 1);
if ((this.progressBar.getValue() % 10) == 0) {
this.contentPanel.paintImmediately(this.progressBar.getBounds());
}
}
/**
* Get the progressBar for fine tuning (e.g., adding text).
*
* @return the progressBar
*/
public final JProgressBar getProgressBar() {
return this.progressBar;
}
/**
* Set the progress back to zero units completed.
*
* It is not certain whether this method actually works the way it was
* intended.
*/
public final void reset() {
this.getProgressBar().setValue(0);
this.contentPanel.paintImmediately(this.getProgressBar().getBounds());
}
}

View File

@@ -0,0 +1,169 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.gui;
import forge.game.card.Card;
import forge.screens.match.VMatchUI;
import forge.screens.match.views.VField;
import forge.view.arcane.CardPanel;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* <p>
* GuiUtils class.
* </p>
*
* @author Forge
* @version $Id: GuiUtils.java 24769 2014-02-09 13:56:04Z Hellfish $
*/
public final class GuiUtils {
private GuiUtils() {
throw new AssertionError();
}
/**
* Attempts to create a font from a filename. Concise error reported if
* exceptions found.
*
* @param filename
* String
* @return Font
*/
public static Font newFont(final String filename) {
final File file = new File(filename);
Font ttf = null;
try {
ttf = Font.createFont(Font.TRUETYPE_FONT, file);
} catch (final FontFormatException e) {
System.err.println("GuiUtils > newFont: bad font format \"" + filename + "\"");
} catch (final IOException e) {
System.err.println("GuiUtils > newFont: can't find \"" + filename + "\"");
}
return ttf;
}
/**
* Clear all visually highlighted card panels on the battlefield.
*/
public static void clearPanelSelections() {
List<VField> view = VMatchUI.SINGLETON_INSTANCE.getFieldViews();
for (VField v : view) {
for (CardPanel p : v.getTabletop().getCardPanels()) {
p.setSelected(false);
}
}
}
/**
* Highlight a card on the playfield.
*
* @param c
* a card to be highlighted
*/
public static void setPanelSelection(final Card c) {
mainLoop:
for (VField v : VMatchUI.SINGLETON_INSTANCE.getFieldViews()) {
List<CardPanel> panels = v.getTabletop().getCardPanels();
for (CardPanel p : panels) {
if (p.getCard().equals(c)) {
p.setSelected(true);
break mainLoop;
}
}
}
}
private static final int minItemWidth = 100;
private static final int itemHeight = 25;
public static void setMenuItemSize(JMenuItem item) {
item.setPreferredSize(new Dimension(Math.max(item.getPreferredSize().width, minItemWidth), itemHeight));
}
public static JMenu createMenu(String label) {
if (label.startsWith("<html>")) { //adjust label if HTML
label = "<html>" + "<div style='height: " + itemHeight + "px; margin-top: 6px;'>" + label.substring(6, label.length() - 7) + "</div></html>";
}
JMenu menu = new JMenu(label);
setMenuItemSize(menu);
return menu;
}
public static JMenuItem createMenuItem(String label, KeyStroke accelerator, final Runnable onClick, boolean enabled, boolean bold) {
if (label.startsWith("<html>")) { //adjust label if HTML
label = "<html>" + "<div style='height: " + itemHeight + "px; margin-top: 6px;'>" + label.substring(6, label.length() - 7) + "</div></html>";
}
JMenuItem item = new JMenuItem(label);
item.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
if (null != onClick) {
onClick.run();
}
}
});
item.setEnabled(enabled);
item.setAccelerator(accelerator);
if (bold) {
item.setFont(item.getFont().deriveFont(Font.BOLD));
}
setMenuItemSize(item);
return item;
}
public static void addMenuItem(JPopupMenu parent, String label, KeyStroke accelerator, Runnable onClick) {
parent.add(createMenuItem(label, accelerator, onClick, true, false));
}
public static void addMenuItem(JMenuItem parent, String label, KeyStroke accelerator, Runnable onClick) {
parent.add(createMenuItem(label, accelerator, onClick, true, false));
}
public static void addMenuItem(JPopupMenu parent, String label, KeyStroke accelerator, Runnable onClick, boolean enabled) {
parent.add(createMenuItem(label, accelerator, onClick, enabled, false));
}
public static void addMenuItem(JMenuItem parent, String label, KeyStroke accelerator, Runnable onClick, boolean enabled) {
parent.add(createMenuItem(label, accelerator, onClick, enabled, false));
}
public static void addMenuItem(JPopupMenu parent, String label, KeyStroke accelerator, Runnable onClick, boolean enabled, boolean bold) {
parent.add(createMenuItem(label, accelerator, onClick, enabled, bold));
}
public static void addMenuItem(JMenuItem parent, String label, KeyStroke accelerator, Runnable onClick, boolean enabled, boolean bold) {
parent.add(createMenuItem(label, accelerator, onClick, enabled, bold));
}
public static void addSeparator(JPopupMenu parent) {
parent.add(new JSeparator());
}
public static void addSeparator(JMenuItem parent) {
parent.add(new JSeparator());
}
}

View File

@@ -0,0 +1,988 @@
/*
* Forge: Play Magic: the Gathering.
* Copyright (c) 2013 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 <http://www.gnu.org/licenses/>.
*/
package forge.gui;
import forge.UiCommand;
import forge.assets.FSkinProp;
import forge.error.BugReporter;
import forge.gui.ImportSourceAnalyzer.OpType;
import forge.properties.ForgeConstants;
import forge.toolbox.*;
import net.miginfocom.swing.MigLayout;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import javax.swing.*;
import javax.swing.Timer;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.*;
import java.util.List;
import java.util.concurrent.ConcurrentSkipListMap;
/**
* This class implements an overlay-based dialog that imports data from a user-selected directory
* into the correct locations in the user and cache directories. There is a lot of I/O and data
* processing done in this class, so most operations are asynchronous.
*/
public class ImportDialog {
private final FButton _btnStart;
private final FButton _btnCancel;
private final FLabel _btnChooseDir;
private final FPanel _topPanel;
private final JPanel _selectionPanel;
// volatile since it is checked from multiple threads
private volatile boolean _cancel;
@SuppressWarnings("serial")
public ImportDialog(String forcedSrcDir, final Runnable onDialogClose) {
_topPanel = new FPanel(new MigLayout("insets dialog, gap 0, center, wrap, fill"));
_topPanel.setOpaque(false);
_topPanel.setBackgroundTexture(FSkin.getIcon(FSkinProp.BG_TEXTURE));
final boolean isMigration = !StringUtils.isEmpty(forcedSrcDir);
// header
_topPanel.add(new FLabel.Builder().text((isMigration ? "Migrate" : "Import") + " profile data").fontSize(15).build(), "center");
// add some help text if this is for the initial data migration
if (isMigration) {
FPanel blurbPanel = new FPanel(new MigLayout("insets panel, gap 10, fill"));
blurbPanel.setOpaque(false);
JPanel blurbPanelInterior = new JPanel(new MigLayout("insets dialog, gap 10, center, wrap, fill"));
blurbPanelInterior.setOpaque(false);
blurbPanelInterior.add(new FLabel.Builder().text("<html><b>What's this?</b></html>").build(), "growx, w 50:50:");
blurbPanelInterior.add(new FLabel.Builder().text(
"<html>Over the last several years, people have had to jump through a lot of hoops to" +
" update to the most recent version. We hope to reduce this workload to a point where a new" +
" user will find that it is fairly painless to update. In order to make this happen, Forge" +
" has changed where it stores your data so that it is outside of the program installation directory." +
" This way, when you upgrade, you will no longer need to import your data every time to get things" +
" working. There are other benefits to having user data separate from program data, too, and it" +
" lays the groundwork for some cool new features.</html>").build(), "growx, w 50:50:");
blurbPanelInterior.add(new FLabel.Builder().text("<html><b>So where's my data going?</b></html>").build(), "growx, w 50:50:");
blurbPanelInterior.add(new FLabel.Builder().text(
"<html>Forge will now store your data in the same place as other applications on your system." +
" Specifically, your personal data, like decks, quest progress, and program preferences will be" +
" stored in <b>" + ForgeConstants.USER_DIR + "</b> and all downloaded content, such as card pictures," +
" skins, and quest world prices will be under <b>" + ForgeConstants.CACHE_DIR + "</b>. If, for whatever" +
" reason, you need to set different paths, cancel out of this dialog, exit Forge, and find the <b>" +
ForgeConstants.PROFILE_TEMPLATE_FILE + "</b> file in the program installation directory. Copy or rename" +
" it to <b>" + ForgeConstants.PROFILE_FILE + "</b> and edit the paths inside it. Then restart Forge and use" +
" this dialog to move your data to the paths that you set. Keep in mind that if you install a future" +
" version of Forge into a different directory, you'll need to copy this file over so Forge will know" +
" where to find your data.</html>").build(), "growx, w 50:50:");
blurbPanelInterior.add(new FLabel.Builder().text(
"<html><b>Remember, your data won't be available until you complete this step!</b></html>").build(), "growx, w 50:50:");
FScrollPane blurbScroller = new FScrollPane(blurbPanelInterior, true,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
blurbPanel.add(blurbScroller, "hmin 150, growy, growx, center, gap 0 0 5 5");
_topPanel.add(blurbPanel, "gap 10 10 20 0, growy, growx, w 50:50:");
}
// import source widgets
JPanel importSourcePanel = new JPanel(new MigLayout("insets 0, gap 10"));
importSourcePanel.setOpaque(false);
importSourcePanel.add(new FLabel.Builder().text("Import from:").build());
final FTextField txfSrc = new FTextField.Builder().readonly().build();
importSourcePanel.add(txfSrc, "pushx, growx");
_btnChooseDir = new FLabel.ButtonBuilder().text("Choose directory...").enabled(!isMigration).build();
final JFileChooser _fileChooser = new JFileChooser();
_fileChooser.setMultiSelectionEnabled(false);
_fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
_btnChooseDir.setCommand(new UiCommand() {
@Override public void run() {
// bring up a file open dialog and, if the OK button is selected, apply the filename
// to the import source text field
if (JFileChooser.APPROVE_OPTION == _fileChooser.showOpenDialog(JOptionPane.getRootFrame())) {
File f = _fileChooser.getSelectedFile();
if (!f.canRead()) {
FOptionPane.showErrorDialog("Cannot access selected directory (Permission denied).");
}
else {
txfSrc.setText(f.getAbsolutePath());
}
}
}
});
importSourcePanel.add(_btnChooseDir, "h pref+8!, w pref+12!");
// add change handler to the import source text field that starts up a
// new analyzer. it also interacts with the current active analyzer,
// if any, to make sure it cancels out before the new one is initiated
txfSrc.getDocument().addDocumentListener(new DocumentListener() {
boolean _analyzerActive; // access synchronized on _onAnalyzerDone
String prevText;
private final Runnable _onAnalyzerDone = new Runnable() {
public synchronized void run() {
_analyzerActive = false;
notify();
}
};
@Override public void removeUpdate(DocumentEvent e) { }
@Override public void changedUpdate(DocumentEvent e) { }
@Override public void insertUpdate(DocumentEvent e) {
// text field is read-only, so the only time this will get updated
// is when _btnChooseDir does it
final String text = txfSrc.getText();
if (text.equals(prevText)) {
// only restart the analyzer if the directory has changed
return;
}
prevText = text;
// cancel any active analyzer
_cancel = true;
if (!text.isEmpty()) {
// ensure we don't get two instances of this function running at the same time
_btnChooseDir.setEnabled(false);
// re-disable the start button. it will be enabled if the previous analyzer has
// already successfully finished
_btnStart.setEnabled(false);
// we have to wait in a background thread since we can't block in the GUI thread
SwingWorker<Void, Void> analyzerStarter = new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
// wait for active analyzer (if any) to quit
synchronized (_onAnalyzerDone) {
while (_analyzerActive) {
_onAnalyzerDone.wait();
}
}
return null;
}
// executes in gui event loop thread
@Override
protected void done() {
_cancel = false;
synchronized (_onAnalyzerDone) {
// this will populate the panel with data selection widgets
_AnalyzerUpdater analyzer = new _AnalyzerUpdater(text, _onAnalyzerDone, isMigration);
analyzer.run();
_analyzerActive = true;
}
if (!isMigration) {
// only enable the directory choosing button if this is not a migration dialog
// since in that case we're permanently locked to the starting directory
_btnChooseDir.setEnabled(true);
}
}
};
analyzerStarter.execute();
}
}
});
_topPanel.add(importSourcePanel, "gaptop 20, pushx, growx");
// prepare import selection panel (will be cleared and filled in later by an analyzer)
_selectionPanel = new JPanel();
_selectionPanel.setOpaque(false);
_topPanel.add(_selectionPanel, "growx, growy, gaptop 10");
// action button widgets
final Runnable cleanup = new Runnable() {
@Override public void run() { SOverlayUtils.hideOverlay(); }
};
_btnStart = new FButton("Start import");
_btnStart.setEnabled(false);
_btnCancel = new FButton("Cancel");
_btnCancel.addActionListener(new ActionListener() {
@Override public void actionPerformed(ActionEvent e) {
_cancel = true;
cleanup.run();
if (null != onDialogClose) {
onDialogClose.run();
}
}
});
JPanel southPanel = new JPanel(new MigLayout("ax center"));
southPanel.setOpaque(false);
southPanel.add(_btnStart, "center, w pref+144!, h pref+12!");
southPanel.add(_btnCancel, "center, w pref+144!, h pref+12!, gap 72");
_topPanel.add(southPanel, "growx");
JPanel overlay = FOverlay.SINGLETON_INSTANCE.getPanel();
overlay.setLayout(new MigLayout("insets 0, gap 0, wrap, ax center, ay center"));
overlay.add(_topPanel, "w 500::90%, h 100::90%");
SOverlayUtils.showOverlay();
// focus cancel button after the dialog is shown
SwingUtilities.invokeLater(new Runnable() {
@Override public void run() { _btnCancel.requestFocusInWindow(); }
});
// if our source dir is provided, set the text, which will fire off an analyzer
if (isMigration) {
File srcDirFile = new File(forcedSrcDir);
txfSrc.setText(srcDirFile.getAbsolutePath());
}
}
// encapsulates the choices in the combobox for choosing the destination paths for
// decks of unknown type
private class _UnknownDeckChoice {
public final String name;
public final String path;
public _UnknownDeckChoice(String name0, String path0) {
name = name0;
path = path0;
}
@Override public String toString() { return name; }
}
// this class owns the import selection widgets and bridges them with the running
// MigrationSourceAnalyzer instance
private class _AnalyzerUpdater extends SwingWorker<Void, Void> {
// associates a file operation type with its enablement checkbox and the set
// of file move/copy operations that enabling it would entail
private final Map<OpType, Pair<FCheckBox, ? extends Map<File, File>>> _selections =
new HashMap<OpType, Pair<FCheckBox, ? extends Map<File, File>>>();
// attached to all changeable widgets to keep the UI in sync
private final ChangeListener _stateChangedListener = new ChangeListener() {
@Override public void stateChanged(ChangeEvent arg0) { _updateUI(); }
};
private final String _srcDir;
private final Runnable _onAnalyzerDone;
private final boolean _isMigration;
private final FLabel _unknownDeckLabel;
private final FComboBoxWrapper<_UnknownDeckChoice> _unknownDeckCombo;
private final FCheckBox _moveCheckbox;
private final FCheckBox _overwriteCheckbox;
private final JTextArea _operationLog;
private final JScrollPane _operationLogScroller;
private final JProgressBar _progressBar;
// updates the _operationLog widget asynchronously to keep the UI responsive
private final _OperationLogAsyncUpdater _operationLogUpdater;
public _AnalyzerUpdater(String srcDir, Runnable onAnalyzerDone, boolean isMigration) {
_srcDir = srcDir;
_onAnalyzerDone = onAnalyzerDone;
_isMigration = isMigration;
_selectionPanel.removeAll();
_selectionPanel.setLayout(new MigLayout("insets 0, gap 5, wrap, fill"));
JPanel cbPanel = new JPanel(new MigLayout("insets 0, gap 5"));
cbPanel.setOpaque(false);
// add deck selections
JPanel knownDeckPanel = new JPanel(new MigLayout("insets 0, gap 5, wrap 2"));
knownDeckPanel.setOpaque(false);
knownDeckPanel.add(new FLabel.Builder().text("Decks").build(), "wrap");
_addSelectionWidget(knownDeckPanel, OpType.CONSTRUCTED_DECK, "Constructed decks");
_addSelectionWidget(knownDeckPanel, OpType.DRAFT_DECK, "Draft decks");
_addSelectionWidget(knownDeckPanel, OpType.PLANAR_DECK, "Planar decks");
_addSelectionWidget(knownDeckPanel, OpType.SCHEME_DECK, "Scheme decks");
_addSelectionWidget(knownDeckPanel, OpType.SEALED_DECK, "Sealed decks");
_addSelectionWidget(knownDeckPanel, OpType.UNKNOWN_DECK, "Unknown decks");
JPanel unknownDeckPanel = new JPanel(new MigLayout("insets 0, gap 5"));
unknownDeckPanel.setOpaque(false);
_unknownDeckCombo = new FComboBoxWrapper<_UnknownDeckChoice>();
_unknownDeckCombo.addItem(new _UnknownDeckChoice("Constructed", ForgeConstants.DECK_CONSTRUCTED_DIR));
_unknownDeckCombo.addItem(new _UnknownDeckChoice("Draft", ForgeConstants.DECK_DRAFT_DIR));
_unknownDeckCombo.addItem(new _UnknownDeckChoice("Planar", ForgeConstants.DECK_PLANE_DIR));
_unknownDeckCombo.addItem(new _UnknownDeckChoice("Scheme", ForgeConstants.DECK_SCHEME_DIR));
_unknownDeckCombo.addItem(new _UnknownDeckChoice("Sealed", ForgeConstants.DECK_SEALED_DIR));
_unknownDeckCombo.addActionListener(new ActionListener() {
@Override public void actionPerformed(ActionEvent arg0) { _updateUI(); }
});
_unknownDeckLabel = new FLabel.Builder().text("Treat unknown decks as:").build();
unknownDeckPanel.add(_unknownDeckLabel);
_unknownDeckCombo.addTo(unknownDeckPanel);
knownDeckPanel.add(unknownDeckPanel, "span");
cbPanel.add(knownDeckPanel, "aligny top");
// add other userDir data elements
JPanel dataPanel = new JPanel(new MigLayout("insets 0, gap 5, wrap"));
dataPanel.setOpaque(false);
dataPanel.add(new FLabel.Builder().text("Other data").build());
_addSelectionWidget(dataPanel, OpType.GAUNTLET_DATA, "Gauntlet data");
_addSelectionWidget(dataPanel, OpType.QUEST_DATA, "Quest saves");
_addSelectionWidget(dataPanel, OpType.PREFERENCE_FILE, "Preference files");
cbPanel.add(dataPanel, "aligny top");
// add cacheDir data elements
JPanel cachePanel = new JPanel(new MigLayout("insets 0, gap 5, wrap 2"));
cachePanel.setOpaque(false);
cachePanel.add(new FLabel.Builder().text("Cached data").build(), "wrap");
_addSelectionWidget(cachePanel, OpType.DEFAULT_CARD_PIC, "Default card pics");
_addSelectionWidget(cachePanel, OpType.SET_CARD_PIC, "Set-specific card pics");
_addSelectionWidget(cachePanel, OpType.TOKEN_PIC, "Card token pics");
_addSelectionWidget(cachePanel, OpType.QUEST_PIC, "Quest-related pics");
_addSelectionWidget(cachePanel, OpType.DB_FILE, "Database files", true, null, "wrap");
_addSelectionWidget(cachePanel, OpType.POSSIBLE_SET_CARD_PIC,
"Import possible set pics from as-yet unsupported cards", false,
"<html>Picture files that are not recognized as belonging to any known card.<br>" +
"It could be that these pictures belong to cards that are not yet supported<br>" +
"by Forge. If you know this to be the case and want the pictures imported for<br>" +
"future use, select this option.<html>", "span");
cbPanel.add(cachePanel, "aligny top");
_selectionPanel.add(cbPanel, "center");
// add move/copy and overwrite checkboxes
JPanel ioOptionPanel = new JPanel(new MigLayout("insets 0, gap 10"));
ioOptionPanel.setOpaque(false);
_moveCheckbox = new FCheckBox("Remove source files after copy");
_moveCheckbox.setToolTipText("Move files into the data directories instead of just copying them");
_moveCheckbox.setSelected(isMigration);
_moveCheckbox.addChangeListener(_stateChangedListener);
ioOptionPanel.add(_moveCheckbox);
_overwriteCheckbox = new FCheckBox("Overwrite files in destination");
_overwriteCheckbox.setToolTipText("Overwrite existing data with the imported data");
_overwriteCheckbox.addChangeListener(_stateChangedListener);
ioOptionPanel.add(_overwriteCheckbox);
_selectionPanel.add(ioOptionPanel);
// add operation summary textfield
_operationLog = new JTextArea();
_operationLog.setFont(new Font("Monospaced", Font.PLAIN, 10));
_operationLog.setOpaque(false);
_operationLog.setWrapStyleWord(true);
_operationLog.setLineWrap(true);
_operationLog.setEditable(false);
// autoscroll when we set/add text unless the user has intentionally scrolled somewhere else
_operationLogScroller = new JScrollPane(_operationLog);
_operationLogScroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
new SmartScroller(_operationLogScroller);
_selectionPanel.add(_operationLogScroller, "w 400:400:, hmin 60, growy, growx");
// add progress bar
_progressBar = new JProgressBar();
_progressBar.setString("Preparing to analyze source directory...");
_progressBar.setStringPainted(true);
_selectionPanel.add(_progressBar, "w 100%!");
// start the op log updater
_operationLogUpdater = new _OperationLogAsyncUpdater(_selections, _operationLog);
_operationLogUpdater.start();
// set initial checkbox labels
_updateUI();
// resize the panel properly now that the _selectionPanel is filled in
_selectionPanel.getParent().validate();
_selectionPanel.getParent().invalidate();
}
private void _addSelectionWidget(JPanel parent, OpType type, String name) {
_addSelectionWidget(parent, type, name, true, null, null);
}
private void _addSelectionWidget(JPanel parent, OpType type, String name, boolean selected,
String tooltip, String constraints) {
FCheckBox cb = new FCheckBox();
cb.setName(name);
cb.setSelected(selected);
cb.setToolTipText(tooltip);
cb.addChangeListener(_stateChangedListener);
// use a skip list map instead of a regular hashmap so that the files are sorted
// alphabetically in the logs. note that this is a concurrent data structure
// since it will be modified and read simultaneously by different threads
_selections.put(type, Pair.of(cb, new ConcurrentSkipListMap<File, File>()));
parent.add(cb, constraints);
}
// must be called from GUI event loop thread
private void _updateUI() {
// update checkbox text labels with current totals
Set<OpType> selectedOptions = new HashSet<OpType>();
for (Map.Entry<OpType, Pair<FCheckBox, ? extends Map<File, File>>> entry : _selections.entrySet()) {
Pair<FCheckBox, ? extends Map<File, File>> selection = entry.getValue();
FCheckBox cb = selection.getLeft();
if (cb.isSelected()) {
selectedOptions.add(entry.getKey());
}
cb.setText(String.format("%s (%d)", cb.getName(), selection.getRight().size()));
}
// asynchronously update the text in the op log, which may be many tens of thousands of lines long
// if this were done synchronously the UI would slow to a crawl
_operationLogUpdater.requestUpdate(selectedOptions, (_UnknownDeckChoice)_unknownDeckCombo.getSelectedItem(),
_moveCheckbox.isSelected(), _overwriteCheckbox.isSelected());
}
@Override
protected Void doInBackground() throws Exception {
Timer timer = null;
try {
Map<OpType, Map<File, File>> selections = new HashMap<OpType, Map<File, File>>();
for (Map.Entry<OpType, Pair<FCheckBox, ? extends Map<File, File>>> entry : _selections.entrySet()) {
selections.put(entry.getKey(), entry.getValue().getRight());
}
ImportSourceAnalyzer.AnalysisCallback cb = new ImportSourceAnalyzer.AnalysisCallback() {
@Override
public boolean checkCancel() { return _cancel; }
@Override
public void addOp(OpType type, File src, File dest) {
// add to concurrent map
_selections.get(type).getRight().put(src, dest);
}
};
final ImportSourceAnalyzer msa = new ImportSourceAnalyzer(_srcDir, cb);
final int numFilesToAnalyze = msa.getNumFilesToAnalyze();
// update only once every half-second so we're not flooding the UI with updates
timer = new Timer(500, null);
timer.setInitialDelay(100);
final Timer finalTimer = timer;
timer.addActionListener(new ActionListener() {
@Override public void actionPerformed(ActionEvent arg0) {
if (_cancel) {
finalTimer.stop();
return;
}
// timers run in the gui event loop, so it's ok to interact with widgets
_progressBar.setValue(msa.getNumFilesAnalyzed());
_updateUI();
// allow the the panel to resize to accommodate additional text
_selectionPanel.getParent().validate();
_selectionPanel.getParent().invalidate();
}
});
// update the progress bar widget from the GUI event loop
SwingUtilities.invokeLater(new Runnable() {
@Override public void run() {
if (_cancel) { return; }
_progressBar.setString("Analyzing...");
_progressBar.setMaximum(numFilesToAnalyze);
_progressBar.setValue(0);
_progressBar.setIndeterminate(false);
// start update timer
finalTimer.start();
}
});
// does not return until analysis is complete or has been canceled
msa.doAnalysis();
} catch (final Exception e) {
_cancel = true;
SwingUtilities.invokeLater(new Runnable() {
@Override public void run() {
_progressBar.setString("Error");
BugReporter.reportException(e);
}
});
} finally {
// ensure the UI update timer is stopped after analysis is complete
if (null != timer) {
timer.stop();
}
}
return null;
}
// executes in gui event loop thread
@Override
protected void done() {
if (!_cancel) {
_progressBar.setValue(_progressBar.getMaximum());
_updateUI();
_progressBar.setString("Analysis complete");
// clear any previously-set action listeners on the start button
// in case we've previously completed an analysis but changed the directory
// instead of starting the import
for (ActionListener a : _btnStart.getActionListeners()) {
_btnStart.removeActionListener(a);
}
// deselect and disable all options that have 0 operations associated with
// them to highlight the important options
for (Pair<FCheckBox, ? extends Map<File, File>> p : _selections.values()) {
FCheckBox cb = p.getLeft();
if (0 == p.getRight().size()) {
cb.removeChangeListener(_stateChangedListener);
cb.setSelected(false);
cb.setEnabled(false);
}
}
if (0 == _selections.get(OpType.UNKNOWN_DECK).getRight().size()) {
_unknownDeckLabel.setEnabled(false);
_unknownDeckCombo.setEnabled(false);
}
// set up the start button to start the prepared import on click
_btnStart.addActionListener(new ActionListener() {
@Override public void actionPerformed(ActionEvent arg0) {
// if this is a migration, warn if active settings will not complete a migration and give the
// user an option to fix
if (_isMigration) {
// assemble a list of selections that need to be selected to complete a full migration
List<String> unselectedButShouldBe = new ArrayList<String>();
for (Map.Entry<OpType, Pair<FCheckBox, ? extends Map<File, File>>> entry : _selections.entrySet()) {
if (OpType.POSSIBLE_SET_CARD_PIC == entry.getKey()) {
continue;
}
// add name to list if checkbox is unselected, but contains operations
Pair<FCheckBox, ? extends Map<File, File>> p = entry.getValue();
FCheckBox cb = p.getLeft();
if (!cb.isSelected() && 0 < p.getRight().size()) {
unselectedButShouldBe.add(cb.getName());
}
}
if (!unselectedButShouldBe.isEmpty() || !_moveCheckbox.isSelected()) {
StringBuilder sb = new StringBuilder("<html>");
if (!unselectedButShouldBe.isEmpty()) {
sb.append("It looks like the following options are not selected, which will result in an incomplete migration:");
sb.append("<ul>");
for (String cbName : unselectedButShouldBe) {
sb.append("<li><b>").append(cbName).append("</b></li>");
}
sb.append("</ul>");
}
if (!_moveCheckbox.isSelected()) {
sb.append(unselectedButShouldBe.isEmpty() ? "It " : "It also ").append("looks like the <b>");
sb.append(_moveCheckbox.getText()).append("</b> option is not selected.<br><br>");
}
sb.append("You can continue anyway, but the migration will be incomplete, and the data migration prompt<br>");
sb.append("will come up again the next time you start Forge in order to migrate the remaining files<br>");
sb.append("unless you move or delete them manually.</html>");
String[] options = { "Whoops, let me fix that!", "Continue with the import, I know what I'm doing." };
int chosen = FOptionPane.showOptionDialog(sb.toString(), "Migration warning", FOptionPane.WARNING_ICON, options);
if (chosen != 1) {
// i.e. option 0 was chosen or the dialog was otherwise closed
return;
}
}
}
// ensure no other actions (except for cancel) can be taken while the import is in progress
_btnStart.setEnabled(false);
_btnChooseDir.setEnabled(false);
for (Pair<FCheckBox, ? extends Map<File, File>> selection : _selections.values()) {
selection.getLeft().setEnabled(false);
}
_unknownDeckCombo.setEnabled(false);
_moveCheckbox.setEnabled(false);
_overwriteCheckbox.setEnabled(false);
// stop updating the operation log -- the importer needs it now
_operationLogUpdater.requestStop();
// jump to the bottom of the log text area so it starts autoscrolling again
// note that since it is controlled by a SmartScroller, just setting the caret position will not work
JScrollBar scrollBar = _operationLogScroller.getVerticalScrollBar();
scrollBar.setValue(scrollBar.getMaximum());
// start importing!
_Importer importer = new _Importer(
_srcDir, _selections, _unknownDeckCombo, _operationLog, _progressBar,
_moveCheckbox.isSelected(), _overwriteCheckbox.isSelected());
importer.run();
_btnCancel.requestFocusInWindow();
}
});
// import ready to proceed: enable the start button
_btnStart.setEnabled(true);
}
// report to the Choose Directory button that this analysis run has stopped
_onAnalyzerDone.run();
}
}
// asynchronously iterates through the given concurrent maps and populates the operation log with
// the proposed operations
private class _OperationLogAsyncUpdater extends Thread {
final Map<OpType, Map<File, File>> _selections;
final JTextArea _operationLog; // safe to set text from another thread
// synchronized-access data
private int _updateCallCnt = 0;
private Set<OpType> _selectedOptions;
private _UnknownDeckChoice _unknownDeckChoice;
private boolean _isMove;
private boolean _isOverwrite;
private boolean _stop;
// only accessed from the event loop thread
int _maxLogLength = 0;
public _OperationLogAsyncUpdater(Map<OpType, Pair<FCheckBox, ? extends Map<File, File>>> selections, JTextArea operationLog) {
super("OperationLogUpdater");
setDaemon(true);
_selections = new HashMap<OpType, Map<File, File>>();
_operationLog = operationLog;
// remove references to FCheckBox when populating map -- we can't safely access it from a thread
// anyway and it's better to keep our data structure clean to prevent mistakes
for (Map.Entry<OpType, Pair<FCheckBox, ? extends Map<File, File>>> entry : selections.entrySet()) {
_selections.put(entry.getKey(), entry.getValue().getRight());
}
}
// updates the synchronized data with values for the next iteration in _run
public synchronized void requestUpdate(
Set<OpType> selectedOptions, _UnknownDeckChoice unknownDeckChoice, boolean isMove, boolean isOverwrite) {
++_updateCallCnt;
_selectedOptions = selectedOptions;
_unknownDeckChoice = unknownDeckChoice;
_isMove = isMove;
_isOverwrite = isOverwrite;
// notify waiter
notify();
}
public synchronized void requestStop() {
_stop = true;
// notify waiter
notify();
}
private void _run() throws InterruptedException {
int lastUpdateCallCnt = _updateCallCnt;
Set<OpType> selectedOptions;
_UnknownDeckChoice unknownDeckChoice;
boolean isMove;
boolean isOverwrite;
while (true) {
synchronized (this) {
// can't check _stop in the while condition since we have to do it in a synchronized block
if (_stop) { break; }
// if we're stopped while looping here, run through the update one last time
// before returning
while (lastUpdateCallCnt == _updateCallCnt && !_stop) {
wait();
}
// safely copy synchronized data to local values that we will use for this runthrough
lastUpdateCallCnt = _updateCallCnt;
selectedOptions = _selectedOptions;
unknownDeckChoice = _unknownDeckChoice;
isMove = _isMove;
isOverwrite = _isOverwrite;
}
// build operation log
final StringBuilder log = new StringBuilder();
int totalOps = 0;
for (OpType opType : selectedOptions) {
Map<File, File> ops = _selections.get(opType);
totalOps += ops.size();
for (Map.Entry<File, File> op : ops.entrySet()) {
File dest = op.getValue();
if (OpType.UNKNOWN_DECK == opType) {
dest = new File(unknownDeckChoice.path, dest.getName());
}
log.append(op.getKey().getAbsolutePath()).append(" -> ");
log.append(dest.getAbsolutePath()).append("\n");
}
}
// append summary
if (0 < totalOps) {
log.append("\n");
}
log.append("Prepared to ").append(isMove ? "move" : "copy");
log.append(" ").append(totalOps).append(" files\n");
log.append(isOverwrite ? "O" : "Not o").append("verwriting existing files");
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
String logText = log.toString();
// setText is thread-safe, but the resizing is not, so might as well do this in the swing event loop thread
_operationLog.setText(log.toString());
if (_maxLogLength < logText.length()) {
_maxLogLength = logText.length();
// resize the panel properly for the new log contents
_selectionPanel.getParent().validate();
_selectionPanel.getParent().invalidate();
_topPanel.getParent().validate();
_topPanel.getParent().invalidate();
}
}
});
}
}
@Override
public void run() {
try { _run(); } catch (final InterruptedException e) {
_cancel = true;
SwingUtilities.invokeLater(new Runnable() {
@Override public void run() {
// we never interrupt the thread, so this is not expected to happen
BugReporter.reportException(e);
}
});
}
}
}
// asynchronously completes the specified I/O operations and updates the progress bar and operation log
private class _Importer extends SwingWorker<Void, Void> {
private final String _srcDir;
private final Map<File, File> _operations;
private final JTextArea _operationLog;
private final JProgressBar _progressBar;
private final boolean _move;
private final boolean _overwrite;
public _Importer(String srcDir, Map<OpType, Pair<FCheckBox, ? extends Map<File, File>>> selections, FComboBoxWrapper<_UnknownDeckChoice> unknownDeckCombo,
JTextArea operationLog, JProgressBar progressBar, boolean move, boolean overwrite) {
_srcDir = srcDir;
_operationLog = operationLog;
_progressBar = progressBar;
_move = move;
_overwrite = overwrite;
// build local operations map that only includes data that we can access from the background thread
// use a tree map to maintain alphabetical order
_operations = new TreeMap<File, File>();
for (Map.Entry<OpType, Pair<FCheckBox, ? extends Map<File, File>>> entry : selections.entrySet()) {
Pair<FCheckBox, ? extends Map<File, File>> selection = entry.getValue();
if (selection.getLeft().isSelected()) {
if (OpType.UNKNOWN_DECK != entry.getKey()) {
_operations.putAll(selection.getRight());
} else {
// map unknown decks to selected directory
for (Map.Entry<File, File> op : selection.getRight().entrySet()) {
_UnknownDeckChoice choice = (_UnknownDeckChoice)unknownDeckCombo.getSelectedItem();
_operations.put(op.getKey(), new File(choice.path, op.getValue().getName()));
}
}
}
}
// set progress bar bounds
_progressBar.setString(_move ? "Moving files..." : "Copying files...");
_progressBar.setMinimum(0);
_progressBar.setMaximum(_operations.size());
}
@Override
protected Void doInBackground() throws Exception {
try {
// working with textbox text is thread safe
_operationLog.setText("");
// only update the text box once very half second, but make the first
// update after only 100ms
final long updateIntervalMs = 500;
long lastUpdateTimestampMs = System.currentTimeMillis() - 400;
StringBuffer opLogBuf = new StringBuffer();
// only update the progress bar when we expect the visual value to change
final long progressInterval = Math.max(1, _operations.size() / _progressBar.getWidth());
// the length of the prefix to remove from source paths
final int srcPathPrefixLen;
if (_srcDir.endsWith("/") || _srcDir.endsWith(File.separator)) {
srcPathPrefixLen = _srcDir.length();
} else
{
srcPathPrefixLen = _srcDir.length() + 1;
}
// stats maintained during import sequence
int numOps = 0;
int numExisting = 0;
int numSucceeded = 0;
int numFailed = 0;
for (Map.Entry<File, File> op : _operations.entrySet()) {
if (_cancel) { break; }
final int curOpNum = ++numOps;
if (0 == curOpNum % progressInterval) {
SwingUtilities.invokeLater(new Runnable() {
@Override public void run() {
if (_cancel) { return; }
_progressBar.setValue(curOpNum);
}
});
}
long curTimeMs = System.currentTimeMillis();
if (updateIntervalMs <= curTimeMs - lastUpdateTimestampMs) {
lastUpdateTimestampMs = curTimeMs;
// working with textbox text is thread safe
_operationLog.append(opLogBuf.toString());
opLogBuf.setLength(0);
}
File srcFile = op.getKey();
File destFile = op.getValue();
try {
// simplify logged source path and log next attempted operation
String srcPath = srcFile.getAbsolutePath();
// I doubt that the srcPath will start with anything other than _srcDir, even with symlinks,
// hardlinks, or Windows junctioned nodes, but it's better to be safe than to have malformed output
if (srcPath.startsWith(_srcDir)) {
srcPath = srcPath.substring(srcPathPrefixLen);
}
opLogBuf.append(_move ? "Moving " : "Copying ").append(srcPath).append(" -> ");
opLogBuf.append(destFile.getAbsolutePath()).append("\n");
if (!destFile.exists()) {
_copyFile(srcFile, destFile, _move);
} else {
if (_overwrite) {
opLogBuf.append(" Destination file exists; overwriting\n");
_copyFile(srcFile, destFile, _move);
} else {
opLogBuf.append(" Destination file exists; skipping copy\n");
}
++numExisting;
}
if (_move) {
// source file may have been deleted already if _copyFile was called
srcFile.delete();
opLogBuf.append(" Removed source file after successful copy\n");
}
++numSucceeded;
} catch (IOException e) {
opLogBuf.append(" Operation failed: ").append(e.getMessage()).append("\n");
++numFailed;
}
}
// append summary footer
opLogBuf.append("\nImport complete: ");
opLogBuf.append(numSucceeded).append(" operation").append(1 == numSucceeded ? "" : "s").append(" succeeded, ");
opLogBuf.append(numFailed).append(" error").append(1 == numFailed ? "" : "s");
if (0 < numExisting) {
opLogBuf.append(", ").append(numExisting);
if (_overwrite) {
opLogBuf.append(" existing destination files overwritten");
} else {
opLogBuf.append(" copy operations skipped due to existing destination files");
}
}
_operationLog.append(opLogBuf.toString());
} catch (final Exception e) {
_cancel = true;
// report any exceptions in a standard dialog
// note that regular I/O errors don't throw, they'll just be mentioned in the log
SwingUtilities.invokeLater(new Runnable() {
@Override public void run() {
_progressBar.setString("Error");
BugReporter.reportException(e);
}
});
}
return null;
}
@Override
protected void done() {
_btnCancel.requestFocusInWindow();
if (_cancel) { return; }
_progressBar.setValue(_progressBar.getMaximum());
_progressBar.setString("Import complete");
_btnCancel.setText("Done");
}
}
// when copying is required, uses java nio classes for ultra-fast I/O
private static void _copyFile(File srcFile, File destFile, boolean deleteSrcAfter) throws IOException {
destFile.getParentFile().mkdirs();
// if this is a move, try a simple rename first
if (deleteSrcAfter) {
if (srcFile.renameTo(destFile)) {
return;
}
}
if (!destFile.exists()) {
destFile.createNewFile();
}
FileChannel src = null;
FileChannel dest = null;
try {
src = new FileInputStream(srcFile).getChannel();
dest = new FileOutputStream(destFile).getChannel();
dest.transferFrom(src, 0, src.size());
} finally {
if (src != null) { src.close(); }
if (dest != null) { dest.close(); }
}
if (deleteSrcAfter) {
srcFile.delete();
}
}
}

View File

@@ -0,0 +1,660 @@
/*
* Forge: Play Magic: the Gathering.
* Copyright (c) 2013 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 <http://www.gnu.org/licenses/>.
*/
package forge.gui;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.assets.ImageUtil;
import forge.card.CardEdition;
import forge.card.CardRules;
import forge.item.IPaperCard;
import forge.item.PaperCard;
import forge.model.FModel;
import forge.properties.ForgeConstants;
import forge.util.FileUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.io.File;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
public class ImportSourceAnalyzer {
public static enum OpType {
CONSTRUCTED_DECK,
DRAFT_DECK,
PLANAR_DECK,
SCHEME_DECK,
SEALED_DECK,
UNKNOWN_DECK,
DEFAULT_CARD_PIC,
SET_CARD_PIC,
POSSIBLE_SET_CARD_PIC,
TOKEN_PIC,
QUEST_PIC,
GAUNTLET_DATA,
QUEST_DATA,
PREFERENCE_FILE,
DB_FILE
}
public static interface AnalysisCallback {
boolean checkCancel();
void addOp(OpType type, File src, File dest);
}
private final File _source;
private final AnalysisCallback _cb;
private final int _numFilesToAnalyze;
private int _numFilesAnalyzed;
public ImportSourceAnalyzer(String source, AnalysisCallback cb) {
_source = new File(source);
_cb = cb;
_numFilesToAnalyze = _countFiles(_source);
}
public int getNumFilesToAnalyze() { return _numFilesToAnalyze; }
public int getNumFilesAnalyzed() { return _numFilesAnalyzed; }
public void doAnalysis() {
_identifyAndAnalyze(_source);
}
private void _identifyAndAnalyze(File root) {
// see if we can figure out the likely identity of the source folder and
// dispatch to the best analysis subroutine to handle it
String dirname = root.getName();
if ("res".equalsIgnoreCase(dirname)) { _analyzeOldResDir(root); }
else if ("constructed".equalsIgnoreCase(dirname)) { _analyzeConstructedDeckDir(root); }
else if ("draft".equalsIgnoreCase(dirname)) { _analyzeDraftDeckDir(root); }
else if ("plane".equalsIgnoreCase(dirname) || "planar".equalsIgnoreCase(dirname)) { _analyzePlanarDeckDir(root); }
else if ("scheme".equalsIgnoreCase(dirname)) { _analyzeSchemeDeckDir(root); }
else if ("sealed".equalsIgnoreCase(dirname)) { _analyzeSealedDeckDir(root); }
else if (StringUtils.containsIgnoreCase(dirname, "deck")) { _analyzeDecksDir(root); }
else if ("gauntlet".equalsIgnoreCase(dirname)) { _analyzeGauntletDataDir(root); }
else if ("layouts".equalsIgnoreCase(dirname)) { _analyzeLayoutsDir(root); }
else if ("pics".equalsIgnoreCase(dirname)) { _analyzeCardPicsDir(root); }
else if ("pics_product".equalsIgnoreCase(dirname)) { _analyzeProductPicsDir(root); }
else if ("preferences".equalsIgnoreCase(dirname)) { _analyzePreferencesDir(root); }
else if ("quest".equalsIgnoreCase(dirname)) { _analyzeQuestDir(root); }
else if (null != FModel.getMagicDb().getEditions().get(dirname)) { _analyzeCardPicsSetDir(root); }
else {
// look at files in directory and make a semi-educated guess based on file extensions
int numUnhandledFiles = 0;
for (File file : root.listFiles()) {
if (_cb.checkCancel()) { return; }
if (file.isFile()) {
String filename = file.getName();
if (StringUtils.endsWithIgnoreCase(filename, ".dck")) {
_analyzeDecksDir(root);
numUnhandledFiles = 0;
break;
} else if (StringUtils.endsWithIgnoreCase(filename, ".jpg")) {
_analyzeCardPicsDir(root);
numUnhandledFiles = 0;
break;
}
++numUnhandledFiles;
} else if (file.isDirectory()) {
_identifyAndAnalyze(file);
}
}
_numFilesAnalyzed += numUnhandledFiles;
}
}
//////////////////////////////////////////////////////////////////////////
// pre-profile res dir
//
private void _analyzeOldResDir(File root) {
_analyzeDir(root, new _Analyzer() {
@Override boolean onDir(File dir) {
String dirname = dir.getName();
if ("decks".equalsIgnoreCase(dirname)) {
_analyzeDecksDir(dir);
} else if ("gauntlet".equalsIgnoreCase(dirname)) {
_analyzeGauntletDataDir(dir);
} else if ("layouts".equalsIgnoreCase(dirname)) {
_analyzeLayoutsDir(dir);
} else if ("pics".equalsIgnoreCase(dirname)) {
_analyzeCardPicsDir(dir);
} else if ("pics_product".equalsIgnoreCase(dirname)) {
_analyzeProductPicsDir(dir);
} else if ("preferences".equalsIgnoreCase(dirname)) {
_analyzePreferencesDir(dir);
} else if ("quest".equalsIgnoreCase(dirname)) {
_analyzeQuestDir(dir);
} else {
return false;
}
return true;
}
});
}
//////////////////////////////////////////////////////////////////////////
// decks
//
private void _analyzeDecksDir(File root) {
_analyzeDir(root, new _Analyzer() {
@Override void onFile(File file) {
// we don't really expect any files in here, but if we find a .dck file, add it to the unknown list
String filename = file.getName();
if (StringUtils.endsWithIgnoreCase(filename, ".dck")) {
File targetFile = new File(_lcaseExt(filename));
_cb.addOp(OpType.UNKNOWN_DECK, file, targetFile);
}
}
@Override boolean onDir(File dir) {
String dirname = dir.getName();
if ("constructed".equalsIgnoreCase(dirname)) {
_analyzeConstructedDeckDir(dir);
} else if ("cube".equalsIgnoreCase(dirname)) {
return false;
} else if ("draft".equalsIgnoreCase(dirname)) {
_analyzeDraftDeckDir(dir);
} else if ("plane".equalsIgnoreCase(dirname) || "planar".equalsIgnoreCase(dirname)) {
_analyzePlanarDeckDir(dir);
} else if ("scheme".equalsIgnoreCase(dirname)) {
_analyzeSchemeDeckDir(dir);
} else if ("sealed".equalsIgnoreCase(dirname)) {
_analyzeSealedDeckDir(dir);
} else {
_analyzeKnownDeckDir(dir, null, OpType.UNKNOWN_DECK);
}
return true;
}
});
}
private void _analyzeConstructedDeckDir(File root) {
_analyzeKnownDeckDir(root, ForgeConstants.DECK_CONSTRUCTED_DIR, OpType.CONSTRUCTED_DECK);
}
private void _analyzeDraftDeckDir(File root) {
_analyzeKnownDeckDir(root, ForgeConstants.DECK_DRAFT_DIR, OpType.DRAFT_DECK);
}
private void _analyzePlanarDeckDir(File root) {
_analyzeKnownDeckDir(root, ForgeConstants.DECK_PLANE_DIR, OpType.PLANAR_DECK);
}
private void _analyzeSchemeDeckDir(File root) {
_analyzeKnownDeckDir(root, ForgeConstants.DECK_SCHEME_DIR, OpType.SCHEME_DECK);
}
private void _analyzeSealedDeckDir(File root) {
_analyzeKnownDeckDir(root, ForgeConstants.DECK_SEALED_DIR, OpType.SEALED_DECK);
}
private void _analyzeKnownDeckDir(File root, final String targetDir, final OpType opType) {
_analyzeDir(root, new _Analyzer() {
@Override void onFile(File file) {
String filename = file.getName();
if (StringUtils.endsWithIgnoreCase(filename, ".dck")) {
File targetFile = new File(targetDir, _lcaseExt(filename));
if (!file.equals(targetFile)) {
_cb.addOp(opType, file, targetFile);
}
}
}
@Override boolean onDir(File dir) {
// if there's a dir beneath a known directory, assume the same kind of decks are in there
_analyzeKnownDeckDir(dir, targetDir, opType);
return true;
}
});
}
//////////////////////////////////////////////////////////////////////////
// gauntlet
//
private void _analyzeGauntletDataDir(File root) {
_analyzeDir(root, new _Analyzer() {
@Override void onFile(File file) {
// find *.dat files, but exclude LOCKED_*
String filename = file.getName();
if (StringUtils.endsWithIgnoreCase(filename, ".dat") && !filename.startsWith("LOCKED_")) {
File targetFile = new File(ForgeConstants.GAUNTLET_DIR.userPrefLoc, _lcaseExt(filename));
if (!file.equals(targetFile)) {
_cb.addOp(OpType.GAUNTLET_DATA, file, targetFile);
}
}
}
});
}
//////////////////////////////////////////////////////////////////////////
// layouts
//
private void _analyzeLayoutsDir(File root) {
_analyzeDir(root, new _Analyzer() {
@Override void onFile(File file) {
// find *_preferred.xml files
String filename = file.getName();
if (StringUtils.endsWithIgnoreCase(filename, "_preferred.xml")) {
File targetFile = new File(ForgeConstants.USER_PREFS_DIR,
file.getName().toLowerCase(Locale.ENGLISH).replace("_preferred", ""));
_cb.addOp(OpType.PREFERENCE_FILE, file, targetFile);
}
}
});
}
//////////////////////////////////////////////////////////////////////////
// default card pics
//
private static String _oldCleanString(String in) {
final StringBuffer out = new StringBuffer();
for (int i = 0; i < in.length(); i++) {
char c = in.charAt(i);
if ((c == ' ') || (c == '-')) {
out.append('_');
} else if (Character.isLetterOrDigit(c) || (c == '_')) {
out.append(c);
}
}
// usually we would want to pass Locale.ENGLISH to the toLowerCase() method to prevent unintentional
// character mangling on some system locales, but we want to replicate the old code here exactly
return out.toString().toLowerCase();
}
private void _addDefaultPicNames(PaperCard c, boolean backFace) {
CardRules card = c.getRules();
String urls = card.getPictureUrl(backFace);
if (StringUtils.isEmpty(urls)) { return; }
int numPics = 1 + StringUtils.countMatches(urls, "\\");
if (c.getArtIndex() > numPics) {
return;
}
String filenameBase = ImageUtil.getImageKey(c, backFace, false);
String filename = filenameBase + ".jpg";
boolean alreadyHadIt = null != _defaultPicNames.put(filename, filename);
if ( alreadyHadIt ) return;
// Do you shift artIndex by one here?
String newLastSymbol = 0 == c.getArtIndex() ? "" : String.valueOf(c.getArtIndex() /* + 1 */);
String oldFilename = _oldCleanString(filenameBase.replaceAll("[0-9]?(\\.full)?$", "")) + newLastSymbol + ".jpg";
//if ( numPics > 1 )
//System.out.printf("Will move %s -> %s%n", oldFilename, filename);
_defaultPicOldNameToCurrentName.put(oldFilename, filename);
}
private Map<String, String> _defaultPicNames;
private Map<String, String> _defaultPicOldNameToCurrentName;
private void _analyzeCardPicsDir(File root) {
if (null == _defaultPicNames) {
_defaultPicNames = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
_defaultPicOldNameToCurrentName = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
for (PaperCard c : FModel.getMagicDb().getCommonCards().getAllCards()) {
_addDefaultPicNames(c, false);
if (ImageUtil.hasBackFacePicture(c)) {
_addDefaultPicNames(c, true);
}
}
for (PaperCard c : FModel.getMagicDb().getVariantCards().getAllCards()) {
_addDefaultPicNames(c, false);
// variants never have backfaces
}
}
_analyzeListedDir(root, ForgeConstants.CACHE_CARD_PICS_DIR, new _ListedAnalyzer() {
@Override public String map(String filename) {
if (_defaultPicOldNameToCurrentName.containsKey(filename)) {
return _defaultPicOldNameToCurrentName.get(filename);
}
return _defaultPicNames.get(filename);
}
@Override public OpType getOpType(String filename) { return OpType.DEFAULT_CARD_PIC; }
@Override boolean onDir(File dir) {
if ("icons".equalsIgnoreCase(dir.getName())) {
_analyzeIconsPicsDir(dir);
} else if ("tokens".equalsIgnoreCase(dir.getName())) {
_analyzeTokenPicsDir(dir);
} else {
_analyzeCardPicsSetDir(dir);
}
return true;
}
});
}
//////////////////////////////////////////////////////////////////////////
// set card pics
//
private static void _addSetCards(Map<String, String> cardFileNames, Iterable<PaperCard> library, Predicate<PaperCard> filter) {
for (PaperCard c : Iterables.filter(library, filter)) {
String filename = ImageUtil.getImageKey(c, false, true) + ".jpg";
cardFileNames.put(filename, filename);
if (ImageUtil.hasBackFacePicture(c)) {
filename = ImageUtil.getImageKey(c, true, true) + ".jpg";
cardFileNames.put(filename, filename);
}
}
}
Map<String, Map<String, String>> _cardFileNamesBySet;
Map<String, String> _nameUpdates;
private void _analyzeCardPicsSetDir(File root) {
if (null == _cardFileNamesBySet) {
_cardFileNamesBySet = new TreeMap<String, Map<String, String>>(String.CASE_INSENSITIVE_ORDER);
for (CardEdition ce : FModel.getMagicDb().getEditions()) {
Map<String, String> cardFileNames = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
Predicate<PaperCard> filter = IPaperCard.Predicates.printedInSet(ce.getCode());
_addSetCards(cardFileNames, FModel.getMagicDb().getCommonCards().getAllCards(), filter);
_addSetCards(cardFileNames, FModel.getMagicDb().getVariantCards().getAllCards(), filter);
_cardFileNamesBySet.put(ce.getCode2(), cardFileNames);
}
// planar cards now don't have the ".full" part in their filenames
_nameUpdates = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
Predicate<PaperCard> predPlanes = new Predicate<PaperCard>() {
@Override
public boolean apply(PaperCard arg0) {
return arg0.getRules().getType().isPlane() || arg0.getRules().getType().isPhenomenon();
}
};
for (PaperCard c : Iterables.filter(FModel.getMagicDb().getVariantCards().getAllCards(), predPlanes)) {
String baseName = ImageUtil.getImageKey(c,false, true);
_nameUpdates.put(baseName + ".full.jpg", baseName + ".jpg");
if (ImageUtil.hasBackFacePicture(c)) {
baseName = ImageUtil.getImageKey(c, true, true);
_nameUpdates.put(baseName + ".full.jpg", baseName + ".jpg");
}
}
}
CardEdition.Collection editions = FModel.getMagicDb().getEditions();
String editionCode = root.getName();
CardEdition edition = editions.get(editionCode);
if (null == edition) {
// not a valid set name, skip
_numFilesAnalyzed += _countFiles(root);
return;
}
final String editionCode2 = edition.getCode2();
final Map<String, String> validFilenames = _cardFileNamesBySet.get(editionCode2);
_analyzeListedDir(root, ForgeConstants.CACHE_CARD_PICS_DIR, new _ListedAnalyzer() {
@Override public String map(String filename) {
filename = editionCode2 + "/" + filename;
if (_nameUpdates.containsKey(filename)) {
filename = _nameUpdates.get(filename);
}
if (validFilenames.containsKey(filename)) {
return validFilenames.get(filename);
} else if (StringUtils.endsWithIgnoreCase(filename, ".jpg")
|| StringUtils.endsWithIgnoreCase(filename, ".png")) {
return filename;
}
return null;
}
@Override public OpType getOpType(String filename) {
return validFilenames.containsKey(filename) ? OpType.SET_CARD_PIC : OpType.POSSIBLE_SET_CARD_PIC;
}
});
}
//////////////////////////////////////////////////////////////////////////
// other image dirs
//
Map<String, String> _iconFileNames;
private void _analyzeIconsPicsDir(File root) {
if (null == _iconFileNames) {
_iconFileNames = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
for (Pair<String, String> nameurl : FileUtil.readNameUrlFile(ForgeConstants.IMAGE_LIST_QUEST_OPPONENT_ICONS_FILE)) {
_iconFileNames.put(nameurl.getLeft(), nameurl.getLeft());
}
for (Pair<String, String> nameurl : FileUtil.readNameUrlFile(ForgeConstants.IMAGE_LIST_QUEST_PET_SHOP_ICONS_FILE)) {
_iconFileNames.put(nameurl.getLeft(), nameurl.getLeft());
}
}
_analyzeListedDir(root, ForgeConstants.CACHE_ICON_PICS_DIR, new _ListedAnalyzer() {
@Override public String map(String filename) { return _iconFileNames.containsKey(filename) ? _iconFileNames.get(filename) : null; }
@Override public OpType getOpType(String filename) { return OpType.QUEST_PIC; }
});
}
Map<String, String> _tokenFileNames;
Map<String, String> _questTokenFileNames;
private void _analyzeTokenPicsDir(File root) {
if (null == _tokenFileNames) {
_tokenFileNames = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
_questTokenFileNames = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
for (Pair<String, String> nameurl : FileUtil.readNameUrlFile(ForgeConstants.IMAGE_LIST_TOKENS_FILE)) {
_tokenFileNames.put(nameurl.getLeft(), nameurl.getLeft());
}
for (Pair<String, String> nameurl : FileUtil.readNameUrlFile(ForgeConstants.IMAGE_LIST_QUEST_TOKENS_FILE)) {
_questTokenFileNames.put(nameurl.getLeft(), nameurl.getLeft());
}
}
_analyzeListedDir(root, ForgeConstants.CACHE_TOKEN_PICS_DIR, new _ListedAnalyzer() {
@Override public String map(String filename) {
if (_questTokenFileNames.containsKey(filename)) { return _questTokenFileNames.get(filename); }
if (_tokenFileNames.containsKey(filename)) { return _tokenFileNames.get(filename); }
return null;
}
@Override public OpType getOpType(String filename) {
return _questTokenFileNames.containsKey(filename) ? OpType.QUEST_PIC : OpType.TOKEN_PIC;
}
});
}
private void _analyzeProductPicsDir(File root) {
// we don't care about the files in the root dir -- the new booster files are .png, not the current .jpg ones
_analyzeDir(root, new _Analyzer() {
@Override boolean onDir(File dir) {
String dirName = dir.getName();
if ("booster".equalsIgnoreCase(dirName)) {
_analyzeSimpleListedDir(dir, ForgeConstants.IMAGE_LIST_QUEST_BOOSTERS_FILE, ForgeConstants.CACHE_BOOSTER_PICS_DIR, OpType.QUEST_PIC);
} else if ("fatpacks".equalsIgnoreCase(dirName)) {
_analyzeSimpleListedDir(dir, ForgeConstants.IMAGE_LIST_QUEST_FATPACKS_FILE, ForgeConstants.CACHE_FATPACK_PICS_DIR, OpType.QUEST_PIC);
} else if ("precons".equalsIgnoreCase(dirName)) {
_analyzeSimpleListedDir(dir, ForgeConstants.IMAGE_LIST_QUEST_PRECONS_FILE, ForgeConstants.CACHE_PRECON_PICS_DIR, OpType.QUEST_PIC);
} else if ("tournamentpacks".equalsIgnoreCase(dirName)) {
_analyzeSimpleListedDir(dir, ForgeConstants.IMAGE_LIST_QUEST_TOURNAMENTPACKS_FILE, ForgeConstants.CACHE_TOURNAMENTPACK_PICS_DIR, OpType.QUEST_PIC);
} else {
return false;
}
return true;
}
});
}
//////////////////////////////////////////////////////////////////////////
// preferences
//
private void _analyzePreferencesDir(File root) {
_analyzeDir(root, new _Analyzer() {
@Override void onFile(File file) {
String filename = file.getName();
if ("editor.preferences".equalsIgnoreCase(filename) || "forge.preferences".equalsIgnoreCase(filename)) {
File targetFile = new File(ForgeConstants.USER_PREFS_DIR, filename.toLowerCase(Locale.ENGLISH));
if (!file.equals(targetFile)) {
_cb.addOp(OpType.PREFERENCE_FILE, file, targetFile);
}
}
}
});
}
//////////////////////////////////////////////////////////////////////////
// quest data
//
private void _analyzeQuestDir(File root) {
_analyzeDir(root, new _Analyzer() {
@Override void onFile(File file) {
String filename = file.getName();
if ("all-prices.txt".equalsIgnoreCase(filename)) {
File targetFile = new File(ForgeConstants.DB_DIR, filename.toLowerCase(Locale.ENGLISH));
if (!file.equals(targetFile)) {
_cb.addOp(OpType.DB_FILE, file, targetFile);
}
}
}
@Override boolean onDir(File dir) {
if ("data".equalsIgnoreCase(dir.getName())) {
_analyzeQuestDataDir(dir);
return true;
}
return false;
}
});
}
private void _analyzeQuestDataDir(File root) {
_analyzeDir(root, new _Analyzer() {
@Override void onFile(File file) {
String filename = file.getName();
if (StringUtils.endsWithIgnoreCase(filename, ".dat")) {
File targetFile = new File(ForgeConstants.QUEST_SAVE_DIR, _lcaseExt(filename));
if (!file.equals(targetFile)) {
_cb.addOp(OpType.QUEST_DATA, file, targetFile);
}
}
}
});
}
//////////////////////////////////////////////////////////////////////////
// utility functions
//
private class _Analyzer {
void onFile(File file) { }
// returns whether the directory has been handled
boolean onDir(File dir) { return false; }
}
private void _analyzeDir(File root, _Analyzer analyzer) {
for (File file : root.listFiles()) {
if (_cb.checkCancel()) { return; }
if (file.isFile()) {
++_numFilesAnalyzed;
analyzer.onFile(file);
} else if (file.isDirectory()) {
if (!analyzer.onDir(file)) {
_numFilesAnalyzed += _countFiles(file);
}
}
}
}
private Map<String, Map<String, String>> _fileNameDb = new HashMap<String, Map<String, String>>();
private void _analyzeSimpleListedDir(File root, String listFile, String targetDir, final OpType opType) {
if (!_fileNameDb.containsKey(listFile)) {
Map<String, String> fileNames = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
for (Pair<String, String> nameurl : FileUtil.readNameUrlFile(listFile)) {
// we use a map instead of a set since we need to match case-insensitively but still map to the correct case
fileNames.put(nameurl.getLeft(), nameurl.getLeft());
}
_fileNameDb.put(listFile, fileNames);
}
final Map<String, String> fileDb = _fileNameDb.get(listFile);
_analyzeListedDir(root, targetDir, new _ListedAnalyzer() {
@Override public String map(String filename) { return fileDb.containsKey(filename) ? fileDb.get(filename) : null; }
@Override public OpType getOpType(String filename) { return opType; }
});
}
private abstract class _ListedAnalyzer {
abstract String map(String filename);
abstract OpType getOpType(String filename);
// returns whether the directory has been handled
boolean onDir(File dir) { return false; }
}
private void _analyzeListedDir(File root, final String targetDir, final _ListedAnalyzer listedAnalyzer) {
_analyzeDir(root, new _Analyzer() {
@Override void onFile(File file) {
String filename = listedAnalyzer.map(file.getName());
if (null != filename) {
File targetFile = new File(targetDir, filename);
if (!file.equals(targetFile)) {
_cb.addOp(listedAnalyzer.getOpType(filename), file, targetFile);
}
}
}
@Override boolean onDir(File dir) { return listedAnalyzer.onDir(dir); }
});
}
private int _countFiles(File root) {
int count = 0;
for (File file : root.listFiles()) {
if (_cb.checkCancel()) { return 0; }
if (file.isFile()) {
++count;
} else if (file.isDirectory()) {
count += _countFiles(file);
}
}
return count;
}
private String _lcaseExt(String filename) {
int lastDotIdx = filename.lastIndexOf('.');
if (0 > lastDotIdx) {
return filename;
}
String basename = filename.substring(0, lastDotIdx);
String ext = filename.substring(lastDotIdx).toLowerCase(Locale.ENGLISH);
if (filename.endsWith(ext)) {
return filename;
}
return basename + ext;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package forge.gui;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import forge.FThreads;
import forge.toolbox.FList;
import forge.toolbox.FMouseAdapter;
import forge.toolbox.FOptionPane;
import forge.toolbox.FScrollPane;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.Collection;
import java.util.List;
/**
* A simple class that shows a list of choices in a dialog. Two properties
* influence the behavior of a list chooser: minSelection and maxSelection.
* These two give the allowed number of selected items for the dialog to be
* closed. A negative value for minSelection suggests that the list is revealed
* and the choice doesn't matter.
* <ul>
* <li>If minSelection is 0, there will be a Cancel button.</li>
* <li>If minSelection is -1, 0 or 1, double-clicking a choice will also close the
* dialog.</li>
* <li>If the number of selections is out of bounds, the "OK" button is
* disabled.</li>
* <li>The dialog was "committed" if "OK" was clicked or a choice was double
* clicked.</li>
* <li>The dialog was "canceled" if "Cancel" or "X" was clicked.</li>
* <li>If the dialog was canceled, the selection will be empty.</li>
* <li>
* </ul>
*
* @param <T>
* the generic type
* @author Forge
* @version $Id: ListChooser.java 25183 2014-03-14 23:09:45Z drdev $
*/
public class ListChooser<T> {
// Data and number of choices for the list
private List<T> list;
private int minChoices, maxChoices;
// Flag: was the dialog already shown?
private boolean called;
// initialized before; listeners may be added to it
private FList<T> lstChoices;
private FOptionPane optionPane;
public ListChooser(final String title, final int minChoices, final int maxChoices, final Collection<T> list, final Function<T, String> display) {
FThreads.assertExecutedByEdt(true);
this.minChoices = minChoices;
this.maxChoices = maxChoices;
this.list = list.getClass().isInstance(List.class) ? (List<T>)list : Lists.newArrayList(list);
this.lstChoices = new FList<T>(new ChooserListModel());
String[] options;
if (minChoices == 0) {
options = new String[] {"OK","Cancel"};
}
else {
options = new String[] {"OK"};
}
if (maxChoices == 1 || minChoices == -1) {
this.lstChoices.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
}
if (display != null) {
this.lstChoices.setCellRenderer(new TransformedCellRenderer(display));
}
FScrollPane listScroller = new FScrollPane(this.lstChoices, true);
int minWidth = this.lstChoices.getAutoSizeWidth();
if (this.lstChoices.getModel().getSize() > this.lstChoices.getVisibleRowCount()) {
minWidth += listScroller.getVerticalScrollBar().getPreferredSize().width;
}
listScroller.setMinimumSize(new Dimension(minWidth, listScroller.getMinimumSize().height));
this.optionPane = new FOptionPane(null, title, null, listScroller, options, minChoices < 0 ? 0 : -1);
this.optionPane.setButtonEnabled(0, minChoices <= 0);
if (minChoices != -1) {
this.optionPane.setDefaultFocus(this.lstChoices);
}
if (minChoices > 0) {
this.optionPane.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
}
if (minChoices != -1) {
this.lstChoices.getSelectionModel().addListSelectionListener(new SelListener());
}
this.lstChoices.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
ListChooser.this.commit();
}
}
});
this.lstChoices.addMouseListener(new FMouseAdapter() {
@Override
public void onLeftClick(MouseEvent e) {
if (e.getClickCount() == 2) {
ListChooser.this.commit();
}
}
});
}
/**
* Returns the FList used in the list chooser. this is useful for
* registering listeners before showing the dialog.
*
* @return a {@link javax.swing.JList} object.
*/
public FList<T> getLstChoices() {
return this.lstChoices;
}
/** @return boolean */
public boolean show() {
return show(list.get(0));
}
/**
* Shows the dialog and returns after the dialog was closed.
*
* @param index0 index to select when shown
* @return a boolean.
*/
public boolean show(final T item) {
if (this.called) {
throw new IllegalStateException("Already shown");
}
int result;
do {
SwingUtilities.invokeLater(new Runnable() { //invoke later so selected item not set until dialog open
@Override
public void run() {
if (list.contains(item)) {
lstChoices.setSelectedValue(item, true);
}
else {
lstChoices.setSelectedIndex(0);
}
}
});
this.optionPane.setVisible(true);
result = this.optionPane.getResult();
if (result != 0) {
this.lstChoices.clearSelection();
}
// can't stop closing by ESC, so repeat if cancelled
} while (this.minChoices > 0 && result != 0);
this.optionPane.dispose();
// this assert checks if we really don't return on a cancel if input is mandatory
assert (this.minChoices <= 0) || (result == 0);
this.called = true;
return (result == 0);
}
/**
* Returns if the dialog was closed by pressing "OK" or double clicking an
* option the last time.
*
* @return a boolean.
*/
public boolean isCommitted() {
if (!this.called) {
throw new IllegalStateException("not yet shown");
}
return (this.optionPane.getResult() == 0);
}
/**
* Returns the selected indices as a list of integers.
*
* @return a {@link java.util.List} object.
*/
public int[] getSelectedIndices() {
if (!this.called) {
throw new IllegalStateException("not yet shown");
}
return this.lstChoices.getSelectedIndices();
}
/**
* Returns the selected values as a list of objects. no casts are necessary
* when retrieving the objects.
*
* @return a {@link java.util.List} object.
*/
public List<T> getSelectedValues() {
if (!this.called) {
throw new IllegalStateException("not yet shown");
}
return this.lstChoices.getSelectedValuesList();
}
/**
* Returns the (minimum) selected index, or -1.
*
* @return a int.
*/
public int getSelectedIndex() {
if (!this.called) {
throw new IllegalStateException("not yet shown");
}
return this.lstChoices.getSelectedIndex();
}
/**
* Returns the (first) selected value, or null.
*
* @return a T object.
*/
public T getSelectedValue() {
if (!this.called) {
throw new IllegalStateException("not yet shown");
}
return (T) this.lstChoices.getSelectedValue();
}
/**
* <p>
* commit.
* </p>
*/
private void commit() {
if (this.optionPane.isButtonEnabled(0)) {
optionPane.setResult(0);
}
}
private class ChooserListModel extends AbstractListModel<T> {
private static final long serialVersionUID = 3871965346333840556L;
@Override
public int getSize() {
return ListChooser.this.list.size();
}
@Override
public T getElementAt(final int index) {
return ListChooser.this.list.get(index);
}
}
private class SelListener implements ListSelectionListener {
@Override
public void valueChanged(final ListSelectionEvent e) {
final int num = ListChooser.this.lstChoices.getSelectedIndices().length;
ListChooser.this.optionPane.setButtonEnabled(0, (num >= ListChooser.this.minChoices) && (num <= ListChooser.this.maxChoices));
}
}
private class TransformedCellRenderer implements ListCellRenderer<T> {
public final Function<T, String> transformer;
public final DefaultListCellRenderer defRenderer;
/**
* TODO: Write javadoc for Constructor.
*/
public TransformedCellRenderer(final Function<T, String> t1) {
transformer = t1;
defRenderer = new DefaultListCellRenderer();
}
/* (non-Javadoc)
* @see javax.swing.ListCellRenderer#getListCellRendererComponent(javax.swing.JList, java.lang.Object, int, boolean, boolean)
*/
@Override
public Component getListCellRendererComponent(JList<? extends T> list, T value, int index, boolean isSelected, boolean cellHasFocus) {
// TODO Auto-generated method stub
return defRenderer.getListCellRendererComponent(list, transformer.apply(value), index, isSelected, cellHasFocus);
}
}
}

View File

@@ -0,0 +1,61 @@
package forge.gui;
import forge.view.FView;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public final class MouseUtil {
private static Cursor cursor;
private static int cursorLockCount;
/**
* Lock cursor as it is currently displayed until unlockCursor called
*/
public static void lockCursor() {
cursorLockCount++;
}
public static void unlockCursor() {
if (cursorLockCount == 0) { return; }
if (--cursorLockCount == 0) {
//update displayed cursor after cursor unlocked
FView.SINGLETON_INSTANCE.getLpnDocument().setCursor(cursor);
}
}
/**
* The only reliable way to ensure the mouse cursor is set properly in Forge.
*
* @param mouseCursor one of the predefined {@code Cursor} types.
*/
public static void resetCursor() {
setCursor(Cursor.getDefaultCursor());
}
public static void setCursor(int cursorType) {
setCursor(Cursor.getPredefinedCursor(cursorType));
}
public static void setCursor(Cursor cursor0) {
if (cursor == cursor0) { return; }
cursor = cursor0;
if (cursorLockCount > 0) { return; }
FView.SINGLETON_INSTANCE.getLpnDocument().setCursor(cursor);
}
public static void setComponentCursor(final Component comp, final int cursorType) {
setComponentCursor(comp, Cursor.getPredefinedCursor(cursorType));
}
public static void setComponentCursor(final Component comp, final Cursor cursor0) {
comp.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
setCursor(cursor0);
}
@Override
public void mouseExited(MouseEvent e) {
resetCursor();
}
});
}
}

View File

@@ -0,0 +1,126 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.gui;
import javax.swing.*;
import java.awt.*;
/**
* A {@link JLabel} with support for multi-line text that wraps when the line
* doesn't fit in the available width. Multi-line text support is handled by the
* {@link MultiLineLabelUI}, the default UI delegate of this component. The text
* in the label can be horizontally and vertically aligned, relative to the
* bounds of the component.
*
* @author Samuel Sjoberg, http://samuelsjoberg.com
* @version 1.0.0
*/
public class MultiLineLabel extends JLabel {
/**
* Default serial version UID.
*/
private static final long serialVersionUID = 1L;
/**
* Horizontal text alignment.
*/
private int halign = SwingConstants.LEFT;
/**
* Vertical text alignment.
*/
private int valign = SwingConstants.CENTER;
/**
* Cache to save heap allocations.
*/
private Rectangle bounds;
/**
* Creates a new empty label.
*/
public MultiLineLabel() {
super();
this.setUI(MultiLineLabelUI.getLabelUI());
}
/**
* Creates a new label with <code>text</code> value.
*
* @param text
* the value of the label
*/
public MultiLineLabel(final String text) {
this();
this.setText(text);
}
/**
* {@inheritDoc}
*
* @return a {@link java.awt.Rectangle} object.
*/
@Override
public Rectangle getBounds() {
if (this.bounds == null) {
this.bounds = new Rectangle();
}
return super.getBounds(this.bounds);
}
/**
* Set the vertical text alignment.
*
* @param alignment
* vertical alignment
*/
public void setVerticalTextAlignment(final int alignment) {
this.firePropertyChange("verticalTextAlignment", this.valign, alignment);
this.valign = alignment;
}
/**
* Set the horizontal text alignment.
*
* @param alignment
* horizontal alignment
*/
public void setHorizontalTextAlignment(final int alignment) {
this.firePropertyChange("horizontalTextAlignment", this.halign, alignment);
this.halign = alignment;
}
/**
* Get the vertical text alignment.
*
* @return vertical text alignment
*/
public int getVerticalTextAlignment() {
return this.valign;
}
/**
* Get the horizontal text alignment.
*
* @return horizontal text alignment
*/
public int getHorizontalTextAlignment() {
return this.halign;
}
}

View File

@@ -0,0 +1,655 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.gui;
import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.LabelUI;
import javax.swing.plaf.basic.BasicLabelUI;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.beans.PropertyChangeEvent;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Label UI delegate that supports multiple lines and line wrapping. Hard line
* breaks (<code>\n</code>) are preserved. If the dimensions of the label is too
* small to fit all content, the string will be clipped and "..." appended to
* the end of the visible text (similar to the default behavior of
* <code>JLabel</code>). If used in conjunction with a {@link MultiLineLabel},
* text alignment (horizontal and vertical) is supported. The UI delegate can be
* used on a regular <code>JLabel</code> if text alignment isn't required. The
* default alignment, left and vertically centered, will then be used.
* <p/>
* Example of usage:
* <p/>
*
* <pre>
* JLabel myLabel = new JLabel();
* myLabel.setUI(MultiLineLabelUI.labelUI);
* myLabel.setText(&quot;A long label that will wrap automatically.&quot;);
* </pre>
* <p/>
* <p/>
* The line and wrapping support is implemented without using a
* <code>View</code> to make it easy for subclasses to add custom text effects
* by overriding {@link #paintEnabledText(JLabel, Graphics, String, int, int)}
* and {@link #paintDisabledText(JLabel, Graphics, String, int, int)}. This
* class is designed to be easily extended by subclasses.
*
* @author Samuel Sjoberg, http://samuelsjoberg.com
* @version 1.3.0
*/
public class MultiLineLabelUI extends BasicLabelUI implements ComponentListener {
/**
* Shared instance of the UI delegate.
*/
private static LabelUI labelUI = new MultiLineLabelUI();
/**
* Client property key used to store the calculated wrapped lines on the
* JLabel.
*/
public static final String PROPERTY_KEY = "WrappedText";
// Static references to avoid heap allocations.
/** Constant <code>paintIconR</code>. */
private static Rectangle paintIconR = new Rectangle();
/** Constant <code>paintTextR</code>. */
private static Rectangle paintTextR = new Rectangle();
/** Constant <code>paintViewR</code>. */
private static Rectangle paintViewR = new Rectangle();
/** Constant <code>paintViewInsets</code>. */
private static Insets paintViewInsets = new Insets(0, 0, 0, 0);
/**
* Font metrics of the JLabel being rendered.
*/
private FontMetrics metrics;
/**
* Default size of the lines list.
*/
private static int defaultSize = 4;
/**
* Get the shared UI instance.
*
* @param c
* the c
* @return a ComponentUI
*/
public static ComponentUI createUI(final JComponent c) {
return MultiLineLabelUI.getLabelUI();
}
/** {@inheritDoc} */
@Override
protected void uninstallDefaults(final JLabel c) {
super.uninstallDefaults(c);
this.clearCache(c);
}
/** {@inheritDoc} */
@Override
protected void installListeners(final JLabel c) {
super.installListeners(c);
c.addComponentListener(this);
}
/** {@inheritDoc} */
@Override
protected void uninstallListeners(final JLabel c) {
super.uninstallListeners(c);
c.removeComponentListener(this);
}
/**
* Clear the wrapped line cache.
*
* @param l
* the label containing a cached value
*/
protected void clearCache(final JLabel l) {
l.putClientProperty(MultiLineLabelUI.PROPERTY_KEY, null);
}
/** {@inheritDoc} */
@Override
public void propertyChange(final PropertyChangeEvent e) {
super.propertyChange(e);
final String name = e.getPropertyName();
if (name.equals("text") || "font".equals(name)) {
this.clearCache((JLabel) e.getSource());
}
}
/**
* Calculate the paint rectangles for the icon and text for the passed
* label.
*
* @param l
* a label
* @param fm
* the font metrics to use, or <code>null</code> to get the font
* metrics from the label
* @param width
* label width
* @param height
* label height
*/
protected void updateLayout(final JLabel l, FontMetrics fm, final int width, final int height) {
if (fm == null) {
fm = l.getFontMetrics(l.getFont());
}
this.metrics = fm;
final String text = l.getText();
final Icon icon = l.getIcon();
final Insets insets = l.getInsets(MultiLineLabelUI.paintViewInsets);
MultiLineLabelUI.paintViewR.x = insets.left;
MultiLineLabelUI.paintViewR.y = insets.top;
MultiLineLabelUI.paintViewR.width = width - (insets.left + insets.right);
MultiLineLabelUI.paintViewR.height = height - (insets.top + insets.bottom);
MultiLineLabelUI.paintIconR.x = 0;
MultiLineLabelUI.paintIconR.y = 0;
MultiLineLabelUI.paintIconR.width = 0;
MultiLineLabelUI.paintIconR.height = 0;
MultiLineLabelUI.paintTextR.x = 0;
MultiLineLabelUI.paintTextR.y = 0;
MultiLineLabelUI.paintTextR.width = 0;
MultiLineLabelUI.paintTextR.height = 0;
this.layoutCL(l, fm, text, icon, MultiLineLabelUI.paintViewR, MultiLineLabelUI.paintIconR,
MultiLineLabelUI.paintTextR);
}
/**
* <p>
* prepareGraphics.
* </p>
*
* @param g
* a {@link java.awt.Graphics} object.
*/
protected void prepareGraphics(final Graphics g) {
}
/** {@inheritDoc} */
@Override
public void paint(final Graphics g, final JComponent c) {
// parent's update method fills the background
this.prepareGraphics(g);
final JLabel label = (JLabel) c;
final String text = label.getText();
final Icon icon = (label.isEnabled()) ? label.getIcon() : label.getDisabledIcon();
if ((icon == null) && (text == null)) {
return;
}
final FontMetrics fm = g.getFontMetrics();
this.updateLayout(label, fm, c.getWidth(), c.getHeight());
if (icon != null) {
icon.paintIcon(c, g, MultiLineLabelUI.paintIconR.x, MultiLineLabelUI.paintIconR.y);
}
if (text != null) {
final View v = (View) c.getClientProperty("html");
if (v != null) {
// HTML view disables multi-line painting.
v.paint(g, MultiLineLabelUI.paintTextR);
} else {
// Paint the multi line text
this.paintTextLines(g, label, fm);
}
}
}
/**
* Paint the wrapped text lines.
*
* @param g
* graphics component to paint on
* @param label
* the label being painted
* @param fm
* font metrics for current font
*/
protected void paintTextLines(final Graphics g, final JLabel label, final FontMetrics fm) {
final List<String> lines = this.getTextLines(label);
// Available component height to paint on.
final int height = this.getAvailableHeight(label);
int textHeight = lines.size() * fm.getHeight();
while (textHeight > height) {
// Remove one line until no. of visible lines is found.
textHeight -= fm.getHeight();
}
MultiLineLabelUI.paintTextR.height = Math.min(textHeight, height);
MultiLineLabelUI.paintTextR.y = this.alignmentY(label, fm, MultiLineLabelUI.paintTextR);
final int textX = MultiLineLabelUI.paintTextR.x;
int textY = MultiLineLabelUI.paintTextR.y;
for (final Iterator<String> it = lines.iterator(); it.hasNext()
&& MultiLineLabelUI.paintTextR.contains(textX, textY + MultiLineLabelUI.getAscent(fm)); textY += fm
.getHeight()) {
String text = it.next().trim();
if (it.hasNext()
&& !MultiLineLabelUI.paintTextR.contains(textX,
textY + fm.getHeight() + MultiLineLabelUI.getAscent(fm))) {
// The last visible row, add a clip indication.
text = this.clip(text, fm, MultiLineLabelUI.paintTextR);
}
final int x = this.alignmentX(label, fm, text, MultiLineLabelUI.paintTextR);
if (label.isEnabled()) {
this.paintEnabledText(label, g, text, x, textY);
} else {
this.paintDisabledText(label, g, text, x, textY);
}
}
}
/**
* Returns the available height to paint text on. This is the height of the
* passed component with insets subtracted.
*
* @param l
* a component
* @return the available height
*/
protected int getAvailableHeight(final JLabel l) {
l.getInsets(MultiLineLabelUI.paintViewInsets);
return l.getHeight() - MultiLineLabelUI.paintViewInsets.top - MultiLineLabelUI.paintViewInsets.bottom;
}
/**
* Add a clip indication to the string. It is important that the string
* length does not exceed the length or the original string.
*
* @param text
* the to be painted
* @param fm
* font metrics
* @param bounds
* the text bounds
* @return the clipped string
*/
protected String clip(final String text, final FontMetrics fm, final Rectangle bounds) {
// Fast and lazy way to insert a clip indication is to simply replace
// the last characters in the string with the clip indication.
// A better way would be to use metrics and calculate how many (if any)
// characters that need to be replaced.
if (text.length() < 3) {
return "...";
}
return text.substring(0, text.length() - 3) + "...";
}
/**
* Establish the vertical text alignment. The default alignment is to center
* the text in the label.
*
* @param label
* the label to paint
* @param fm
* font metrics
* @param bounds
* the text bounds rectangle
* @return the vertical text alignment, defaults to CENTER.
*/
protected int alignmentY(final JLabel label, final FontMetrics fm, final Rectangle bounds) {
final int height = this.getAvailableHeight(label);
final int textHeight = bounds.height;
if (label instanceof MultiLineLabel) {
final int align = ((MultiLineLabel) label).getVerticalTextAlignment();
switch (align) {
case SwingConstants.TOP:
return MultiLineLabelUI.getAscent(fm) + MultiLineLabelUI.paintViewInsets.top;
case SwingConstants.BOTTOM:
return (((MultiLineLabelUI.getAscent(fm) + height) - MultiLineLabelUI.paintViewInsets.top) + MultiLineLabelUI.paintViewInsets.bottom)
- textHeight;
default:
}
}
// Center alignment
final int textY = MultiLineLabelUI.paintViewInsets.top + ((height - textHeight) / 2)
+ MultiLineLabelUI.getAscent(fm);
return Math.max(textY, MultiLineLabelUI.getAscent(fm) + MultiLineLabelUI.paintViewInsets.top);
}
/**
* <p>
* getAscent.
* </p>
*
* @param fm
* a {@link java.awt.FontMetrics} object.
* @return a int.
*/
private static int getAscent(final FontMetrics fm) {
return fm.getAscent() + fm.getLeading();
}
/**
* Establish the horizontal text alignment. The default alignment is left
* aligned text.
*
* @param label
* the label to paint
* @param fm
* font metrics
* @param s
* the string to paint
* @param bounds
* the text bounds rectangle
* @return the x-coordinate to use when painting for proper alignment
*/
protected int alignmentX(final JLabel label, final FontMetrics fm, final String s, final Rectangle bounds) {
if (label instanceof MultiLineLabel) {
final int align = ((MultiLineLabel) label).getHorizontalTextAlignment();
switch (align) {
case SwingConstants.RIGHT:
return (bounds.x + MultiLineLabelUI.paintViewR.width) - fm.stringWidth(s);
case SwingConstants.CENTER:
return (bounds.x + (MultiLineLabelUI.paintViewR.width / 2)) - (fm.stringWidth(s) / 2);
default:
return bounds.x;
}
}
return bounds.x;
}
/**
* Check the given string to see if it should be rendered as HTML. Code
* based on implementation found in
* <code>BasicHTML.isHTMLString(String)</code> in future JDKs.
*
* @param s
* the string
* @return <code>true</code> if string is HTML, otherwise <code>false</code>
*/
private static boolean isHTMLString(final String s) {
if (s != null) {
if ((s.length() >= 6) && (s.charAt(0) == '<') && (s.charAt(5) == '>')) {
final String tag = s.substring(1, 5);
return tag.equalsIgnoreCase("html");
}
}
return false;
}
/** {@inheritDoc} */
@Override
public Dimension getPreferredSize(final JComponent c) {
final Dimension d = super.getPreferredSize(c);
final JLabel label = (JLabel) c;
if (MultiLineLabelUI.isHTMLString(label.getText())) {
return d; // HTML overrides everything and we don't need to process
}
// Width calculated by super is OK. The preferred width is the width of
// the unwrapped content as long as it does not exceed the width of the
// parent container.
if (c.getParent() != null) {
// Ensure that preferred width never exceeds the available width
// (including its border insets) of the parent container.
final Insets insets = c.getParent().getInsets();
final Dimension size = c.getParent().getSize();
if (size.width > 0) {
// If width isn't set component shouldn't adjust.
d.width = size.width - insets.left - insets.right;
}
}
this.updateLayout(label, null, d.width, d.height);
// The preferred height is either the preferred height of the text
// lines, or the height of the icon.
d.height = Math.max(d.height, this.getPreferredHeight(label));
return d;
}
/**
* The preferred height of the label is the height of the lines with added
* top and bottom insets.
*
* @param label
* the label
* @return the preferred height of the wrapped lines.
*/
protected int getPreferredHeight(final JLabel label) {
final int numOfLines = this.getTextLines(label).size();
final Insets insets = label.getInsets(MultiLineLabelUI.paintViewInsets);
return (numOfLines * this.metrics.getHeight()) + insets.top + insets.bottom;
}
/**
* Get the lines of text contained in the text label. The prepared lines is
* cached as a client property, accessible via {@link #PROPERTY_KEY}.
*
* @param l
* the label
* @return the text lines of the label.
*/
@SuppressWarnings("unchecked")
protected List<String> getTextLines(final JLabel l) {
List<String> lines = (List<String>) l.getClientProperty(MultiLineLabelUI.PROPERTY_KEY);
if (lines == null) {
lines = this.prepareLines(l);
l.putClientProperty(MultiLineLabelUI.PROPERTY_KEY, lines);
}
return lines;
}
/** {@inheritDoc} */
@Override
public void componentHidden(final ComponentEvent e) {
// Don't care
}
/** {@inheritDoc} */
@Override
public void componentMoved(final ComponentEvent e) {
// Don't care
}
/** {@inheritDoc} */
@Override
public void componentResized(final ComponentEvent e) {
this.clearCache((JLabel) e.getSource());
}
/** {@inheritDoc} */
@Override
public void componentShown(final ComponentEvent e) {
// Don't care
}
/**
* Prepare the text lines for rendering. The lines are wrapped to fit in the
* current available space for text. Explicit line breaks are preserved.
*
* @param l
* the label to render
* @return a list of text lines to render
*/
protected List<String> prepareLines(final JLabel l) {
final List<String> lines = new ArrayList<String>(MultiLineLabelUI.defaultSize);
final String text = l.getText();
if (text == null) {
return null; // Null guard
}
final PlainDocument doc = new PlainDocument();
try {
doc.insertString(0, text, null);
} catch (final BadLocationException e) {
return null;
}
final Element root = doc.getDefaultRootElement();
for (int i = 0, j = root.getElementCount(); i < j; i++) {
this.wrap(lines, root.getElement(i));
}
return lines;
}
/**
* If necessary, wrap the text into multiple lines.
*
* @param lines
* line array in which to store the wrapped lines
* @param elem
* the document element containing the text content
*/
protected void wrap(final List<String> lines, final Element elem) {
final int p1 = elem.getEndOffset();
final Document doc = elem.getDocument();
for (int p0 = elem.getStartOffset(); p0 < p1;) {
final int p = this.calculateBreakPosition(doc, p0, p1);
try {
lines.add(doc.getText(p0, p - p0));
} catch (final BadLocationException e) {
throw new Error("Can't get line text. p0=" + p0 + " p=" + p);
}
p0 = (p == p0) ? p1 : p;
}
}
/**
* Calculate the position on which to break (wrap) the line.
*
* @param doc
* the document
* @param p0
* start position
* @param p1
* end position
* @return the actual end position, will be <code>p1</code> if content does
* not need to wrap, otherwise it will be less than <code>p1</code>.
*/
protected int calculateBreakPosition(final Document doc, final int p0, final int p1) {
final Segment segment = SegmentCache.getSegment();
try {
doc.getText(p0, p1 - p0, segment);
} catch (final BadLocationException e) {
throw new Error("Can't get line text");
}
final int width = MultiLineLabelUI.paintTextR.width;
final int p = p0 + Utilities.getBreakLocation(segment, this.metrics, 0, width, null, p0);
SegmentCache.releaseSegment(segment);
return p;
}
/**
* Gets the label ui.
*
* @return the labelUI
*/
public static LabelUI getLabelUI() {
return MultiLineLabelUI.labelUI;
}
/**
* Sets the label ui.
*
* @param labelUI
* the new label ui
*/
public static void setLabelUI(final LabelUI labelUI) {
MultiLineLabelUI.labelUI = labelUI;
}
/**
* Static singleton {@link Segment} cache.
*
* @author Samuel Sjoberg
* @see javax.swing.text.SegmentCache
*/
protected static final class SegmentCache {
/**
* Reused segments.
*/
private final ArrayList<Segment> segments = new ArrayList<Segment>(2);
/**
* Singleton instance.
*/
private static SegmentCache cache = new SegmentCache();
/**
* Private constructor.
*/
private SegmentCache() {
}
/**
* Returns a <code>Segment</code>. When done, the <code>Segment</code>
* should be recycled by invoking {@link #releaseSegment(Segment)}.
*
* @return a <code>Segment</code>.
*/
public static Segment getSegment() {
final int size = SegmentCache.cache.segments.size();
if (size > 0) {
return SegmentCache.cache.segments.remove(size - 1);
}
return new Segment();
}
/**
* Releases a <code>Segment</code>. A segment should not be used after
* it is released, and a segment should never be released more than
* once.
*
* @param segment
* the segment
*/
public static void releaseSegment(final Segment segment) {
segment.array = null;
segment.count = 0;
SegmentCache.cache.segments.add(segment);
}
}
}

View File

@@ -0,0 +1,158 @@
package forge.gui;
import forge.Singletons;
import forge.assets.FSkinProp;
import forge.screens.match.TargetingOverlay;
import forge.toolbox.FLabel;
import forge.toolbox.FOverlay;
import forge.toolbox.FPanel;
import forge.toolbox.FSkin;
import forge.toolbox.FSkin.SkinnedButton;
import forge.toolbox.FSkin.SkinnedLabel;
import net.miginfocom.swing.MigLayout;
import javax.swing.FocusManager;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* All overlay interaction is handled here.
*
* <br><br><i>(S at beginning of class name denotes a static factory.)</i>
*/
public final class SOverlayUtils {
private static int counter = 0;
/**
* A standardized overlay for a game start condition.
*/
public static void startGameOverlay() {
final JPanel overlay = SOverlayUtils.genericOverlay();
final int w = overlay.getWidth();
final int h = overlay.getHeight();
final int pnlW = 400;
final int pnlH = 300;
// Adds the "loading" panel to generic overlay container
// (which is preset with null layout and close button)
final FPanel pnl = new FPanel();
pnl.setLayout(new MigLayout("insets 0, gap 0, ax center, wrap"));
pnl.setBackground(FSkin.getColor(FSkin.Colors.CLR_ACTIVE));
pnl.setBounds(new Rectangle(((w - pnlW) / 2), ((h - pnlH) / 2), pnlW, pnlH));
pnl.add(new FLabel.Builder().icon(FSkin.getIcon(FSkinProp.ICO_LOGO)).build(),
"h 200px!, align center");
pnl.add(new FLabel.Builder().text("Loading new game...")
.fontSize(22).build(), "h 40px!, align center");
overlay.add(pnl);
}
/**
* A standardized overlay for a loading condition (note: thread issues, as of 1-Mar-12).
* @param msg0 &emsp; {@link java.lang.String}
* @return {@link javax.swing.JPanel}
*/
// NOTE: This animation happens on the EDT; if the EDT is tied up doing something
// else, the animation is effectively frozen. So, this needs some work.
public static JPanel loadingOverlay(final String msg0) {
final JPanel overlay = SOverlayUtils.genericOverlay();
final FPanel pnlLoading = new FPanel();
final int w = overlay.getWidth();
final int h = overlay.getHeight();
final SkinnedLabel lblLoading = new SkinnedLabel("");
lblLoading.setOpaque(true);
lblLoading.setBackground(FSkin.getColor(FSkin.Colors.CLR_TEXT));
lblLoading.setMinimumSize(new Dimension(0, 20));
pnlLoading.setBounds(((w - 170) / 2), ((h - 80) / 2), 170, 80);
pnlLoading.setLayout(new MigLayout("wrap, align center"));
pnlLoading.add(new FLabel.Builder().fontSize(18)
.text(msg0).build(), "h 20px!, w 140px!, gap 0 0 5px 0");
pnlLoading.add(lblLoading, "gap 0 0 0 10px");
overlay.add(pnlLoading);
SOverlayUtils.counter = 0;
final Timer timer = new Timer(300, new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
lblLoading.setMinimumSize(new Dimension(10 * (SOverlayUtils.counter++), 20));
lblLoading.revalidate();
if (SOverlayUtils.counter > 13) { SOverlayUtils.counter = 0; }
}
});
timer.start();
return overlay;
}
/**
* A template overlay with close button, null layout, ready for anything.
* @return {@link javax.swing.JPanel}
*/
public static JPanel genericOverlay() {
final JPanel overlay = FOverlay.SINGLETON_INSTANCE.getPanel();
final int w = overlay.getWidth();
final SkinnedButton btnCloseTopRight = new SkinnedButton("X");
btnCloseTopRight.setBounds(w - 25, 10, 15, 15);
btnCloseTopRight.setForeground(FSkin.getColor(FSkin.Colors.CLR_TEXT));
btnCloseTopRight.setBorder(new FSkin.LineSkinBorder(FSkin.getColor(FSkin.Colors.CLR_TEXT)));
btnCloseTopRight.setOpaque(false);
btnCloseTopRight.setBackground(new Color(0, 0, 0));
btnCloseTopRight.setFocusPainted(false);
btnCloseTopRight.addActionListener(new ActionListener() { @Override
public void actionPerformed(ActionEvent arg0) { SOverlayUtils.hideOverlay(); } });
overlay.removeAll();
overlay.setLayout(null);
overlay.add(btnCloseTopRight);
return overlay;
}
private static boolean _overlayHasFocus;
public static boolean overlayHasFocus() {
return _overlayHasFocus;
}
private static Component prevFocusOwner;
public static void showOverlay() {
Singletons.getView().getNavigationBar().setEnabled(false);
prevFocusOwner = FocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner();
FOverlay.SINGLETON_INSTANCE.getPanel().setVisible(true);
// ensure no background element has focus
FOverlay.SINGLETON_INSTANCE.getPanel().requestFocusInWindow();
_overlayHasFocus = true;
}
/**
* Removes child components and closes overlay.
*/
public static void hideOverlay() {
Singletons.getView().getNavigationBar().setEnabled(true);
FOverlay.SINGLETON_INSTANCE.getPanel().removeAll();
FOverlay.SINGLETON_INSTANCE.getPanel().setVisible(false);
if (null != prevFocusOwner) {
prevFocusOwner.requestFocusInWindow();
prevFocusOwner = null;
}
_overlayHasFocus = false;
}
public static void showTargetingOverlay() {
TargetingOverlay.SINGLETON_INSTANCE.getPanel().setVisible(true);
}
/**
* Removes child components and closes overlay.
*/
public static void hideTargetingOverlay() {
TargetingOverlay.SINGLETON_INSTANCE.getPanel().setVisible(false);
}
}

View File

@@ -0,0 +1,80 @@
package forge.gui;
import javax.swing.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
@SuppressWarnings("serial")
public class UnsortedListModel<T> extends AbstractListModel<T> {
List<T> model;
public UnsortedListModel() {
model = new ArrayList<T>();
}
@Override
public int getSize() {
return model.size();
}
@Override
public T getElementAt(int index) {
return model.get(index);
}
public void add(T element) {
model.add(element);
fireIntervalAdded(this, getSize() - 1, getSize() - 1);
fireContentsChanged(this, 0, getSize() - 1);
}
public void addAll(T[] elements) {
for (T e : elements) {
model.add(e);
}
fireIntervalAdded(this, getSize() - elements.length, getSize() - 1);
fireContentsChanged(this, 0, getSize() - 1);
}
public void addAll(Collection<T> elements) {
if (elements.isEmpty()) {
return;
}
model.addAll(elements);
fireIntervalAdded(this, getSize() - elements.size(), getSize() - 1);
fireContentsChanged(this, 0, getSize() - 1);
}
public void addAll(ListModel<T> otherModel) {
Collection<T> elements = new ArrayList<T>();
int size = otherModel.getSize();
for (int i = 0; size > i; ++i) {
elements.add((T)otherModel.getElementAt(i));
}
addAll(elements);
}
public void clear() {
int prevSize = getSize();
model.clear();
fireIntervalRemoved(this, 0, prevSize - 1);
fireContentsChanged(this, 0, prevSize - 1);
}
public boolean contains(Object element) {
return model.contains(element);
}
public Iterator<T> iterator() {
return model.iterator();
}
public void removeElement(int idx) {
model.remove(idx);
fireIntervalRemoved(this, idx, idx);
fireContentsChanged(this, 0, getSize());
}
}

View File

@@ -0,0 +1,163 @@
package forge.gui;
import javax.swing.*;
import java.awt.*;
/**
* FlowLayout subclass that fully supports wrapping of components.
*/
@SuppressWarnings("serial")
public class WrapLayout extends FlowLayout {
/**
* Constructs a new <code>WrapLayout</code> with a left
* alignment and a default 5-unit horizontal and vertical gap.
*/
public WrapLayout() {
super();
}
/**
* Constructs a new <code>FlowLayout</code> with the specified
* alignment and a default 5-unit horizontal and vertical gap.
* The value of the alignment argument must be one of
* <code>FlowLayout.LEFT</code>, <code>FlowLayout.CENTER</code>,
* or <code>FlowLayout.RIGHT</code>.
* @param align the alignment value
*/
public WrapLayout(int align) {
super(align);
}
/**
* Creates a new flow layout manager with the indicated alignment
* and the indicated horizontal and vertical gaps.
* <p>
* The value of the alignment argument must be one of
* <code>FlowLayout.LEFT</code>, <code>FlowLayout.CENTER</code>,
* or <code>FlowLayout.RIGHT</code>.
* @param align the alignment value
* @param hgap the horizontal gap between components
* @param vgap the vertical gap between components
*/
public WrapLayout(int align, int hgap, int vgap) {
super(align, hgap, vgap);
}
/**
* Returns the preferred dimensions for this layout given the
* <i>visible</i> components in the specified target container.
* @param target the component which needs to be laid out
* @return the preferred dimensions to lay out the
* subcomponents of the specified container
*/
@Override
public Dimension preferredLayoutSize(Container target) {
return layoutSize(target, true);
}
/**
* Returns the minimum dimensions needed to layout the <i>visible</i>
* components contained in the specified target container.
* @param target the component which needs to be laid out
* @return the minimum dimensions to lay out the
* subcomponents of the specified container
*/
@Override
public Dimension minimumLayoutSize(Container target) {
Dimension minimum = layoutSize(target, false);
minimum.width -= (getHgap() + 1);
return minimum;
}
/**
* Returns the minimum or preferred dimension needed to layout the target
* container.
*
* @param target target to get layout size for
* @param preferred should preferred size be calculated
* @return the dimension to layout the target container
*/
private Dimension layoutSize(Container target, boolean preferred) {
synchronized (target.getTreeLock()) {
// Each row must fit with the width allocated to the container.
// When the container width = 0, the preferred width of the container
// has not yet been calculated so we use a width guaranteed to be less
// than we need so that it gets recalculated later when the widget is
// shown.
int hgap = getHgap();
Insets insets = target.getInsets();
int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2);
int targetWidth = Math.max(horizontalInsetsAndGap, target.getSize().width);
int maxWidth = targetWidth - horizontalInsetsAndGap;
// Fit components into the allowed width
Dimension dim = new Dimension(0, 0);
int rowWidth = 0;
int rowHeight = 0;
final int nmembers = target.getComponentCount();
for (int i = 0; i < nmembers; i++) {
Component m = target.getComponent(i);
if (!m.isVisible()) {
continue;
}
Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize();
// can't add the component to current row. Start a new row if
// there's at least one component in this row.
if (0 < rowWidth && rowWidth + d.width > maxWidth) {
addRow(dim, rowWidth, rowHeight);
rowWidth = 0;
rowHeight = 0;
}
// Add a horizontal gap for all components after the first
if (rowWidth != 0) {
rowWidth += hgap;
}
rowWidth += d.width;
rowHeight = Math.max(rowHeight, d.height);
}
// add last row
addRow(dim, rowWidth, rowHeight);
dim.width += horizontalInsetsAndGap;
dim.height += insets.top + insets.bottom + getVgap() * 2;
// When using a scroll pane or the DecoratedLookAndFeel we need to
// make sure the preferred size is less than the size of the
// target container so shrinking the container size works
// correctly. Removing the horizontal gap is an easy way to do this.
Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target);
if (scrollPane != null) {
dim.width -= (hgap + 1);
}
return dim;
}
}
/*
* A new row has been completed. Use the dimensions of this row
* to update the preferred size for the container.
*
* @param dim update the width and height when appropriate
* @param rowWidth the width of the row to add
* @param rowHeight the height of the row to add
*/
private void addRow(Dimension dim, int rowWidth, int rowHeight) {
dim.width = Math.max(dim.width, rowWidth);
if (dim.height > 0) {
dim.height += getVgap();
}
dim.height += rowHeight;
}
}

View File

@@ -0,0 +1,32 @@
package forge.gui.framework;
import forge.UiCommand;
/**
* An intentionally empty ICDoc to fill field slots unused
* by the current layout of a match UI.
*/
public class CEmptyDoc implements ICDoc {
/* (non-Javadoc)
* @see forge.gui.framework.ICDoc#getCommandOnSelect()
*/
@Override
public UiCommand getCommandOnSelect() {
return null;
}
/* (non-Javadoc)
* @see forge.gui.framework.ICDoc#initialize()
*/
@Override
public void initialize() {
}
/* (non-Javadoc)
* @see forge.gui.framework.ICDoc#update()
*/
@Override
public void update() {
}
}

View File

@@ -0,0 +1,478 @@
package forge.gui.framework;
import com.google.common.collect.Lists;
import forge.assets.FSkinProp;
import forge.model.FModel;
import forge.properties.ForgePreferences;
import forge.properties.ForgePreferences.FPref;
import forge.toolbox.FPanel;
import forge.toolbox.FSkin;
import forge.toolbox.FSkin.SkinImage;
import forge.view.FView;
import net.miginfocom.swing.MigLayout;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
/**
* Top-level container in drag layout. A cell holds
* tabs, a drag handle, and a tab overflow selector.
* <br>A cell also has two borders, right and bottom,
* for resizing.
*/
@SuppressWarnings("serial")
public final class DragCell extends JPanel implements ILocalRepaint {
// Layout creation worker vars
private RectangleOfDouble roughSize;
private int smoothX = 0;
private int smoothY = 0;
private int smoothW = 0;
private int smoothH = 0;
// Core layout stuff
private final JPanel pnlHead = new JPanel(new MigLayout("insets 0, gap 0, hidemode 3"));
private final FPanel pnlBody = new FPanel();
private final JPanel pnlBorderRight = new JPanel();
private final JPanel pnlBorderBottom = new JPanel();
private final int tabPaddingPx = 2;
private final int margin = 2 * tabPaddingPx;
// Tab handling layout stuff
private final List<IVDoc<? extends ICDoc>> allDocs = new ArrayList<IVDoc<? extends ICDoc>>();
private final JLabel lblHandle = new DragHandle();
private final JLabel lblOverflow = new JLabel();
private IVDoc<? extends ICDoc> docSelected = null;
/**
*
*/
public DragCell() {
super(new MigLayout("insets 0, gap 0, wrap 2"));
this.setOpaque(false);
pnlHead.setOpaque(false);
pnlHead.setBackground(Color.DARK_GRAY);
lblOverflow.setForeground(Color.white);
lblOverflow.setHorizontalAlignment(SwingConstants.CENTER);
lblOverflow.setHorizontalTextPosition(SwingConstants.CENTER);
lblOverflow.setFont(new Font(Font.DIALOG, Font.PLAIN, 10));
lblOverflow.setOpaque(true);
lblOverflow.setBackground(Color.black);
lblOverflow.setToolTipText("Other tabs");
pnlBorderRight.setOpaque(false);
pnlBorderRight.addMouseListener(SResizingUtil.getResizeXListener());
pnlBorderRight.addMouseMotionListener(SResizingUtil.getDragXListener());
pnlBorderBottom.setOpaque(false);
pnlBorderBottom.addMouseListener(SResizingUtil.getResizeYListener());
pnlBorderBottom.addMouseMotionListener(SResizingUtil.getDragYListener());
lblOverflow.addMouseListener(SOverflowUtil.getOverflowListener());
pnlHead.add(lblHandle, "pushx, growx, h 100%!, gap " + tabPaddingPx + "px " + tabPaddingPx + "px 0 0", -1);
pnlHead.add(lblOverflow, "w 20px!, h 100%!, gap " + tabPaddingPx + "px " + tabPaddingPx + "px 0 0", -1);
pnlBody.setCornerDiameter(0);
}
/**
* Refreshes the cell layout without affecting contents.
* <p>
* Primarily used to toggle visibility of tabs.
*/
public void doCellLayout(boolean showTabs) {
this.removeAll();
int borderT = SLayoutConstants.BORDER_T;
int headH = ((showTabs || allDocs.size() > 1) ? SLayoutConstants.HEAD_H : 0);
this.add(pnlHead,
"w 100% - " + borderT + "px!" + ", " + "h " + headH + "px!");
this.add(pnlBorderRight,
"w " + borderT + "px!" + ", " + "h 100% - " + borderT + "px!, span 1 2");
this.add(pnlBody,
"w 100% - " + borderT + "px!" + ", " + "h 100% - " + (headH + borderT) + "px!");
this.add(pnlBorderBottom,
"w 100% - " + borderT + "px!" + ", " + "h " + borderT + "px!");
if (this.isShowing()) {
this.validate();
}
}
/**
* Determines visibility of tabs on game screen.
*/
private boolean showGameTabs() {
ForgePreferences prefs = FModel.getPreferences();
return !prefs.getPrefBoolean(FPref.UI_HIDE_GAME_TABS);
}
/** @return {@link javax.swing.JPanel} */
public JPanel getHead() {
return DragCell.this.pnlHead;
}
/** @return {@link javax.swing.JPanel} */
public JPanel getBody() {
return DragCell.this.pnlBody;
}
/** @return {@link javax.swing.JPanel} */
public JPanel getBorderRight() {
return DragCell.this.pnlBorderRight;
}
/** @return {@link javax.swing.JPanel} */
public JPanel getBorderBottom() {
return DragCell.this.pnlBorderBottom;
}
/**
* Returns a defensive copy list of all documents in this cell.
* @return {@link java.util.List}<{@link forge.gui.framework.IVDoc}>
*/
public List<IVDoc<? extends ICDoc>> getDocs() {
return Lists.newArrayList(allDocs);
}
@Override
public void repaintSelf() {
final Dimension d = DragCell.this.getSize();
repaint(0, 0, d.width, d.height);
}
/** @return int */
public int getW() {
return this.getWidth();
}
/** @return int */
public int getH() {
return this.getHeight();
}
/**
* Screen location of left edge of cell.
* @return int
*/
public int getAbsX() {
int i = 0;
try { i = (int) this.getLocationOnScreen().getX(); }
catch (final Exception e) { }
return i;
}
/**
* Screen location of right edge of cell.
* @return int
*/
public int getAbsX2() {
int i = 0;
try { i = this.getAbsX() + this.getW(); }
catch (final Exception e) { }
return i;
}
/**
* Screen location of upper edge of cell.
* @return int
*/
public int getAbsY() {
int i = 0;
try { i = (int) this.getLocationOnScreen().getY(); }
catch (final Exception e) { }
return i;
}
/**
* Screen location of lower edge of cell.
* @return int
*/
public int getAbsY2() {
int i = 0;
try { i = this.getAbsY() + this.getH(); }
catch (final Exception e) { }
return i;
}
/**
* Automatically calculates rough bounds of this cell.
* @param rectangleOfDouble
*/
public void updateRoughBounds() {
final double contentW = FView.SINGLETON_INSTANCE.getPnlContent().getWidth();
final double contentH = FView.SINGLETON_INSTANCE.getPnlContent().getHeight();
this.roughSize = new RectangleOfDouble(this.getX() / contentW, this.getY() / contentH,
this.getW() / contentW, this.getH() / contentH);
}
/** Explicitly sets percent bounds of this cell. Will be smoothed
* later to avoid pixel rounding errors.
* @param x0 &emsp; double
* @param y0 &emsp; double
* @param w0 &emsp; double
* @param h0 &emsp; double
*/
public void setRoughBounds(RectangleOfDouble rectangleOfDouble) {
this.roughSize = rectangleOfDouble;
}
/**
* TODO: Write javadoc for this method.
* @return
*/
public RectangleOfDouble getRoughBounds() {
return roughSize;
}
/** Sets bounds in superclass using smoothed values from this class. */
public void setSmoothBounds() {
super.setBounds(smoothX, smoothY, smoothW, smoothH);
}
/** @param x0 &emsp; int */
public void setSmoothX(final int x0) {
this.smoothX = x0;
}
/** @param y0 &emsp; int */
public void setSmoothY(final int y0) {
this.smoothY = y0;
}
/** @param w0 &emsp; int */
public void setSmoothW(final int w0) {
this.smoothW = w0;
}
/** @param h0 &emsp; int */
public void setSmoothH(final int h0) {
this.smoothH = h0;
}
/** Adds a document to the tabs.
* @param doc0 &emsp; {@link forge.gui.framework.IVDoc} */
public void addDoc(final IVDoc<? extends ICDoc> doc0) {
if (doc0 instanceof VEmptyDoc) { return; }
allDocs.add(doc0);
doc0.setParentCell(this);
pnlHead.add(doc0.getTabLabel(), "h 100%!, gap " + tabPaddingPx + "px " + tabPaddingPx + "px 0 0", allDocs.size() - 1);
// Ensure that a tab is selected
setSelected(getSelected());
doCellLayout(showGameTabs());
}
/** Removes a document from the layout and tabs.
* @param doc0 &emsp; {@link forge.gui.framework.IVDoc} */
public void removeDoc(final IVDoc<? extends ICDoc> doc0) {
boolean wasSelected = (docSelected == doc0);
allDocs.remove(doc0);
pnlHead.remove(doc0.getTabLabel());
if (wasSelected) { //after removing selected doc, select most recent doc if possible
setSelected(null);
}
doCellLayout(showGameTabs());
}
/** - Deselects previous selection, if there is one<br>
* - Decrements the priorities of all other tabs<br>
* - Sets selected as priority 1<br>
*
* <br><b>null</b> will reset
* (deselect all tabs, and then select the first in the group).
*
* <br><br>Unless there are no tab docs in this cell, there
* will always be a selection.
*
* @param doc0 &emsp; {@link forge.gui.framework.IVDoc} tab document.
*/
public void setSelected(final IVDoc<? extends ICDoc> doc0) {
if (null != doc0 && docSelected == doc0) {
// already selected
return;
}
docSelected = null;
pnlBody.removeAll();
// Priorities are used to "remember" tab selection history.
for (final IVDoc<? extends ICDoc> doc : allDocs) {
if (doc.equals(doc0)) {
docSelected = doc0;
doc.getTabLabel().priorityOne();
doc.getTabLabel().setSelected(true);
doc.populate();
doc.getLayoutControl().update();
}
else {
doc.getTabLabel().setSelected(false);
doc.getTabLabel().priorityDecrease();
}
}
pnlBody.revalidate();
pnlBody.repaint();
// Reached the end without a selection? Select the first in the group.
if (docSelected == null && allDocs.size() > 0) { setSelected(allDocs.get(0)); }
}
/** Returns currently selected document in this cell.
* @return {@link forge.gui.framework.IVDoc} */
public IVDoc<? extends ICDoc> getSelected() {
return docSelected;
}
/**
* Enable/disable resize on the X axis for this cell.
*
* @param enable0 &emsp; boolean
*/
public void toggleResizeX(final boolean enable0) {
this.removeMouseListener(SResizingUtil.getResizeXListener());
if (enable0) {
this.addMouseListener(SResizingUtil.getResizeXListener());
}
}
/**
* Enable/disable resize on the Y axis for this cell.
*
* @param enable0 &emsp; boolean
*/
public void toggleResizeY(final boolean enable0) {
this.removeMouseListener(SResizingUtil.getResizeYListener());
if (enable0) {
this.addMouseListener(SResizingUtil.getResizeYListener());
}
}
/**
* Refreshes visual display of head bar.
*/
public void refresh() {
final int headW = pnlHead.getWidth();
if (docSelected == null) { return; }
if (headW <= 0) { return; }
if (allDocs.isEmpty()) { return; }
// Order tabs by priority
final List<DragTab> priority = new ArrayList<DragTab>();
final DragTab selectedTab = docSelected.getTabLabel();
DragTab nextTab = selectedTab;
while (nextTab != null) {
priority.add(nextTab);
nextTab = getNextTabInPriority(nextTab.getPriority());
}
// Like Einstein's cosmological constant, the extra "8" here
// makes the whole thing work, but the reason for its existence is unknown.
// Part of it is 4px of padding from lblHandle...
int tempW = lblOverflow.getWidth() + margin + 8;
int docOverflowCounter = 0;
// Hide/show all other tabs.
for (final DragTab tab : priority) {
tempW += tab.getWidth() + margin;
tab.setVisible(false);
tab.setMaximumSize(null);
if (tab.equals(selectedTab) || tempW < headW) { tab.setVisible(true); }
else { docOverflowCounter++; }
}
// Resize selected tab if necessary.
tempW = (docOverflowCounter == 0 ? headW - margin : headW - lblOverflow.getWidth() - margin - 10);
selectedTab.setMaximumSize(new Dimension(tempW, 20));
// Update overflow label
lblOverflow.setText("+" + docOverflowCounter);
if (docOverflowCounter == 0) { lblOverflow.setVisible(false); }
else { lblOverflow.setVisible(true); }
}
private DragTab getNextTabInPriority(final int currentPriority0) {
DragTab neo = null;
DragTab temp;
int lowest = Integer.MAX_VALUE;
for (final IVDoc<? extends ICDoc> d : allDocs) {
temp = d.getTabLabel();
// This line prevents two tabs from having the same priority.
if (neo != null && temp.getPriority() == lowest) {
temp.priorityDecrease();
}
if (d.equals(docSelected)) { continue; }
if (temp.getPriority() > lowest) { continue; }
if (temp.getPriority() <= currentPriority0) { continue; }
// If he's The One, he's made it through the tests.
lowest = temp.getPriority();
neo = temp;
}
return neo;
}
/** Paints dragging handle image the length of the label. */
private class DragHandle extends JLabel {
private final SkinImage img = FSkin.getImage(FSkinProp.IMG_HANDLE);
private boolean hovered = false;
public DragHandle() {
this.addMouseListener(SRearrangingUtil.getRearrangeClickEvent());
this.addMouseMotionListener(SRearrangingUtil.getRearrangeDragEvent());
this.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(final MouseEvent e) {
hovered = true; repaintSelf();
}
@Override
public void mouseExited(final MouseEvent e) {
hovered = false; repaintSelf();
}
});
}
@Override
public void paintComponent(final Graphics g) {
super.paintComponent(g);
if (!hovered) { return; }
final Dimension imgSize = img.getSizeForPaint(g);
final int imgW = imgSize.width;
if (imgW < 1) { return; }
final int imgH = imgSize.height;
for (int x = 0; x < getWidth(); x += imgW) {
FSkin.drawImage(g, img, x, ((getHeight() - imgH) / 2));
}
}
}
}

View File

@@ -0,0 +1,92 @@
package forge.gui.framework;
import forge.toolbox.FSkin;
import forge.toolbox.FSkin.SkinnedLabel;
import javax.swing.border.EmptyBorder;
import java.awt.*;
/**
* The tab label object in drag layout.
* No modification should be necessary to this object.
* Simply call the constructor with a title string argument.
*/
@SuppressWarnings("serial")
public final class DragTab extends SkinnedLabel implements ILocalRepaint {
private boolean selected = false;
private int priority = 10;
/**
* The tab label object in drag layout.
* No modification should be necessary to this object.
* Simply call the constructor with a title string argument.
*
* @param title0 &emsp; {java.lang.String}
*/
public DragTab(final String title0) {
super(title0);
setToolTipText(title0);
setOpaque(false);
setSelected(false);
setBorder(new EmptyBorder(2, 5, 2, 5));
this.setForeground(FSkin.getColor(FSkin.Colors.CLR_TEXT));
this.addMouseListener(SRearrangingUtil.getRearrangeClickEvent());
this.addMouseMotionListener(SRearrangingUtil.getRearrangeDragEvent());
}
/** @param isSelected0 &emsp; boolean */
public void setSelected(final boolean isSelected0) {
selected = isSelected0;
repaintSelf();
}
/** Decreases display priority of this tab in relation to its siblings in an overflow case. */
public void priorityDecrease() {
priority++;
}
/** Sets this tab as first to be displayed if siblings overflow. */
public void priorityOne() {
priority = 1;
}
/**
* Returns display priority of this tab in relation to its siblings in an overflow case.
* @return int
*/
public int getPriority() {
return priority;
}
// There should be no need for this method.
@SuppressWarnings("unused")
private void setPriority() {
// Intentionally empty.
}
@Override
public void repaintSelf() {
final Dimension d = DragTab.this.getSize();
repaint(0, 0, d.width, d.height);
}
@Override
public void paintComponent(final Graphics g) {
if (!selected) {
FSkin.setGraphicsColor(g, FSkin.getColor(FSkin.Colors.CLR_INACTIVE));
g.fillRoundRect(0, 0, getWidth() - 1, getHeight() * 2, 6, 6);
FSkin.setGraphicsColor(g, FSkin.getColor(FSkin.Colors.CLR_BORDERS));
g.drawRoundRect(0, 0, getWidth() - 1, getHeight() * 2, 6, 6);
}
else {
FSkin.setGraphicsColor(g, FSkin.getColor(FSkin.Colors.CLR_ACTIVE));
g.fillRoundRect(0, 0, getWidth() - 1, getHeight() * 2, 6, 6);
FSkin.setGraphicsColor(g, FSkin.getColor(FSkin.Colors.CLR_BORDERS));
g.drawRoundRect(0, 0, getWidth() - 1, getHeight() * 2, 6, 6);
}
super.paintComponent(g);
}
}

View File

@@ -0,0 +1,123 @@
/**
*
*/
package forge.gui.framework;
import forge.screens.deckeditor.views.*;
import forge.screens.home.gauntlet.VSubmenuGauntletBuild;
import forge.screens.home.gauntlet.VSubmenuGauntletContests;
import forge.screens.home.gauntlet.VSubmenuGauntletLoad;
import forge.screens.home.gauntlet.VSubmenuGauntletQuick;
import forge.screens.home.quest.*;
import forge.screens.home.sanctioned.VSubmenuConstructed;
import forge.screens.home.sanctioned.VSubmenuDraft;
import forge.screens.home.sanctioned.VSubmenuSealed;
import forge.screens.home.settings.VSubmenuAvatars;
import forge.screens.home.settings.VSubmenuDownloaders;
import forge.screens.home.settings.VSubmenuPreferences;
import forge.screens.home.settings.VSubmenuReleaseNotes;
import forge.screens.match.views.*;
import forge.screens.workshop.views.VCardDesigner;
import forge.screens.workshop.views.VCardScript;
import forge.screens.workshop.views.VWorkshopCatalog;
/**
* These are the identifiers for tabs found in the drag layout.
* These IDs are used in the save XML and card layouts.
*
* <br><br><i>(E at beginning of class name denotes an enum.)</i>
*/
public enum EDocID { /** */
CARD_PICTURE (VPicture.SINGLETON_INSTANCE), /** */
CARD_DETAIL (VDetail.SINGLETON_INSTANCE), /** */
CARD_ANTES (VAntes.SINGLETON_INSTANCE), /** */
EDITOR_ALLDECKS (VAllDecks.SINGLETON_INSTANCE), /** */
EDITOR_STATISTICS (VStatistics.SINGLETON_INSTANCE), /** */
EDITOR_PROBABILITIES (VProbabilities.SINGLETON_INSTANCE), /** */
EDITOR_CATALOG (VCardCatalog.SINGLETON_INSTANCE), /** */
EDITOR_CURRENTDECK (VCurrentDeck.SINGLETON_INSTANCE), /** */
EDITOR_DECKGEN (VDeckgen.SINGLETON_INSTANCE), /** */
WORKSHOP_CATALOG (VWorkshopCatalog.SINGLETON_INSTANCE), /** */
WORKSHOP_CARDDESIGNER (VCardDesigner.SINGLETON_INSTANCE), /** */
WORKSHOP_CARDSCRIPT (VCardScript.SINGLETON_INSTANCE), /** */
HOME_QUESTCHALLENGES (VSubmenuChallenges.SINGLETON_INSTANCE), /** */
HOME_QUESTDUELS (VSubmenuDuels.SINGLETON_INSTANCE), /** */
HOME_QUESTDATA (VSubmenuQuestData.SINGLETON_INSTANCE), /** */
HOME_QUESTDECKS (VSubmenuQuestDecks.SINGLETON_INSTANCE), /** */
HOME_QUESTPREFS (VSubmenuQuestPrefs.SINGLETON_INSTANCE), /** */
HOME_GAUNTLETBUILD (VSubmenuGauntletBuild.SINGLETON_INSTANCE), /** */
HOME_GAUNTLETLOAD (VSubmenuGauntletLoad.SINGLETON_INSTANCE), /** */
HOME_GAUNTLETQUICK (VSubmenuGauntletQuick.SINGLETON_INSTANCE), /** */
HOME_GAUNTLETCONTESTS (VSubmenuGauntletContests.SINGLETON_INSTANCE), /** */
HOME_PREFERENCES (VSubmenuPreferences.SINGLETON_INSTANCE), /** */
HOME_AVATARS (VSubmenuAvatars.SINGLETON_INSTANCE), /** */
HOME_UTILITIES (VSubmenuDownloaders.SINGLETON_INSTANCE), /** */
HOME_CONSTRUCTED (VSubmenuConstructed.SINGLETON_INSTANCE), /** */
HOME_DRAFT (VSubmenuDraft.SINGLETON_INSTANCE), /** */
HOME_SEALED (VSubmenuSealed.SINGLETON_INSTANCE), /** */
HOME_RELEASE_NOTES (VSubmenuReleaseNotes.SINGLETON_INSTANCE),
REPORT_MESSAGE (VPrompt.SINGLETON_INSTANCE), /** */
REPORT_STACK (VStack.SINGLETON_INSTANCE), /** */
REPORT_COMBAT (VCombat.SINGLETON_INSTANCE), /** */
REPORT_LOG (VLog.SINGLETON_INSTANCE), /** */
REPORT_PLAYERS (VPlayers.SINGLETON_INSTANCE), /** */
DEV_MODE (VDev.SINGLETON_INSTANCE), /** */
BUTTON_DOCK (VDock.SINGLETON_INSTANCE), /** */
// Non-user battlefields (AI or teammate), use setDoc to register.
FIELD_0 (null), /** */
FIELD_1 (null), /** */
FIELD_2 (null), /** */
FIELD_3 (null), /** */
FIELD_4 (null), /** */
FIELD_5 (null), /** */
FIELD_6 (null), /** */
FIELD_7 (null), /** */
// Non-user hands (AI or teammate), use setDoc to register.
HAND_0 (null), /** */
HAND_1 (null), /** */
HAND_2 (null), /** */
HAND_3 (null), /** */
HAND_4 (null), /** */
HAND_5 (null), /** */
HAND_6 (null), /** */
HAND_7 (null), /** */
COMMAND_0 (null), /** */
COMMAND_1 (null), /** */
COMMAND_2 (null), /** */
COMMAND_3 (null), /** */
COMMAND_4 (null), /** */
COMMAND_5 (null), /** */
COMMAND_6 (null), /** */
COMMAND_7 (null); /** */
public final static EDocID[] Commands = new EDocID[] {COMMAND_0, COMMAND_1, COMMAND_2, COMMAND_3, COMMAND_4, COMMAND_5, COMMAND_6, COMMAND_7};
public final static EDocID[] Fields = new EDocID[] {FIELD_0, FIELD_1, FIELD_2, FIELD_3, FIELD_4, FIELD_5, FIELD_6, FIELD_7};
public final static EDocID[] Hands = new EDocID[] {HAND_0, HAND_1, HAND_2, HAND_3, HAND_4, HAND_5, HAND_6, HAND_7};
// End enum declarations, start enum methods.
private IVDoc<? extends ICDoc> vDoc;
/** @param doc0 &emsp; {@link forge.gui.framework.IVDoc} */
EDocID(final IVDoc<? extends ICDoc> doc0) {
this.vDoc = doc0;
}
/** @param doc0 &emsp; {@link forge.gui.framework.IVDoc} */
public void setDoc(final IVDoc<? extends ICDoc> doc0) {
this.vDoc = doc0;
}
/** @return {@link forge.gui.framework.IVDoc} */
public IVDoc<? extends ICDoc> getDoc() {
if (vDoc == null) { throw new NullPointerException("No document found for " + this.name() + "."); }
return vDoc;
}
}

View File

@@ -0,0 +1,217 @@
package forge.gui.framework;
import forge.Singletons;
import forge.assets.FSkinProp;
import forge.properties.FileLocation;
import forge.properties.ForgeConstants;
import forge.screens.bazaar.CBazaarUI;
import forge.screens.bazaar.VBazaarUI;
import forge.screens.deckeditor.CDeckEditorUI;
import forge.screens.deckeditor.VDeckEditorUI;
import forge.screens.home.CHomeUI;
import forge.screens.home.VHomeUI;
import forge.screens.match.CMatchUI;
import forge.screens.match.VMatchUI;
import forge.screens.workshop.CWorkshopUI;
import forge.screens.workshop.VWorkshopUI;
import forge.toolbox.FOptionPane;
import forge.toolbox.FSkin;
import forge.toolbox.FSkin.SkinImage;
import java.io.File;
/**
* Definitions for Forge screens
*
*/
public enum FScreen {
HOME_SCREEN(
VHomeUI.SINGLETON_INSTANCE,
CHomeUI.SINGLETON_INSTANCE,
"Home",
FSkin.getIcon(FSkinProp.ICO_FAVICON),
false,
"Exit Forge",
null),
MATCH_SCREEN(
VMatchUI.SINGLETON_INSTANCE,
CMatchUI.SINGLETON_INSTANCE,
"Game",
FSkin.getIcon(FSkinProp.ICO_ALPHASTRIKE), //TODO: Create icon for match screen
true,
"Concede Game",
ForgeConstants.MATCH_LAYOUT_FILE),
WORKSHOP_SCREEN(
VWorkshopUI.SINGLETON_INSTANCE,
CWorkshopUI.SINGLETON_INSTANCE,
"Workshop",
FSkin.getIcon(FSkinProp.ICO_SETTINGS), //TODO: Create icon for workshop screen
false,
"Back to Home",
ForgeConstants.WORKSHOP_LAYOUT_FILE),
DECK_EDITOR_CONSTRUCTED(
VDeckEditorUI.SINGLETON_INSTANCE,
CDeckEditorUI.SINGLETON_INSTANCE,
"Deck Editor",
FSkin.getImage(FSkinProp.IMG_PACK),
false,
"Back to Home",
ForgeConstants.EDITOR_LAYOUT_FILE),
DECK_EDITOR_ARCHENEMY(
VDeckEditorUI.SINGLETON_INSTANCE,
CDeckEditorUI.SINGLETON_INSTANCE,
"Scheme Deck Editor",
FSkin.getImage(FSkinProp.IMG_PACK),
true,
"Close Editor",
ForgeConstants.EDITOR_LAYOUT_FILE),
DECK_EDITOR_COMMANDER(
VDeckEditorUI.SINGLETON_INSTANCE,
CDeckEditorUI.SINGLETON_INSTANCE,
"Commander Deck Editor",
FSkin.getImage(FSkinProp.IMG_PACK),
true,
"Close Editor",
ForgeConstants.EDITOR_LAYOUT_FILE),
DECK_EDITOR_PLANECHASE(
VDeckEditorUI.SINGLETON_INSTANCE,
CDeckEditorUI.SINGLETON_INSTANCE,
"Planar Deck Editor",
FSkin.getImage(FSkinProp.IMG_PACK),
true,
"Close Editor",
ForgeConstants.EDITOR_LAYOUT_FILE),
DECK_EDITOR_VANGUARD(
VDeckEditorUI.SINGLETON_INSTANCE,
CDeckEditorUI.SINGLETON_INSTANCE,
"Vanguard Deck Editor",
FSkin.getImage(FSkinProp.IMG_PACK),
true,
"Close Editor",
ForgeConstants.EDITOR_LAYOUT_FILE),
DECK_EDITOR_DRAFT(
VDeckEditorUI.SINGLETON_INSTANCE,
CDeckEditorUI.SINGLETON_INSTANCE,
"Draft Deck Editor",
FSkin.getImage(FSkinProp.IMG_PACK),
true,
"Close Editor",
ForgeConstants.EDITOR_LAYOUT_FILE),
DECK_EDITOR_SEALED(
VDeckEditorUI.SINGLETON_INSTANCE,
CDeckEditorUI.SINGLETON_INSTANCE,
"Sealed Deck Editor",
FSkin.getImage(FSkinProp.IMG_PACK),
true,
"Close Editor",
ForgeConstants.EDITOR_LAYOUT_FILE),
DECK_EDITOR_QUEST(
VDeckEditorUI.SINGLETON_INSTANCE,
CDeckEditorUI.SINGLETON_INSTANCE,
"Quest Deck Editor",
FSkin.getImage(FSkinProp.IMG_PACK),
true,
"Close Editor",
ForgeConstants.EDITOR_LAYOUT_FILE),
QUEST_CARD_SHOP(
VDeckEditorUI.SINGLETON_INSTANCE,
CDeckEditorUI.SINGLETON_INSTANCE,
"Spell Shop",
FSkin.getIcon(FSkinProp.ICO_QUEST_BOOK),
true,
"Leave Shop",
ForgeConstants.EDITOR_LAYOUT_FILE),
DRAFTING_PROCESS(
VDeckEditorUI.SINGLETON_INSTANCE,
CDeckEditorUI.SINGLETON_INSTANCE,
"Draft",
FSkin.getImage(FSkinProp.IMG_ZONE_HAND),
true,
"Leave Draft",
ForgeConstants.EDITOR_LAYOUT_FILE),
QUEST_BAZAAR(
VBazaarUI.SINGLETON_INSTANCE,
CBazaarUI.SINGLETON_INSTANCE,
"Bazaar",
FSkin.getIcon(FSkinProp.ICO_QUEST_BOTTLES),
true,
"Leave Bazaar",
null);
private final IVTopLevelUI view;
private final ICDoc controller;
private final String tabCaption;
private final SkinImage tabIcon;
private final boolean allowTabClose;
private final String closeButtonTooltip;
private final FileLocation layoutFile;
private FScreen(IVTopLevelUI view0, ICDoc controller0, String tabCaption0, SkinImage tabIcon0, boolean allowTabClose0, String closeButtonTooltip0, FileLocation layoutFile0) {
this.view = view0;
this.controller = controller0;
this.tabCaption = tabCaption0;
this.tabIcon = tabIcon0;
this.allowTabClose = allowTabClose0;
this.closeButtonTooltip = closeButtonTooltip0;
this.layoutFile = layoutFile0;
}
public IVTopLevelUI getView() {
return view;
}
public ICDoc getController() {
return controller;
}
public String getTabCaption() {
return tabCaption;
}
public SkinImage getTabIcon() {
return tabIcon;
}
public boolean allowTabClose() {
return allowTabClose;
}
public String getCloseButtonTooltip() {
return closeButtonTooltip;
}
public boolean onSwitching(FScreen toScreen) {
return view.onSwitching(this, toScreen);
}
public boolean onClosing() {
return view.onClosing(this);
}
public FileLocation getLayoutFile() {
return layoutFile;
}
public boolean deleteLayoutFile() {
if (layoutFile == null) { return false; }
try {
File file = new File(layoutFile.userPrefLoc);
file.delete();
return true;
}
catch (final Exception e) {
e.printStackTrace();
FOptionPane.showErrorDialog("Failed to delete layout file.");
}
return false;
}
public void open() {
Singletons.getControl().setCurrentScreen(this);
}
public void close() {
Singletons.getView().getNavigationBar().closeTab(this);
}
}

View File

@@ -0,0 +1,36 @@
package forge.gui.framework;
import forge.UiCommand;
/**
* Dictates methods required for any controller
* of an {@link forge.gui.framework.IVDoc}.
*
* <br><br><i>(I at beginning of class name denotes an interface.)</i>
* <br><i>(C at beginning of class name denotes a controller class.)</i>
*/
public interface ICDoc {
/**
* Fires when this controller's view tab is selected.
* Since this method is fired when all tabs are first
* initialized, be wary of NPEs created by referring to
* non-existent components.
*
* @return {@link forge.UiCommand} */
UiCommand getCommandOnSelect();
/**
* This method is called once, after the view singleton has been fully realized
* for the first time. It should execute operations which should only
* be done once, but require non-null view components.
* <br><br>
* This method should only be called once, in FView, after singletons are populated.
*/
void initialize();
/**
* Update whatever content is in the panel.
*/
void update();
}

View File

@@ -0,0 +1,12 @@
/**
*
*/
package forge.gui.framework;
/**
* This interface provides a unifying type to all enums
* used as tab identifiers in XML and card layouts.
*
* <br><br><i>(I at beginning of class name denotes an interface.)</i>
*/
public interface IDocIdList { }

View File

@@ -0,0 +1,15 @@
package forge.gui.framework;
/**
* This interface requires a repaintThis() method, which
* boost performance by locally repaints a component,
* rather than repainting the entire screen.
*
* <br><br><i>(I at beginning of class name denotes an interface.)</i>
*/
public interface ILocalRepaint {
/** Boosts performance on repaints by locally repainting a component,
* rather than repainting the entire screen.
*/
void repaintSelf();
}

View File

@@ -0,0 +1,62 @@
package forge.gui.framework;
/**
* This interface provides a unifying type to any component
* (usually JPanels or JScrollPanes) which could be used as
* a tab. A single one of these components is referred to as
* a "document" throughout the codebase. The tabs and their
* documents are contained in "cells" for resizing and dragging.
*
* <br><br><i>(I at beginning of class name denotes an interface.)</i>
* <br><i>(V at beginning of class name denotes a view class.)</i>
*/
public interface IVDoc<TCDoc extends ICDoc> {
/**
* Returns the ID used to identify this tab in save XML and card layouts.
*
* @return {@link forge.gui.framework.EDocID}
*/
EDocID getDocumentID();
/**
* Returns tab label object used in title bars.
*
* @return {@link forge.gui.framework.DragTab}
*/
DragTab getTabLabel();
/** Retrieves control object associated with this document.
* @return {@link forge.gui.home.ICSubmenu}
*/
TCDoc getLayoutControl();
/** Sets the current parent cell of this view,
* allowing access to its body and head sections.
*
* @param cell0 &emsp; {@link forge.gui.framework.DragCell}
*/
void setParentCell(DragCell cell0);
/**
* Gets parent cell for this view.
*
* @return {@link forge.gui.framework.DragCell}
*/
DragCell getParentCell();
/**
* Targets the drag cell body (use <code>parentCell.getBody()</code>).
* Populates panel components, independent of constructor.
* Expected to provide a completely fresh layout to the body panel.
* <br><br>
* The body panel will be empty when this method is called.
* However, its layout may need to be redefined as required.
* <br><br>
* This method basically lays out components
* that have been previously instantiated (cached) by the constructor.
* Non-cached components can be created dynamically if needed.
*/
void populate();
}

View File

@@ -0,0 +1,39 @@
package forge.gui.framework;
/**
* This interface provides a unifying type for all top-level
* UI components.
*
* <br><br><i>(I at beginning of class name denotes an interface.)</i>
* <br><i>(V at beginning of class name denotes a view class.)</i>
*
*/
public interface IVTopLevelUI {
/** Called during the preload sequence, this method caches
* all of the view singletons and component instances,
* before any operations are performed on them.
* <br><br>
* Although this is sometimes empty, it's important, since in many cases
* non-lazy components must be prepared before each panel is populated.
*/
void instantiate();
/**
* Removes all children and (re)populates top level content,
* independent of constructor. Expected to provide
* a completely fresh layout on the component.
*/
void populate();
/**
* Fires when this view's tab is being switched away from.
*
* @return true to allow switching away from tab, false otherwise */
boolean onSwitching(FScreen fromScreen, FScreen toScreen);
/**
* Fires when this view's tab is closing.
*
* @return true to allow closing tab, false otherwise */
boolean onClosing(FScreen screen);
}

View File

@@ -0,0 +1,11 @@
package forge.gui.framework;
/**
* TODO: Write javadoc for this type.
*
*/
public class InvalidLayoutFileException extends RuntimeException {
private static final long serialVersionUID = 8201703516717298690L;
}

View File

@@ -0,0 +1,90 @@
package forge.gui.framework;
/**
* Dimensions for a cell, stored as % of available space
*
*/
public class RectangleOfDouble {
private final double x;
private final double y;
private final double w;
private final double h;
public RectangleOfDouble(final double x0, final double y0, final double w0, final double h0) {
if (x0 > 1) { throw new IllegalArgumentException("X value greater than 100%!"); }
x = x0;
if (y0 > 1) { throw new IllegalArgumentException("Y value greater than 100%!"); }
y = y0;
if (w0 > 1) { throw new IllegalArgumentException("W value greater than 100%!"); }
w = w0;
if (h0 > 1) { throw new IllegalArgumentException("H value greater than 100%!"); }
h = h0;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
long temp;
temp = Double.doubleToLongBits(h);
result = prime * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(w);
result = prime * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(x);
result = prime * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(y);
result = prime * result + (int) (temp ^ (temp >>> 32));
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RectangleOfDouble other = (RectangleOfDouble) obj;
if (Double.doubleToLongBits(h) != Double.doubleToLongBits(other.h))
return false;
if (Double.doubleToLongBits(w) != Double.doubleToLongBits(other.w))
return false;
if (Double.doubleToLongBits(x) != Double.doubleToLongBits(other.x))
return false;
if (Double.doubleToLongBits(y) != Double.doubleToLongBits(other.y))
return false;
return true;
}
public final double getX() {
return x;
}
public final double getY() {
return y;
}
public final double getW() {
return w;
}
public final double getH() {
return h;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return String.format("Rectangle @(%f, %f) sz=(%f, %f)", x,y, w,h);
}
}

View File

@@ -0,0 +1,166 @@
package forge.gui.framework;
import forge.FThreads;
import forge.view.FFrame;
import javax.swing.*;
import java.awt.*;
import java.util.Timer;
import java.util.TimerTask;
/**
* Experimental static factory for generic operations carried out
* onto specific members of the framework. Doublestrike 11-04-12
*
* <br><br><i>(S at beginning of class name denotes a static factory.)</i>
*/
public class SDisplayUtil {
private static boolean remindIsRunning = false;
private static int counter = 0;
private static int[] newA = null, newR = null, newG = null, newB = null;
private static Timer timer1 = null;
/** Flashes animation on input panel if play is currently waiting on input.
*
* @param tab0 &emsp; {@link java.gui.framework.IVDoc}
*/
public static void remind(final IVDoc<? extends ICDoc> tab0) {
showTab(tab0);
final JPanel pnl = tab0.getParentCell().getBody();
// To adjust, only touch these two values.
final int steps = 5; // Number of delays
final int delay = 80; // Milliseconds between steps
if (remindIsRunning) { return; }
if (pnl == null) { return; }
remindIsRunning = true;
final int oldR = pnl.getBackground().getRed();
final int oldG = pnl.getBackground().getGreen();
final int oldB = pnl.getBackground().getBlue();
final int oldA = pnl.getBackground().getAlpha();
counter = 0;
newR = new int[steps];
newG = new int[steps];
newB = new int[steps];
newA = new int[steps];
for (int i = 0; i < steps; i++) {
newR[i] = ((255 - oldR) / steps * i);
newG[i] = (oldG / steps * i);
newB[i] = (oldB / steps * i);
newA[i] = ((255 - oldA) / steps * i);
}
final TimerTask tt = new TimerTask() {
@Override
public void run() {
counter++;
if (counter != (steps - 1)) {
SwingUtilities.invokeLater(new Runnable() { @Override
public void run() {
int r = newR == null ? oldR : newR[counter];
int a = newA == null ? oldA : newR[counter];
pnl.setBackground(new Color(r, oldG, oldB, a));
}
});
}
else {
SwingUtilities.invokeLater(new Runnable() { @Override
public void run() { pnl.setBackground(new Color(oldR, oldG, oldB, oldA)); } });
remindIsRunning = false;
timer1.cancel();
newR = null;
newG = null;
newB = null;
newA = null;
}
}
};
timer1 = new Timer();
timer1.scheduleAtFixedRate(tt, 0, delay);
}
/** @param tab0 &emsp; {@link java.gui.framework.IVDoc} */
public static void showTab(final IVDoc<? extends ICDoc> tab0) {
Runnable showTabRoutine = new Runnable() {
@Override
public void run() {
// FThreads.assertExecutedByEdt(true); - always true
Component c = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner();
DragCell dc = tab0.getParentCell();
if (dc != null)
dc.setSelected(tab0);
// set focus back to previous owner, if any
if (null != c) {
c.requestFocusInWindow();
}
}
};
FThreads.invokeInEdtLater(showTabRoutine);
}
public static GraphicsDevice getGraphicsDevice(Point point) {
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
for (GraphicsDevice gd : env.getScreenDevices()) {
if (gd.getDefaultConfiguration().getBounds().contains(point)) {
return gd;
}
}
return null;
}
public static GraphicsDevice getGraphicsDevice(Rectangle rect) {
return getGraphicsDevice(new Point(rect.x + (rect.width / 2), rect.y + (rect.height / 2)));
}
public static Rectangle getScreenBoundsForPoint(Point point) {
GraphicsDevice gd = getGraphicsDevice(point);
if (gd == null) {
//return bounds of default monitor if point not on any screen
return GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
}
return gd.getDefaultConfiguration().getBounds();
}
public static Rectangle getScreenMaximizedBounds(Rectangle rect) {
GraphicsDevice gd = getGraphicsDevice(rect);
if (gd == null) {
//return bounds of default monitor if center of rect not on any screen
return GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
}
GraphicsConfiguration config = gd.getDefaultConfiguration();
Rectangle bounds = config.getBounds();
Insets screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(config);
bounds.x += screenInsets.left;
bounds.y += screenInsets.top;
bounds.width -= screenInsets.left + screenInsets.right;
bounds.height -= screenInsets.top + screenInsets.bottom;
return bounds;
}
public static boolean setFullScreenWindow(FFrame frame, boolean fullScreen) {
return setFullScreenWindow(getGraphicsDevice(frame.getNormalBounds()), frame, fullScreen);
}
public static boolean setFullScreenWindow(Window window, boolean fullScreen) {
return setFullScreenWindow(getGraphicsDevice(window.getBounds()), window, fullScreen);
}
private static boolean setFullScreenWindow(GraphicsDevice gd, Window window, boolean fullScreen) {
if (gd != null && gd.isFullScreenSupported()) {
if (fullScreen) {
gd.setFullScreenWindow(window);
}
else if (gd.getFullScreenWindow() == window) {
gd.setFullScreenWindow(null);
}
return true;
}
return false;
}
}

View File

@@ -0,0 +1,14 @@
package forge.gui.framework;
/**
* Holds static constants used throughout layout.
*
* <br><br><i>(S at beginning of class name denotes a static factory.)</i>
*/
public class SLayoutConstants {
/** Height of head area in drag panel. */
public static final int HEAD_H = 20;
/** Thickness of resize border in drag panel. */
public static final int BORDER_T = 5;
}

View File

@@ -0,0 +1,459 @@
package forge.gui.framework;
import forge.Singletons;
import forge.properties.FileLocation;
import forge.properties.ForgeConstants;
import forge.toolbox.FAbsolutePositioner;
import forge.util.CollectionSuppliers;
import forge.util.ThreadUtil;
import forge.util.maps.HashMapOfLists;
import forge.util.maps.MapOfLists;
import forge.view.FFrame;
import forge.view.FView;
import javax.swing.border.EmptyBorder;
import javax.xml.stream.*;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import java.awt.*;
import java.io.*;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Handles layout saving and loading.
*
* <br><br><i>(S at beginning of class name denotes a static factory.)</i>
*/
public final class SLayoutIO {
private static class Property {
public final static String x = "x";
public final static String y = "y";
public final static String w = "w";
public final static String h = "h";
public final static String sel = "sel";
public final static String doc = "doc";
public final static String max = "max";
public final static String fs = "fs";
}
private static final XMLEventFactory EF = XMLEventFactory.newInstance();
private static final XMLEvent NEWLINE = EF.createDTD("\n");
private static final XMLEvent TAB = EF.createDTD("\t");
private final static AtomicBoolean saveWindowRequested = new AtomicBoolean(false);
public static void saveWindowLayout() {
if (saveWindowRequested.getAndSet(true)) { return; }
ThreadUtil.delay(500, new Runnable() {
@Override
public void run() {
finishSaveWindowLayout();
saveWindowRequested.set(false);
}
});
}
private synchronized static void finishSaveWindowLayout() {
final FFrame window = FView.SINGLETON_INSTANCE.getFrame();
if (window.isMinimized()) { return; } //don't update saved layout if minimized
final Rectangle normalBounds = window.getNormalBounds();
final FileLocation file = ForgeConstants.WINDOW_LAYOUT_FILE;
final String fWriteTo = file.userPrefLoc;
final XMLOutputFactory out = XMLOutputFactory.newInstance();
FileOutputStream fos = null;
XMLEventWriter writer = null;
try {
fos = new FileOutputStream(fWriteTo);
writer = out.createXMLEventWriter(fos);
writer.add(EF.createStartDocument());
writer.add(NEWLINE);
writer.add(EF.createStartElement("", "", "layout"));
writer.add(EF.createAttribute(Property.x, String.valueOf(normalBounds.x)));
writer.add(EF.createAttribute(Property.y, String.valueOf(normalBounds.y)));
writer.add(EF.createAttribute(Property.w, String.valueOf(normalBounds.width)));
writer.add(EF.createAttribute(Property.h, String.valueOf(normalBounds.height)));
writer.add(EF.createAttribute(Property.max, window.isMaximized() ? "1" : "0"));
writer.add(EF.createAttribute(Property.fs, window.isFullScreen() ? "1" : "0"));
writer.add(EF.createEndElement("", "", "layout"));
writer.flush();
writer.add(EF.createEndDocument());
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block ignores the exception, but sends it to System.err and probably forge.log.
e.printStackTrace();
} catch (XMLStreamException e) {
// TODO Auto-generated catch block ignores the exception, but sends it to System.err and probably forge.log.
e.printStackTrace();
} finally {
if (writer != null ) {
try { writer.close(); } catch (XMLStreamException e) {}
}
if ( fos != null ) {
try { fos.close(); } catch (IOException e) {}
}
}
}
public static void loadWindowLayout() {
final FFrame window = FView.SINGLETON_INSTANCE.getFrame();
final XMLInputFactory inputFactory = XMLInputFactory.newInstance();
final FileLocation file = ForgeConstants.WINDOW_LAYOUT_FILE;
boolean usedCustomPrefsFile = false;
FileInputStream fis = null;
try {
File userSetting = new File(file.userPrefLoc);
if (userSetting.exists()) {
usedCustomPrefsFile = true;
fis = new FileInputStream(userSetting);
}
else {
fis = new FileInputStream(file.defaultLoc);
}
XMLEventReader reader = null;
try {
XMLEvent event;
StartElement element;
Iterator<?> attributes;
Attribute attribute;
reader = inputFactory.createXMLEventReader(fis);
while (reader != null && reader.hasNext()) {
event = reader.nextEvent();
if (event.isStartElement()) {
element = event.asStartElement();
if (element.getName().getLocalPart().equals("layout")) {
attributes = element.getAttributes();
Dimension minSize = window.getMinimumSize();
int x = 0, y = 0, w = minSize.width, h = minSize.height;
boolean max = false, fs = false;
while (attributes.hasNext()) {
attribute = (Attribute) attributes.next();
switch (attribute.getName().toString()) {
case Property.x: x = Integer.parseInt(attribute.getValue()); break;
case Property.y: y = Integer.parseInt(attribute.getValue()); break;
case Property.w: w = Integer.parseInt(attribute.getValue()); break;
case Property.h: h = Integer.parseInt(attribute.getValue()); break;
case Property.max: max = attribute.getValue().equals("1"); break;
case Property.fs: fs = attribute.getValue().equals("1"); break;
}
}
//ensure the window is accessible
int centerX = x + w / 2;
int centerY = y + h / 2;
Rectangle screenBounds = SDisplayUtil.getScreenBoundsForPoint(new Point(centerX, centerY));
if (centerX < screenBounds.x) {
x = screenBounds.x;
}
else if (centerX > screenBounds.x + screenBounds.width) {
x = screenBounds.x + screenBounds.width - w;
if (x < screenBounds.x) {
x = screenBounds.x;
}
}
if (centerY < screenBounds.y) {
y = screenBounds.y;
}
else if (centerY > screenBounds.y + screenBounds.height) {
y = screenBounds.y + screenBounds.height - h;
if (y < screenBounds.y) {
y = screenBounds.y;
}
}
window.setWindowLayout(x, y, w, h, max, fs);
}
}
}
}
catch (final Exception e) {
try {
if (reader != null) { reader.close(); };
}
catch (final XMLStreamException x) {
e.printStackTrace();
}
e.printStackTrace();
if (usedCustomPrefsFile) {
throw new InvalidLayoutFileException();
}
else {
throw new RuntimeException(e);
}
}
}
catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
finally {
if (fis != null ) {
try {
fis.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
}
private final static AtomicBoolean saveRequested = new AtomicBoolean(false);
/** Publicly-accessible save method, to neatly handle exception handling.
* @param f0 file to save layout to, if null, saves to filePreferred
*
*
*/
public static void saveLayout(final File f0) {
if( saveRequested.getAndSet(true) ) return;
ThreadUtil.delay(100, new Runnable() {
@Override
public void run() {
save(f0);
saveRequested.set(false);
}
});
}
private synchronized static void save(final File f0) {
final String fWriteTo;
FileLocation file = Singletons.getControl().getCurrentScreen().getLayoutFile();
if (f0 == null) {
if (file == null) {
return;
}
fWriteTo = file.userPrefLoc;
}
else {
fWriteTo = f0.getPath();
}
final XMLOutputFactory out = XMLOutputFactory.newInstance();
FileOutputStream fos = null;
XMLEventWriter writer = null;
try {
fos = new FileOutputStream(fWriteTo);
writer = out.createXMLEventWriter(fos);
final List<DragCell> cells = FView.SINGLETON_INSTANCE.getDragCells();
writer.add(EF.createStartDocument());
writer.add(NEWLINE);
writer.add(EF.createStartElement("", "", "layout"));
writer.add(NEWLINE);
for (final DragCell cell : cells) {
cell.updateRoughBounds();
RectangleOfDouble bounds = cell.getRoughBounds();
writer.add(TAB);
writer.add(EF.createStartElement("", "", "cell"));
writer.add(EF.createAttribute(Property.x, String.valueOf(Math.rint(bounds.getX() * 100000) / 100000)));
writer.add(EF.createAttribute(Property.y, String.valueOf(Math.rint(bounds.getY() * 100000) / 100000)));
writer.add(EF.createAttribute(Property.w, String.valueOf(Math.rint(bounds.getW() * 100000) / 100000)));
writer.add(EF.createAttribute(Property.h, String.valueOf(Math.rint(bounds.getH() * 100000) / 100000)));
if (cell.getSelected() != null) {
writer.add(EF.createAttribute(Property.sel, cell.getSelected().getDocumentID().toString()));
}
writer.add(NEWLINE);
for (final IVDoc<? extends ICDoc> vDoc : cell.getDocs()) {
createNode(writer, Property.doc, vDoc.getDocumentID().toString());
}
writer.add(TAB);
writer.add(EF.createEndElement("", "", "cell"));
writer.add(NEWLINE);
}
writer.flush();
writer.add(EF.createEndDocument());
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block ignores the exception, but sends it to System.err and probably forge.log.
e.printStackTrace();
} catch (XMLStreamException e) {
// TODO Auto-generated catch block ignores the exception, but sends it to System.err and probably forge.log.
e.printStackTrace();
} finally {
if ( writer != null )
try { writer.close(); } catch (XMLStreamException e) {}
if ( fos != null )
try { fos.close(); } catch (IOException e) {}
}
}
public static void loadLayout(final File f) {
final FView view = FView.SINGLETON_INSTANCE;
FAbsolutePositioner.SINGLETON_INSTANCE.hideAll();
view.getPnlInsets().removeAll();
view.getPnlInsets().setLayout(new BorderLayout());
view.getPnlInsets().add(view.getPnlContent(), BorderLayout.CENTER);
view.getPnlInsets().setBorder(new EmptyBorder(SLayoutConstants.BORDER_T, SLayoutConstants.BORDER_T, 0, 0));
view.removeAllDragCells();
FileLocation file = Singletons.getControl().getCurrentScreen().getLayoutFile();
if (file != null) {
// Read a model for new layout
MapOfLists<LayoutInfo, EDocID> model = null;
boolean usedCustomPrefsFile = false;
FileInputStream fis = null;
try {
if (f != null && f.exists()) {
fis = new FileInputStream(f);
}
else {
File userSetting = new File(file.userPrefLoc);
if (userSetting.exists()) {
usedCustomPrefsFile = true;
fis = new FileInputStream(userSetting);
}
else {
fis = new FileInputStream(file.defaultLoc);
}
}
final XMLInputFactory inputFactory = XMLInputFactory.newInstance();
XMLEventReader xer = null;
try {
xer = inputFactory.createXMLEventReader(fis);
model = readLayout(xer);
} catch (final Exception e) { // I don't care what happened inside, the layout is wrong
try {
if (xer != null) { xer.close(); }
}
catch (final XMLStreamException x) {
x.printStackTrace();
}
e.printStackTrace();
if (usedCustomPrefsFile) { // the one we can safely delete
throw new InvalidLayoutFileException();
}
else {
throw new RuntimeException(e);
}
}
}
catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
finally {
if (fis != null) {
try {
fis.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
// Apply new layout
for (Entry<LayoutInfo, Collection<EDocID>> kv : model.entrySet()) {
LayoutInfo layoutInfo = kv.getKey();
DragCell cell = new DragCell();
cell.setRoughBounds(layoutInfo.getBounds());
FView.SINGLETON_INSTANCE.addDragCell(cell);
for(EDocID edoc : kv.getValue()) {
try {
//System.out.println(String.format("adding doc %s -> %s", edoc, edoc.getDoc()));
cell.addDoc(edoc.getDoc());
}
catch (IllegalArgumentException e) {
System.err.println("Failed to get doc for " + edoc);
}
}
if (layoutInfo.getSelectedId() != null) {
cell.setSelected(layoutInfo.getSelectedId().getDoc());
}
}
}
// Rough bounds are all in place; resize the window.
SResizingUtil.resizeWindow();
}
private static class LayoutInfo
{
private final RectangleOfDouble bounds;
private final EDocID selectedId;
public LayoutInfo(RectangleOfDouble bounds0, EDocID selectedId0) {
this.bounds = bounds0;
this.selectedId = selectedId0;
}
public RectangleOfDouble getBounds() {
return this.bounds;
}
public EDocID getSelectedId() {
return this.selectedId;
}
}
private static MapOfLists<LayoutInfo, EDocID> readLayout(final XMLEventReader reader) throws XMLStreamException
{
XMLEvent event;
StartElement element;
Iterator<?> attributes;
Attribute attribute;
EDocID selectedId = null;
double x0 = 0, y0 = 0, w0 = 0, h0 = 0;
MapOfLists<LayoutInfo, EDocID> model = new HashMapOfLists<LayoutInfo, EDocID>(CollectionSuppliers.<EDocID>arrayLists());
LayoutInfo currentKey = null;
while (null != reader && reader.hasNext()) {
event = reader.nextEvent();
if (event.isStartElement()) {
element = event.asStartElement();
if (element.getName().getLocalPart().equals("cell")) {
attributes = element.getAttributes();
while (attributes.hasNext()) {
attribute = (Attribute) attributes.next();
String atrName = attribute.getName().toString();
if (atrName.equals(Property.x)) x0 = Double.parseDouble(attribute.getValue());
else if (atrName.equals(Property.y)) y0 = Double.parseDouble(attribute.getValue());
else if (atrName.equals(Property.w)) w0 = Double.parseDouble(attribute.getValue());
else if (atrName.equals(Property.h)) h0 = Double.parseDouble(attribute.getValue());
else if (atrName.equals(Property.sel)) selectedId = EDocID.valueOf(attribute.getValue());
}
currentKey = new LayoutInfo(new RectangleOfDouble(x0, y0, w0, h0), selectedId);
}
else if (element.getName().getLocalPart().equals(Property.doc)) {
event = reader.nextEvent();
model.add(currentKey, EDocID.valueOf(event.asCharacters().getData()));
}
}
}
return model;
}
private static void createNode(final XMLEventWriter writer0, final String propertyName, final String value) throws XMLStreamException {
writer0.add(TAB);
writer0.add(TAB);
writer0.add(EF.createStartElement("", "", propertyName));
writer0.add(EF.createCharacters(value));
writer0.add(EF.createEndElement("", "", propertyName));
writer0.add(NEWLINE);
}
}

View File

@@ -0,0 +1,118 @@
package forge.gui.framework;
import forge.view.FView;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
/**
* Package-private utilities for generic overflow behavior
* in title bar for any cell in layout.
*
* <br><br><i>(S at beginning of class name denotes a static factory.)</i>
*/
public final class SOverflowUtil {
private static final MouseListener MAD_OVERFLOW_SELECT = new MouseAdapter() {
@Override
public void mouseReleased(final MouseEvent e) {
final JLabel src = ((JLabel) e.getSource());
final DragCell pnlParent = ((DragCell) src.getParent().getParent());
final JPanel pnlOverflow = FView.SINGLETON_INSTANCE.getPnlTabOverflow();
final String constraints = "w 150px!, h 20px!, gap 5px 5px 2px 2px";
final int w = 160;
int h = 0;
pnlOverflow.removeAll();
for (final IVDoc<? extends ICDoc> t : pnlParent.getDocs()) {
if (t.getTabLabel().isVisible()) { continue; }
pnlOverflow.add(new OverflowLabel(t, pnlParent), constraints);
h += 24;
}
pnlOverflow.revalidate();
pnlOverflow.setVisible(true);
int x = src.getParent().getParent().getX() + src.getX() + SLayoutConstants.BORDER_T;
final int y = src.getParent().getParent().getY() + src.getY() + SLayoutConstants.BORDER_T + src.getHeight() + 3;
// If overflow will appear offscreen, offset.
if (x + w > FView.SINGLETON_INSTANCE.getPnlContent().getWidth()) {
x += src.getWidth() - w;
}
pnlOverflow.setBounds(x, y, w, h);
}
@Override
public void mouseEntered(final MouseEvent e) {
((JLabel) e.getSource()).setBackground(Color.cyan);
((JLabel) e.getSource()).repaint();
}
@Override
public void mouseExited(final MouseEvent e) {
((JLabel) e.getSource()).setBackground(Color.black);
((JLabel) e.getSource()).repaint();
}
};
/** @return {@link java.awt.event.MouseListener} */
private static final MouseListener MAD_HIDE_OVERFLOW = new MouseAdapter() {
@Override
public void mouseClicked(final MouseEvent e) {
final JPanel pnl = FView.SINGLETON_INSTANCE.getPnlTabOverflow();
if (pnl != null) {
pnl.setVisible(pnl.isVisible() ? false : true);
}
}
};
/** @return {@link java.awt.event.MouseListener} */
public static MouseListener getOverflowListener() {
return MAD_OVERFLOW_SELECT;
}
/** @return {@link java.awt.event.MouseListener} */
public static MouseListener getHideOverflowListener() {
return MAD_HIDE_OVERFLOW;
}
@SuppressWarnings("serial")
private static class OverflowLabel extends JLabel implements ILocalRepaint {
public OverflowLabel(final IVDoc<? extends ICDoc> tab0, final DragCell parent0) {
super(tab0.getTabLabel().getText());
setOpaque(true);
setBackground(Color.LIGHT_GRAY);
this.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(final MouseEvent e) {
setBackground(Color.ORANGE);
repaintSelf();
}
@Override
public void mouseExited(final MouseEvent e) {
setBackground(Color.LIGHT_GRAY);
repaintSelf();
}
@Override
public void mousePressed(final MouseEvent e) {
FView.SINGLETON_INSTANCE.getPnlTabOverflow().setVisible(false);
parent0.setSelected(tab0);
parent0.refresh();
}
});
}
@Override
public void repaintSelf() {
final Dimension d = OverflowLabel.this.getSize();
repaint(0, 0, d.width, d.height);
}
}
}

View File

@@ -0,0 +1,484 @@
package forge.gui.framework;
import forge.assets.FSkinProp;
import forge.gui.MouseUtil;
import forge.toolbox.FSkin;
import forge.toolbox.FSkin.SkinCursor;
import forge.toolbox.FSkin.SkinnedLayeredPane;
import forge.view.FView;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
/**
* Package-private utilities for rearranging drag behavior using
* the draggable panels registered in FView.
*
* <br><br><i>(S at beginning of class name denotes a static factory.)</i>
*/
public final class SRearrangingUtil {
private enum Dropzone {
BODY,
RIGHT,
NONE,
TOP,
BOTTOM,
LEFT
}
private static int evtX;
private static int evtY;
private static int tempX;
private static int tempY;
private static int tempW;
private static int tempH;
private static JPanel pnlPreview = FView.SINGLETON_INSTANCE.getPnlPreview();
private static SkinnedLayeredPane pnlDocument = FView.SINGLETON_INSTANCE.getLpnDocument();
private static DragCell cellTarget = null;
private static DragCell cellSrc = null;
private static DragCell cellNew = null;
private static Dropzone dropzone = Dropzone.NONE;
private static List<IVDoc<? extends ICDoc>> docsToMove = new ArrayList<IVDoc<? extends ICDoc>>();
private static IVDoc<? extends ICDoc> srcSelectedDoc = null;
private static final SkinCursor CUR_L = FSkin.getCursor(FSkinProp.IMG_CUR_L, 16, 16, "CUR_L");
private static final SkinCursor CUR_T = FSkin.getCursor(FSkinProp.IMG_CUR_T, 16, 16, "CUR_T");
private static final SkinCursor CUR_B = FSkin.getCursor(FSkinProp.IMG_CUR_B, 16, 16, "CUR_B");
private static final SkinCursor CUR_R = FSkin.getCursor(FSkinProp.IMG_CUR_R, 16, 16, "CUR_R");
private static final SkinCursor CUR_TAB = FSkin.getCursor(FSkinProp.IMG_CUR_TAB, 16, 16, "CUR_TAB");
private static final MouseListener MAD_REARRANGE = new MouseAdapter() {
@Override
public void mousePressed(final MouseEvent e) {
SRearrangingUtil.startRearrange(e);
}
@Override
public void mouseReleased(final MouseEvent e) {
SRearrangingUtil.endRearrange();
}
};
private static final MouseMotionListener MMA_REARRANGE = new MouseMotionAdapter() {
@Override
public void mouseDragged(final MouseEvent e) {
SRearrangingUtil.rearrange(e);
}
};
/**
* Initiates a rearranging of cells or tabs.<br>
* - Sets up source cell<br>
* - Determines if it's a tab or a cell being dragged<br>
* - Resets preview panel
*/
private static void startRearrange(final MouseEvent e) {
cellSrc = (DragCell) ((Container) e.getSource()).getParent().getParent();
docsToMove.clear();
dropzone = Dropzone.NONE;
// Save selected tab in case this tab will be dragged.
srcSelectedDoc = cellSrc.getSelected();
// If only a single tab, select it, and add it to docsToMove.
if (e.getSource() instanceof DragTab) {
for (final IVDoc<? extends ICDoc> vDoc : cellSrc.getDocs()) {
if (vDoc.getTabLabel() == (DragTab) (e.getSource())) {
cellSrc.setSelected(vDoc);
docsToMove.add(vDoc);
}
}
}
// Otherwise, add all of the documents.
else {
for (final IVDoc<? extends ICDoc> vDoc : cellSrc.getDocs()) {
docsToMove.add(vDoc);
}
}
// Reset and show preview panel
pnlPreview.setVisible(true);
pnlPreview.setBounds(0, 0, 0, 0);
}
/**
* Tracks the mouse during a rearrange drag. Updates preview
* panel as necessary, to show the user where their doc
* or cell will be dropped. dropzone is set to direct resizing
* operations when rearrange is finished [see endRearrange()].
*
* @param e &emsp; {@link java.awt.event.MouseEvent}
*/
private static void rearrange(final MouseEvent e) {
// nestingMargin controls the thickness of the "zones" bordering
// the center body.
final int nestingMargin = 30;
evtX = (int) e.getLocationOnScreen().getX();
evtY = (int) e.getLocationOnScreen().getY();
// Find out over which panel the event occurred.
for (final DragCell t : FView.SINGLETON_INSTANCE.getDragCells()) {
tempX = t.getAbsX();
tempY = t.getAbsY();
tempW = t.getW();
tempH = t.getH();
if (evtX < tempX) { continue; } // Left
if (evtY < tempY) { continue; } // Top
if (evtX > tempX + tempW) { continue; } // Right
if (evtY > tempY + tempH) { continue; } // Bottom
cellTarget = t;
break;
}
if (evtX < (tempX + nestingMargin)
&& (cellTarget.getW() / 2) > SResizingUtil.W_MIN) {
dropzone = Dropzone.LEFT;
pnlDocument.setCursor(CUR_L);
pnlPreview.setBounds(
cellTarget.getX() + SLayoutConstants.BORDER_T,
cellTarget.getY() + SLayoutConstants.BORDER_T,
((tempW - SLayoutConstants.BORDER_T) / 2),
tempH - SLayoutConstants.BORDER_T
);
}
else if (evtX > (tempX + tempW - nestingMargin)
&& (cellTarget.getW() / 2) > SResizingUtil.W_MIN) {
dropzone = Dropzone.RIGHT;
pnlDocument.setCursor(CUR_R);
tempW = cellTarget.getW() / 2;
pnlPreview.setBounds(
cellTarget.getX() + cellTarget.getW() - tempW,
cellTarget.getY() + SLayoutConstants.BORDER_T,
tempW,
tempH - SLayoutConstants.BORDER_T
);
}
else if (evtY < (tempY + nestingMargin + SLayoutConstants.HEAD_H) && evtY > tempY + SLayoutConstants.HEAD_H
&& (cellTarget.getH() / 2) > SResizingUtil.H_MIN) {
dropzone = Dropzone.TOP;
pnlDocument.setCursor(CUR_T);
pnlPreview.setBounds(
cellTarget.getX() + SLayoutConstants.BORDER_T,
cellTarget.getY() + SLayoutConstants.BORDER_T,
tempW - SLayoutConstants.BORDER_T,
(tempH / 2)
);
}
else if (evtY > (tempY + tempH - nestingMargin)
&& (cellTarget.getH() / 2) > SResizingUtil.H_MIN) {
dropzone = Dropzone.BOTTOM;
pnlDocument.setCursor(CUR_B);
tempH = cellTarget.getH() / 2;
pnlPreview.setBounds(
cellTarget.getX() + SLayoutConstants.BORDER_T,
cellTarget.getY() + cellTarget.getH() - tempH,
tempW - SLayoutConstants.BORDER_T,
tempH
);
}
else if (cellTarget.equals(cellSrc)) {
dropzone = Dropzone.NONE;
MouseUtil.resetCursor();
pnlPreview.setBounds(0, 0, 0, 0);
}
else {
dropzone = Dropzone.BODY;
pnlDocument.setCursor(CUR_TAB);
pnlPreview.setBounds(
cellTarget.getX() + SLayoutConstants.BORDER_T,
cellTarget.getY() + SLayoutConstants.BORDER_T,
tempW - SLayoutConstants.BORDER_T,
tempH - SLayoutConstants.BORDER_T
);
}
}
/**
* Finalizes a drop, using dropzone hint and docsToMove to
* transfer docs and remove + resize cells as necessary.
*/
private static void endRearrange() {
// Resize preview panel in preparation for next event.
MouseUtil.resetCursor();
pnlPreview.setVisible(false);
pnlPreview.setBounds(0, 0, 0, 0);
// Source and target are the same?
if (dropzone.equals(Dropzone.NONE) || (cellTarget.equals(cellSrc) && cellSrc.getDocs().size() == 1))
{
if (srcSelectedDoc != cellSrc.getSelected())
{
SLayoutIO.saveLayout(null); //still need to save layout if selection changed
}
srcSelectedDoc = null;
return;
}
// Prep vals for possible resize
tempX = cellTarget.getX();
tempY = cellTarget.getY();
tempW = cellTarget.getW();
tempH = cellTarget.getH();
cellNew = new DragCell();
// Insert a new cell if necessary, change bounds on target as appropriate.
switch (dropzone) {
case LEFT:
cellNew.setBounds(
tempX, tempY,
tempW / 2, tempH);
cellTarget.setBounds(
tempX + cellNew.getW(), tempY,
tempW - cellNew.getW(), tempH);
FView.SINGLETON_INSTANCE.addDragCell(cellNew);
break;
case RIGHT:
cellTarget.setBounds(
tempX, tempY,
tempW / 2, tempH);
cellNew.setBounds(
cellTarget.getX() + cellTarget.getW(), tempY ,
tempW - cellTarget.getW(), tempH);
FView.SINGLETON_INSTANCE.addDragCell(cellNew);
break;
case TOP:
cellNew.setBounds(
tempX, tempY,
tempW, tempH - (tempH / 2));
cellTarget.setBounds(
tempX, tempY + cellNew.getH(),
tempW, tempH - cellNew.getH());
FView.SINGLETON_INSTANCE.addDragCell(cellNew);
break;
case BOTTOM:
cellTarget.setBounds(
tempX, tempY,
tempW, tempH / 2);
cellNew.setBounds(
tempX, cellTarget.getY() + cellTarget.getH(),
tempW, tempH - cellTarget.getH());
FView.SINGLETON_INSTANCE.addDragCell(cellNew);
break;
case BODY:
cellNew = cellTarget;
break;
default:
}
for (final IVDoc<? extends ICDoc> vDoc : docsToMove) {
cellSrc.removeDoc(vDoc);
cellNew.addDoc(vDoc);
cellNew.setSelected(vDoc);
}
// Remove old cell if necessary, or, enforce rough bounds on new cell.
if (cellSrc.getDocs().size() == 0) {
fillGap();
FView.SINGLETON_INSTANCE.removeDragCell(cellSrc);
}
cellNew.updateRoughBounds();
cellTarget.updateRoughBounds();
cellSrc.setSelected(srcSelectedDoc);
srcSelectedDoc = null;
cellSrc.refresh();
cellTarget.refresh();
cellNew.validate();
cellNew.refresh();
updateBorders();
SLayoutIO.saveLayout(null);
}
/** The gap created by displaced panels must be filled.
* from any side which shares corners with the gap. */
private static void fillGap() {
// Variables to help with matching the borders
final List<DragCell> cellsToResize = new ArrayList<DragCell>();
final int srcX = cellSrc.getAbsX();
final int srcX2 = cellSrc.getAbsX2();
final int srcY = cellSrc.getAbsY();
final int srcY2 = cellSrc.getAbsY2();
final int srcW = cellSrc.getW();
final int srcH = cellSrc.getH();
// Border check flags
boolean foundT = false;
boolean foundB = false;
boolean foundR = false;
boolean foundL = false;
// Start algorithm
cellsToResize.clear();
foundT = false;
foundB = false;
// Look for matching panels to left of source, expand them to the right.
for (final DragCell cell : FView.SINGLETON_INSTANCE.getDragCells()) {
if (cell.getAbsX2() != srcX) { continue; }
if (cell.getAbsY() == srcY) {
foundT = true;
cellsToResize.add(cell);
}
if (cell.getAbsY2() == srcY2) {
foundB = true;
if (!cellsToResize.contains(cell)) { cellsToResize.add(cell); }
}
if (cell.getAbsY() > srcY && cell.getAbsY2() < srcY2) { cellsToResize.add(cell); }
}
if (foundT && foundB) {
for (final DragCell cell : cellsToResize) {
cell.setBounds(cell.getX(), cell.getY(), cell.getW() + srcW, cell.getH());
cell.updateRoughBounds();
}
return;
}
cellsToResize.clear();
foundT = false;
foundB = false;
// Look for matching panels to right of source, expand them to the left.
for (final DragCell cell : FView.SINGLETON_INSTANCE.getDragCells()) {
if (cell.getAbsX() != srcX2) { continue; }
if (cell.getAbsY() == srcY) {
foundT = true;
cellsToResize.add(cell);
}
if (cell.getAbsY2() == srcY2) {
foundB = true;
if (!cellsToResize.contains(cell)) { cellsToResize.add(cell); }
}
if (cell.getAbsY() > srcY && cell.getAbsY2() < srcY2) { cellsToResize.add(cell); }
}
if (foundT && foundB) {
for (final DragCell cell : cellsToResize) {
cell.setBounds(cellSrc.getX(), cell.getY(), cell.getW() + srcW, cell.getH());
cell.updateRoughBounds();
}
return;
}
cellsToResize.clear();
foundL = false;
foundR = false;
// Look for matching panels below source, expand them upwards.
for (final DragCell cell : FView.SINGLETON_INSTANCE.getDragCells()) {
if (cell.getAbsY() != srcY2) { continue; }
if (cell.getAbsX() == srcX) {
foundL = true;
cellsToResize.add(cell);
}
if (cell.getAbsX2() == srcX2) {
foundR = true;
if (!cellsToResize.contains(cell)) { cellsToResize.add(cell); }
}
if (cell.getAbsX() > srcX && cell.getAbsX2() < srcX2) { cellsToResize.add(cell); }
}
if (foundL && foundR) {
for (final DragCell cell : cellsToResize) {
cell.setBounds(cell.getX(), cellSrc.getY(), cell.getW(), cell.getH() + srcH);
cell.updateRoughBounds();
}
return;
}
cellsToResize.clear();
foundL = false;
foundR = false;
// Look for matching panels above source, expand them downwards.
for (final DragCell cell : FView.SINGLETON_INSTANCE.getDragCells()) {
if (cell.getAbsY2() != srcY) { continue; }
if (cell.getAbsX() == srcX) {
foundL = true;
cellsToResize.add(cell);
}
if (cell.getAbsX2() == srcX2) {
foundR = true;
if (!cellsToResize.contains(cell)) { cellsToResize.add(cell); }
}
if (cell.getAbsX() > srcX && cell.getAbsX2() < srcX2) { cellsToResize.add(cell); }
}
if (foundL && foundR) {
for (final DragCell cell : cellsToResize) {
cell.setBounds(cell.getX(), cell.getY(), cell.getW(), cell.getH() + srcH);
cell.updateRoughBounds();
}
return;
}
throw new UnsupportedOperationException("Gap was not filled.");
}
/**
* Fills a gap left by a source cell.
* <br><br>
* Cell will not be removed, but its coordinates will be filled
* by its neighbors.
*
* @param sourceCell0 &emsp; {@link forge.gui.framework.DragCell}
*/
public static void fillGap(final DragCell sourceCell0) {
cellSrc = sourceCell0;
fillGap();
}
/** Hides outer borders for components on edges,
* preventing illegal resizing (and misleading cursor). */
public static void updateBorders() {
final List<DragCell> cells = FView.SINGLETON_INSTANCE.getDragCells();
final JPanel pnlContent = FView.SINGLETON_INSTANCE.getPnlContent();
for (final DragCell t : cells) {
if (t.getAbsX2() == (pnlContent.getLocationOnScreen().getX() + pnlContent.getWidth())) {
t.getBorderRight().setVisible(false);
}
else {
t.getBorderRight().setVisible(true);
}
if (t.getAbsY2() == (pnlContent.getLocationOnScreen().getY() + pnlContent.getHeight())) {
t.getBorderBottom().setVisible(false);
}
else {
t.getBorderBottom().setVisible(true);
}
}
cells.clear();
}
/** @return {@link java.awt.event.MouseListener} */
public static MouseListener getRearrangeClickEvent() {
return MAD_REARRANGE;
}
/** @return {@link java.awt.event.MouseMotionListener} */
public static MouseMotionListener getRearrangeDragEvent() {
return MMA_REARRANGE;
}
}

View File

@@ -0,0 +1,417 @@
package forge.gui.framework;
import forge.gui.FNetOverlay;
import forge.gui.MouseUtil;
import forge.toolbox.FAbsolutePositioner;
import forge.toolbox.FOverlay;
import forge.view.FDialog;
import forge.view.FFrame;
import forge.view.FNavigationBar;
import forge.view.FView;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;
/**
* Package-private utilities for resizing drag behavior using
* the draggable panels registered in FView.
*
* <br><br><i>(S at beginning of class name denotes a static factory.)</i>
*/
public final class SResizingUtil {
private static final List<DragCell> LEFT_PANELS = new ArrayList<DragCell>();
private static final List<DragCell> RIGHT_PANELS = new ArrayList<DragCell>();
private static final List<DragCell> TOP_PANELS = new ArrayList<DragCell>();
private static final List<DragCell> BOTTOM_PANELS = new ArrayList<DragCell>();
private static int dX;
private static int evtX;
private static int dY;
private static int evtY;
/** Minimum cell width. */
public static final int W_MIN = 100;
/** Minimum cell height. */
public static final int H_MIN = 75;
private static final MouseListener MAD_RESIZE_X = new MouseAdapter() {
@Override
public void mouseEntered(final MouseEvent e) {
MouseUtil.setCursor(Cursor.E_RESIZE_CURSOR);
}
@Override
public void mouseExited(final MouseEvent e) {
MouseUtil.resetCursor();
}
@Override
public void mousePressed(final MouseEvent e) {
SResizingUtil.startResizeX(e);
}
@Override
public void mouseReleased(final MouseEvent e) {
SResizingUtil.endResize();
}
};
private static final MouseListener MAD_RESIZE_Y = new MouseAdapter() {
@Override
public void mouseEntered(final MouseEvent e) {
MouseUtil.setCursor(Cursor.N_RESIZE_CURSOR);
}
@Override
public void mouseExited(final MouseEvent e) {
MouseUtil.resetCursor();
}
@Override
public void mousePressed(final MouseEvent e) {
SResizingUtil.startResizeY(e);
}
@Override
public void mouseReleased(final MouseEvent e) {
SResizingUtil.endResize();
}
};
private static final MouseMotionListener MMA_DRAG_X = new MouseMotionAdapter() {
@Override
public void mouseDragged(final MouseEvent e) {
SResizingUtil.resizeX(e);
}
};
private static final MouseMotionListener MMA_DRAG_Y = new MouseMotionAdapter() {
@Override
public void mouseDragged(final MouseEvent e) {
SResizingUtil.resizeY(e);
}
};
private static final ComponentListener CAD_RESIZE = new ComponentAdapter() {
@Override
public void componentResized(final ComponentEvent e) {
resizeWindow();
SRearrangingUtil.updateBorders();
}
};
public static void resizeWindow() {
final List<DragCell> cells = FView.SINGLETON_INSTANCE.getDragCells();
final FFrame frame = FView.SINGLETON_INSTANCE.getFrame();
final FNavigationBar navigationBar = FView.SINGLETON_INSTANCE.getNavigationBar();
final JPanel pnlContent = FView.SINGLETON_INSTANCE.getPnlContent();
final JPanel pnlInsets = FView.SINGLETON_INSTANCE.getPnlInsets();
Rectangle mainBounds = frame.getContentPane().getBounds();
FDialog.getBackdropPanel().setBounds(mainBounds);
int navigationBarHeight = navigationBar.getPreferredSize().height;
navigationBar.setSize(mainBounds.width, navigationBarHeight);
navigationBar.validate();
if (!frame.isTitleBarHidden()) { //adjust bounds for titlebar if not hidden
mainBounds.y += navigationBarHeight;
mainBounds.height -= navigationBarHeight;
}
FAbsolutePositioner.SINGLETON_INSTANCE.containerResized(mainBounds);
FOverlay.SINGLETON_INSTANCE.getPanel().setBounds(mainBounds);
FNetOverlay.SINGLETON_INSTANCE.containerResized(mainBounds);
pnlInsets.setBounds(mainBounds);
pnlInsets.validate();
final int w = pnlContent.getWidth();
final int h = pnlContent.getHeight();
double roughVal = 0;
int smoothVal = 0;
Set<Component> existingComponents = new HashSet<Component>();
for (Component c : pnlContent.getComponents()) {
existingComponents.add(c);
}
// This is the core of the pixel-perfect layout. To avoid <20>1 px errors on borders
// from rounding individual panels, the intermediate values (exactly accurate, in %)
// for width and height are rounded based on comparison to other panels in the
// layout. This is to avoid errors such as:
// 10% = 9.8px -> 10px -> x 3 = 30px
// 30% = 29.4px -> 29px (!)
for (final DragCell cellA : cells) {
RectangleOfDouble cellSizeA = cellA.getRoughBounds();
roughVal = cellSizeA.getX() * w + cellSizeA.getW() * w;
smoothVal = (int) Math.round(roughVal);
for (final DragCell cellB : cells) {
RectangleOfDouble cellSizeB = cellB.getRoughBounds();
if ((cellSizeB.getX() * w + cellSizeB.getW() * w) == roughVal) {
cellB.setSmoothW(smoothVal - (int) Math.round(cellSizeB.getX() * w));
}
}
cellA.setSmoothW(smoothVal - (int) Math.round(cellSizeA.getX() * w));
roughVal = cellSizeA.getY() * h + cellSizeA.getH() * h;
smoothVal = (int) Math.round(roughVal);
for (final DragCell cellB : cells) {
RectangleOfDouble cellSizeB = cellB.getRoughBounds();
if (cellSizeB.getY() * h + cellSizeB.getH() * h == roughVal) {
cellB.setSmoothH(smoothVal - (int) Math.round(cellSizeB.getY() * h));
}
}
cellA.setSmoothH(smoothVal - (int) Math.round(cellSizeA.getY() * h));
// X and Y coordinate can be rounded as usual.
cellA.setSmoothX((int) Math.round(cellSizeA.getX() * w));
cellA.setSmoothY((int) Math.round(cellSizeA.getY() * h));
// only add component if not already in container; otherwise the keyboard focus
// jumps around to the most recenly added component
if (!existingComponents.contains(cellA)) {
pnlContent.add(cellA);
}
}
// Lock in final bounds and build heads.
for (final DragCell t : cells) {
t.setSmoothBounds();
t.validate();
t.refresh();
}
cells.clear();
}
/** @param e &emsp; {@link java.awt.event.MouseEvent} */
public static void resizeX(final MouseEvent e) {
dX = (int) e.getLocationOnScreen().getX() - evtX;
evtX = (int) e.getLocationOnScreen().getX();
boolean leftLock = false;
boolean rightLock = false;
for (final DragCell t : LEFT_PANELS) {
if ((t.getW() + dX) < W_MIN) { leftLock = true; break; }
}
for (final DragCell t : RIGHT_PANELS) {
if ((t.getW() - dX) < W_MIN) { rightLock = true; break; }
}
if (dX < 0 && leftLock) { return; }
if (dX > 0 && rightLock) { return; }
for (final DragCell t : LEFT_PANELS) {
t.setBounds(t.getX(), t.getY(), t.getW() + dX, t.getH());
t.refresh();
}
for (final DragCell t : RIGHT_PANELS) {
t.setBounds(t.getX() + dX, t.getY(), t.getW() - dX, t.getH());
t.refresh();
}
}
/** @param e &emsp; {@link java.awt.event.MouseEvent} */
public static void resizeY(final MouseEvent e) {
dY = (int) e.getLocationOnScreen().getY() - evtY;
evtY = (int) e.getLocationOnScreen().getY();
boolean topLock = false;
boolean bottomLock = false;
for (final DragCell t : TOP_PANELS) {
if ((t.getH() + dY) < H_MIN) { topLock = true; break; }
}
for (final DragCell t : BOTTOM_PANELS) {
if ((t.getH() - dY) < H_MIN) { bottomLock = true; break; }
}
if (dY < 0 && topLock) { return; }
if (dY > 0 && bottomLock) { return; }
for (final DragCell t : TOP_PANELS) {
t.setBounds(t.getX(), t.getY(), t.getW(), t.getH() + dY);
t.revalidate();
t.repaintSelf();
}
for (final DragCell t : BOTTOM_PANELS) {
t.setBounds(t.getX(), t.getY() + dY, t.getW(), t.getH() - dY);
t.revalidate();
t.repaintSelf();
}
}
/** @param e &emsp; {@link java.awt.event.MouseEvent} */
public static void startResizeX(final MouseEvent e) {
MouseUtil.lockCursor(); //lock cursor while resizing
evtX = (int) e.getLocationOnScreen().getX();
LEFT_PANELS.clear();
RIGHT_PANELS.clear();
final DragCell src = (DragCell) ((JPanel) e.getSource()).getParent();
final int srcX2 = src.getAbsX2();
int limTop = -1;
int limBottom = Integer.MAX_VALUE;
int tempX = -1;
int tempX2 = -1;
int tempY = -1;
int tempY2 = -1;
// Add all panels who share a left or right edge with the
// same coordinate as the right edge of the source panel.
for (final DragCell t : FView.SINGLETON_INSTANCE.getDragCells()) {
tempX = t.getAbsX();
tempX2 = t.getAbsX2();
if (srcX2 == tempX) { RIGHT_PANELS.add(t); }
else if (srcX2 == tempX2) { LEFT_PANELS.add(t); }
}
// Set limits at panels which are level at intersections.
for (final DragCell pnlL : LEFT_PANELS) {
if (pnlL.equals(src)) { continue; }
tempY = pnlL.getAbsY();
tempY2 = pnlL.getAbsY2();
for (final DragCell pnlR : RIGHT_PANELS) {
// Upper edges match? Set a bottom limit.
if (tempY >= src.getAbsY2() && tempY == pnlR.getAbsY() && tempY < limBottom) {
limBottom = tempY;
}
// Lower edges match? Set an upper limit.
else if (tempY2 <= src.getAbsY() && tempY2 == pnlR.getAbsY2() && tempY2 > limTop) {
limTop = tempY2;
}
}
}
// Remove non-contiguous panels from left side using limits.
final Iterator<DragCell> itrLeft = LEFT_PANELS.iterator();
while (itrLeft.hasNext()) {
final DragCell t = itrLeft.next();
if (t.getAbsY() >= limBottom || t.getAbsY2() <= limTop) {
itrLeft.remove();
}
}
// Remove non-contiguous panels from right side using limits.
final Iterator<DragCell> itrRight = RIGHT_PANELS.iterator();
while (itrRight.hasNext()) {
final DragCell t = itrRight.next();
if (t.getAbsY() >= limBottom || t.getAbsY2() <= limTop) {
itrRight.remove();
}
}
}
/** @param e &emsp; {@link java.awt.event.MouseEvent} */
public static void startResizeY(final MouseEvent e) {
MouseUtil.lockCursor(); //lock cursor while resizing
evtY = (int) e.getLocationOnScreen().getY();
TOP_PANELS.clear();
BOTTOM_PANELS.clear();
final DragCell src = (DragCell) ((JPanel) e.getSource()).getParent();
final int srcY2 = src.getAbsY2();
int limLeft = -1;
int limRight = Integer.MAX_VALUE;
int tempX = -1;
int tempX2 = -1;
int tempY = -1;
int tempY2 = -1;
// Add all panels who share a top or bottom edge with the
// same coordinate as the bottom edge of the source panel.
for (final DragCell t : FView.SINGLETON_INSTANCE.getDragCells()) {
tempY = t.getAbsY();
tempY2 = t.getAbsY2();
if (srcY2 == tempY) { BOTTOM_PANELS.add(t); }
else if (srcY2 == tempY2) { TOP_PANELS.add(t); }
}
// Set limits at panels which are level at intersections.
for (final DragCell pnlT : TOP_PANELS) {
if (pnlT.equals(src)) { continue; }
tempX = pnlT.getAbsX();
tempX2 = pnlT.getAbsX2();
for (final DragCell pnlB : BOTTOM_PANELS) {
// Right edges match? Set a right limit.
if (tempX >= src.getAbsX2() && tempX == pnlB.getAbsX() && tempX < limRight) {
limRight = tempX;
}
// Left edges match? Set an left limit.
else if (tempX2 <= src.getAbsX() && tempX2 == pnlB.getAbsX2() && tempX2 > limLeft) {
limLeft = tempX2;
}
}
}
// Remove non-contiguous panels from left side using limits.
final Iterator<DragCell> itrTop = TOP_PANELS.iterator();
while (itrTop.hasNext()) {
final DragCell t = itrTop.next();
if (t.getAbsX() >= limRight || t.getAbsX2() <= limLeft) {
itrTop.remove();
}
}
// Remove non-contiguous panels from right side using limits.
final Iterator<DragCell> itrBottom = BOTTOM_PANELS.iterator();
while (itrBottom.hasNext()) {
final DragCell t = itrBottom.next();
if (t.getAbsX() >= limRight || t.getAbsX2() <= limLeft) {
itrBottom.remove();
}
}
}
/** */
public static void endResize() {
MouseUtil.unlockCursor();
SLayoutIO.saveLayout(null);
}
/** @return {@link java.awt.event.MouseListener} */
public static MouseListener getResizeXListener() {
return MAD_RESIZE_X;
}
/** @return {@link java.awt.event.MouseListener} */
public static MouseListener getResizeYListener() {
return MAD_RESIZE_Y;
}
/** @return {@link java.awt.event.MouseMotionListener} */
public static MouseMotionListener getDragXListener() {
return MMA_DRAG_X;
}
/** @return {@link java.awt.event.MouseMotionListener} */
public static MouseMotionListener getDragYListener() {
return MMA_DRAG_Y;
}
/** @return {@link java.awt.event.ComponentListener} */
public static ComponentListener getWindowResizeListener() {
return CAD_RESIZE;
}
}

View File

@@ -0,0 +1,70 @@
package forge.gui.framework;
/**
* An intentionally empty IVDoc to fill field slots unused
* by the current layout of a match UI.
*/
public class VEmptyDoc implements IVDoc<CEmptyDoc> {
// Fields used with interface IVDoc
private final CEmptyDoc control;
private final EDocID docID;
/**
* An intentionally empty IVDoc to fill field slots unused
* by the current layout of a match UI.
*
* @param id0 EDocID
*/
public VEmptyDoc(final EDocID id0) {
id0.setDoc(this);
docID = id0;
control = new CEmptyDoc();
}
/* (non-Javadoc)
* @see forge.gui.framework.IVDoc#getDocumentID()
*/
@Override
public EDocID getDocumentID() {
return docID;
}
/* (non-Javadoc)
* @see forge.gui.framework.IVDoc#getTabLabel()
*/
@Override
public DragTab getTabLabel() {
return null;
}
/* (non-Javadoc)
* @see forge.gui.framework.IVDoc#getLayoutControl()
*/
@Override
public CEmptyDoc getLayoutControl() {
return control;
}
/* (non-Javadoc)
* @see forge.gui.framework.IVDoc#setParentCell(forge.gui.framework.DragCell)
*/
@Override
public void setParentCell(DragCell cell0) {
}
/* (non-Javadoc)
* @see forge.gui.framework.IVDoc#getParentCell()
*/
@Override
public DragCell getParentCell() {
return null;
}
/* (non-Javadoc)
* @see forge.gui.framework.IVDoc#populate()
*/
@Override
public void populate() {
}
}

View File

@@ -0,0 +1,3 @@
/** Forge Card Game. */
package forge.gui.framework;

View File

@@ -0,0 +1,3 @@
/** Forge Card Game. */
package forge.gui;

View File

@@ -0,0 +1,141 @@
package forge.itemmanager;
import forge.game.GameFormat;
import forge.gui.GuiUtils;
import forge.item.PaperCard;
import forge.itemmanager.filters.*;
import forge.model.FModel;
import forge.quest.QuestWorld;
import forge.screens.home.quest.DialogChooseSets;
import javax.swing.*;
import java.util.List;
/**
* ItemManager for cards
*
*/
@SuppressWarnings("serial")
public class CardManager extends ItemManager<PaperCard> {
public CardManager(boolean wantUnique0) {
super(PaperCard.class, wantUnique0);
}
@Override
protected void addDefaultFilters() {
addDefaultFilters(this);
}
@Override
protected ItemFilter<PaperCard> createSearchFilter() {
return createSearchFilter(this);
}
@Override
protected void buildAddFilterMenu(JMenu menu) {
buildAddFilterMenu(menu, this);
}
/* Static overrides shared with SpellShopManager*/
public static void addDefaultFilters(final ItemManager<? super PaperCard> itemManager) {
itemManager.addFilter(new CardColorFilter(itemManager));
itemManager.addFilter(new CardTypeFilter(itemManager));
itemManager.addFilter(new CardCMCFilter(itemManager));
}
public static ItemFilter<PaperCard> createSearchFilter(final ItemManager<? super PaperCard> itemManager) {
return new CardSearchFilter(itemManager);
}
public static void buildAddFilterMenu(JMenu menu, final ItemManager<? super PaperCard> itemManager) {
GuiUtils.addSeparator(menu); //separate from current search item
JMenu fmt = GuiUtils.createMenu("Format");
for (final GameFormat f : FModel.getFormats()) {
GuiUtils.addMenuItem(fmt, f.getName(), null, new Runnable() {
@Override
public void run() {
itemManager.addFilter(new CardFormatFilter(itemManager, f));
}
}, CardFormatFilter.canAddFormat(f, itemManager.getFilter(CardFormatFilter.class)));
}
menu.add(fmt);
GuiUtils.addMenuItem(menu, "Sets...", null, new Runnable() {
@Override
public void run() {
CardSetFilter existingFilter = itemManager.getFilter(CardSetFilter.class);
if (existingFilter != null) {
existingFilter.edit();
}
else {
final DialogChooseSets dialog = new DialogChooseSets(null, null, true);
dialog.setOkCallback(new Runnable() {
@Override
public void run() {
List<String> sets = dialog.getSelectedSets();
if (!sets.isEmpty()) {
itemManager.addFilter(new CardSetFilter(itemManager, sets, dialog.getWantReprints()));
}
}
});
}
}
});
JMenu world = GuiUtils.createMenu("Quest world");
for (final QuestWorld w : FModel.getWorlds()) {
GuiUtils.addMenuItem(world, w.getName(), null, new Runnable() {
@Override
public void run() {
itemManager.addFilter(new CardQuestWorldFilter(itemManager, w));
}
}, CardQuestWorldFilter.canAddQuestWorld(w, itemManager.getFilter(CardQuestWorldFilter.class)));
}
menu.add(world);
GuiUtils.addSeparator(menu);
GuiUtils.addMenuItem(menu, "Colors", null, new Runnable() {
@Override
public void run() {
itemManager.addFilter(new CardColorFilter(itemManager));
}
}, itemManager.getFilter(CardColorFilter.class) == null);
GuiUtils.addMenuItem(menu, "Types", null, new Runnable() {
@Override
public void run() {
itemManager.addFilter(new CardTypeFilter(itemManager));
}
}, itemManager.getFilter(CardTypeFilter.class) == null);
GuiUtils.addMenuItem(menu, "Converted mana costs", null, new Runnable() {
@Override
public void run() {
itemManager.addFilter(new CardCMCFilter(itemManager));
}
}, itemManager.getFilter(CardCMCFilter.class) == null);
GuiUtils.addSeparator(menu);
GuiUtils.addMenuItem(menu, "CMC range", null, new Runnable() {
@Override
public void run() {
itemManager.addFilter(new CardCMCRangeFilter(itemManager));
}
}, itemManager.getFilter(CardCMCRangeFilter.class) == null);
GuiUtils.addMenuItem(menu, "Power range", null, new Runnable() {
@Override
public void run() {
itemManager.addFilter(new CardPowerFilter(itemManager));
}
}, itemManager.getFilter(CardPowerFilter.class) == null);
GuiUtils.addMenuItem(menu, "Toughness range", null, new Runnable() {
@Override
public void run() {
itemManager.addFilter(new CardToughnessFilter(itemManager));
}
}, itemManager.getFilter(CardToughnessFilter.class) == null);
}
}

View File

@@ -0,0 +1,365 @@
package forge.itemmanager;
import forge.UiCommand;
import forge.Singletons;
import forge.assets.FSkinProp;
import forge.deck.DeckBase;
import forge.deck.io.DeckPreferences;
import forge.game.GameFormat;
import forge.game.GameType;
import forge.gui.GuiUtils;
import forge.deck.DeckProxy;
import forge.gui.framework.FScreen;
import forge.item.InventoryItem;
import forge.itemmanager.ColumnDef;
import forge.itemmanager.ItemColumn;
import forge.itemmanager.ItemManagerConfig;
import forge.itemmanager.filters.*;
import forge.itemmanager.views.*;
import forge.model.FModel;
import forge.quest.QuestWorld;
import forge.screens.deckeditor.CDeckEditorUI;
import forge.screens.deckeditor.SEditorIO;
import forge.screens.deckeditor.controllers.ACEditorBase;
import forge.screens.deckeditor.controllers.CEditorLimited;
import forge.screens.deckeditor.controllers.CEditorQuest;
import forge.screens.home.quest.DialogChooseSets;
import forge.toolbox.FOptionPane;
import forge.toolbox.FSkin;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* ItemManager for decks
*
*/
@SuppressWarnings("serial")
public final class DeckManager extends ItemManager<DeckProxy> {
private static final FSkin.SkinIcon icoDelete = FSkin.getIcon(FSkinProp.ICO_DELETE);
private static final FSkin.SkinIcon icoDeleteOver = FSkin.getIcon(FSkinProp.ICO_DELETE_OVER);
private static final FSkin.SkinIcon icoEdit = FSkin.getIcon(FSkinProp.ICO_EDIT);
private static final FSkin.SkinIcon icoEditOver = FSkin.getIcon(FSkinProp.ICO_EDIT_OVER);
private final GameType gametype;
private UiCommand cmdDelete, cmdSelect;
/**
* Creates deck list for selected decks for quick deleting, editing, and
* basic info. "selectable" and "editable" assumed true.
*
* @param gt
*/
public DeckManager(final GameType gt) {
super(DeckProxy.class, true);
this.gametype = gt;
this.addSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
if (cmdSelect != null) {
cmdSelect.run();
}
}
});
this.setItemActivateCommand(new UiCommand() {
@Override
public void run() {
editDeck(getSelectedItem());
}
});
}
@Override
public void setup(ItemManagerConfig config0) {
boolean wasStringOnly = (this.getConfig() == ItemManagerConfig.STRING_ONLY);
boolean isStringOnly = (config0 == ItemManagerConfig.STRING_ONLY);
Map<ColumnDef, ItemTableColumn> colOverrides = null;
if (config0.getCols().containsKey(ColumnDef.DECK_ACTIONS)) {
ItemTableColumn column = new ItemTableColumn(new ItemColumn(config0.getCols().get(ColumnDef.DECK_ACTIONS)));
column.setCellRenderer(new DeckActionsRenderer());
colOverrides = new HashMap<ColumnDef, ItemTableColumn>();
colOverrides.put(ColumnDef.DECK_ACTIONS, column);
}
super.setup(config0, colOverrides);
if (isStringOnly != wasStringOnly) {
this.restoreDefaultFilters();
}
}
/**
* Sets the delete command.
*
* @param c0 &emsp; {@link forge.UiCommand} command executed on delete.
*/
public void setDeleteCommand(final UiCommand c0) {
this.cmdDelete = c0;
}
/**
* Sets the select command.
*
* @param c0 &emsp; {@link forge.UiCommand} command executed on row select.
*/
public void setSelectCommand(final UiCommand c0) {
this.cmdSelect = c0;
}
@Override
protected void addDefaultFilters() {
if (this.getConfig() == ItemManagerConfig.STRING_ONLY) { return; }
addFilter(new DeckColorFilter(this));
}
@Override
protected ItemFilter<DeckProxy> createSearchFilter() {
return new DeckSearchFilter(this);
}
@Override
protected void buildAddFilterMenu(JMenu menu) {
GuiUtils.addSeparator(menu); //separate from current search item
Set<String> folders = new HashSet<String>();
for (final Entry<DeckProxy, Integer> deckEntry : getPool()) {
String path = deckEntry.getKey().getPath();
if (StringUtils.isNotEmpty(path)) { //don't include root folder as option
folders.add(path);
}
}
JMenu folder = GuiUtils.createMenu("Folder");
if (folders.size() > 0) {
for (final String f : folders) {
GuiUtils.addMenuItem(folder, f, null, new Runnable() {
@Override
public void run() {
addFilter(new DeckFolderFilter(DeckManager.this, f));
}
}, true);
}
}
else {
folder.setEnabled(false);
}
menu.add(folder);
JMenu fmt = GuiUtils.createMenu("Format");
for (final GameFormat f : FModel.getFormats()) {
GuiUtils.addMenuItem(fmt, f.getName(), null, new Runnable() {
@Override
public void run() {
addFilter(new DeckFormatFilter(DeckManager.this, f));
}
}, DeckFormatFilter.canAddFormat(f, getFilter(DeckFormatFilter.class)));
}
menu.add(fmt);
GuiUtils.addMenuItem(menu, "Sets...", null, new Runnable() {
@Override
public void run() {
DeckSetFilter existingFilter = getFilter(DeckSetFilter.class);
if (existingFilter != null) {
existingFilter.edit();
}
else {
final DialogChooseSets dialog = new DialogChooseSets(null, null, true);
dialog.setOkCallback(new Runnable() {
@Override
public void run() {
List<String> sets = dialog.getSelectedSets();
if (!sets.isEmpty()) {
addFilter(new DeckSetFilter(DeckManager.this, sets, dialog.getWantReprints()));
}
}
});
}
}
});
JMenu world = GuiUtils.createMenu("Quest world");
for (final QuestWorld w : FModel.getWorlds()) {
GuiUtils.addMenuItem(world, w.getName(), null, new Runnable() {
@Override
public void run() {
addFilter(new DeckQuestWorldFilter(DeckManager.this, w));
}
}, DeckQuestWorldFilter.canAddQuestWorld(w, getFilter(DeckQuestWorldFilter.class)));
}
menu.add(world);
GuiUtils.addSeparator(menu);
GuiUtils.addMenuItem(menu, "Colors", null, new Runnable() {
@Override
public void run() {
addFilter(new DeckColorFilter(DeckManager.this));
}
}, getFilter(DeckColorFilter.class) == null);
}
private void editDeck(final DeckProxy deck) {
if (deck == null) { return; }
ACEditorBase<? extends InventoryItem, ? extends DeckBase> editorCtrl = null;
FScreen screen = null;
switch (this.gametype) {
case Quest:
screen = FScreen.DECK_EDITOR_QUEST;
editorCtrl = new CEditorQuest(FModel.getQuest());
break;
case Constructed:
screen = FScreen.DECK_EDITOR_CONSTRUCTED;
DeckPreferences.setCurrentDeck(deck.toString());
//re-use constructed controller
break;
case Sealed:
screen = FScreen.DECK_EDITOR_SEALED;
editorCtrl = new CEditorLimited(FModel.getDecks().getSealed(), screen);
break;
case Draft:
screen = FScreen.DECK_EDITOR_DRAFT;
editorCtrl = new CEditorLimited(FModel.getDecks().getDraft(), screen);
break;
default:
return;
}
if (!Singletons.getControl().ensureScreenActive(screen)) { return; }
if (editorCtrl != null) {
CDeckEditorUI.SINGLETON_INSTANCE.setEditorController(editorCtrl);
}
if (!SEditorIO.confirmSaveChanges(screen, true)) { return; } //ensure previous deck on screen is saved if needed
CDeckEditorUI.SINGLETON_INSTANCE.getCurrentEditorController().getDeckController().load(deck.getPath(), deck.getName());
}
public boolean deleteDeck(DeckProxy deck) {
if (deck == null) { return false; }
if (!FOptionPane.showConfirmDialog(
"Are you sure you want to delete '" + deck.getName() + "'?",
"Delete Deck", "Delete", "Cancel", false)) {
return false;
}
// consider using deck proxy's method to delete deck
switch(this.gametype) {
case Constructed:
case Draft:
case Sealed:
deck.deleteFromStorage();
break;
case Quest:
deck.deleteFromStorage();
FModel.getQuest().save();
break;
default:
throw new UnsupportedOperationException("Delete not implemneted for game type = " + gametype.toString());
}
this.removeItem(deck, 1);
if (this.cmdDelete != null) {
this.cmdDelete.run();
}
return true;
}
public class DeckActionsRenderer extends ItemCellRenderer {
private int overActionIndex = -1;
private static final int imgSize = 20;
@Override
public boolean alwaysShowTooltip() {
return false;
}
/*
* (non-Javadoc)
*
* @see
* javax.swing.table.DefaultTableCellRenderer#getTableCellRendererComponent
* (javax.swing.JTable, java.lang.Object, boolean, boolean, int, int)
*/
@Override
public final Component getTableCellRendererComponent(final JTable table, final Object value,
final boolean isSelected, final boolean hasFocus, final int row, final int column) {
setToolTipText(null);
return super.getTableCellRendererComponent(table, "", isSelected, hasFocus, row, column);
}
@Override
public <T extends InventoryItem> void processMouseEvent(final MouseEvent e, final ItemListView<T> listView, final Object value, final int row, final int column) {
Rectangle cellBounds = listView.getTable().getCellRect(row, column, false);
int x = e.getX() - cellBounds.x;
if (e.getID() == MouseEvent.MOUSE_PRESSED && e.getButton() == 1) {
DeckProxy deck = (DeckProxy) value;
if (x >= 0 && x < imgSize) { //delete button
if (DeckManager.this.deleteDeck(deck)) {
e.consume();
return;
}
}
else if (x >= imgSize && x < imgSize * 2) { //edit button
DeckManager.this.editDeck(deck);
}
listView.getTable().setRowSelectionInterval(row, row);
listView.getTable().repaint();
e.consume();
}
}
/*private void setOverActionIndex(final ItemListView<?> listView, int overActionIndex0) {
if (this.overActionIndex == overActionIndex0) { return; }
this.overActionIndex = overActionIndex0;
switch (this.overActionIndex) {
case -1:
this.setToolTipText(null);
break;
case 0:
this.setToolTipText("Delete this deck");
break;
case 1:
this.setToolTipText("Edit this deck");
break;
}
listView.getTable().repaint();
}*/
/*
* (non-Javadoc)
*
* @see javax.swing.JComponent#paint(java.awt.Graphics)
*/
@Override
public final void paint(final Graphics g) {
super.paint(g);
FSkin.drawImage(g, overActionIndex == 0 ? icoDeleteOver : icoDelete, 0, 0, imgSize, imgSize);
FSkin.drawImage(g, overActionIndex == 1 ? icoEditOver : icoEdit, imgSize - 1, -1, imgSize, imgSize);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package forge.itemmanager;
import forge.toolbox.FSkin.SkinnedScrollPane;
import javax.swing.*;
import javax.swing.border.Border;
/**
* Simple container pane meant to contain item managers.
*
*/
@SuppressWarnings("serial")
public final class ItemManagerContainer extends SkinnedScrollPane {
public ItemManagerContainer() {
this(null);
}
public ItemManagerContainer(ItemManager<?> itemManager) {
super(null, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
setBorder((Border)null);
setOpaque(false);
getViewport().setOpaque(false);
setItemManager(itemManager);
}
/**
*
* Gets the item pool.
*
* @return ItemPoolView
*/
public void setItemManager(ItemManager<?> itemManager) {
if (itemManager != null) {
itemManager.initialize(); //ensure item manager is initialized
}
this.getViewport().setView(itemManager);
}
}

View File

@@ -0,0 +1,32 @@
package forge.itemmanager;
import forge.item.InventoryItem;
import forge.itemmanager.filters.ItemFilter;
import javax.swing.*;
/**
* TODO: Write javadoc for this type.
*
*/
@SuppressWarnings("serial")
public final class SpellShopManager extends ItemManager<InventoryItem> {
public SpellShopManager(boolean wantUnique0) {
super(InventoryItem.class, wantUnique0);
}
@Override
protected void addDefaultFilters() {
CardManager.addDefaultFilters(this);
}
@Override
protected ItemFilter<? extends InventoryItem> createSearchFilter() {
return CardManager.createSearchFilter(this);
}
@Override
protected void buildAddFilterMenu(JMenu menu) {
CardManager.buildAddFilterMenu(menu, this);
}
}

View File

@@ -0,0 +1,65 @@
package forge.itemmanager.filters;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.card.CardRules;
import forge.item.PaperCard;
import forge.itemmanager.ItemManager;
import forge.itemmanager.SpellShopManager;
import forge.itemmanager.SItemManagerUtil.StatTypes;
import javax.swing.*;
import java.util.ArrayList;
import java.util.List;
/**
* TODO: Write javadoc for this type.
*
*/
public class CardCMCFilter extends StatTypeFilter<PaperCard> {
public CardCMCFilter(ItemManager<? super PaperCard> itemManager0) {
super(itemManager0);
}
@Override
public ItemFilter<PaperCard> createCopy() {
return new CardCMCFilter(itemManager);
}
@Override
protected void buildWidget(JPanel widget) {
if (itemManager instanceof SpellShopManager) {
addToggleButton(widget, StatTypes.PACK_OR_DECK);
}
addToggleButton(widget, StatTypes.CMC_0);
addToggleButton(widget, StatTypes.CMC_1);
addToggleButton(widget, StatTypes.CMC_2);
addToggleButton(widget, StatTypes.CMC_3);
addToggleButton(widget, StatTypes.CMC_4);
addToggleButton(widget, StatTypes.CMC_5);
addToggleButton(widget, StatTypes.CMC_6);
}
@Override
protected final Predicate<PaperCard> buildPredicate() {
final List<Predicate<CardRules>> cmcs = new ArrayList<Predicate<CardRules>>();
for (StatTypes s : buttonMap.keySet()) {
if (s.predicate != null && buttonMap.get(s).isSelected()) {
cmcs.add(s.predicate);
}
}
if (cmcs.size() == buttonMap.size()) {
return new Predicate<PaperCard>() { //use custom return true delegate to validate the item is a card
@Override
public boolean apply(PaperCard card) {
return true;
}
};
}
return Predicates.compose(Predicates.or(cmcs), PaperCard.FN_GET_RULES);
}
}

View File

@@ -0,0 +1,38 @@
package forge.itemmanager.filters;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.card.CardRules;
import forge.card.CardRulesPredicates;
import forge.item.PaperCard;
import forge.itemmanager.ItemManager;
/**
* TODO: Write javadoc for this type.
*
*/
public class CardCMCRangeFilter extends ValueRangeFilter<PaperCard> {
public CardCMCRangeFilter(ItemManager<? super PaperCard> itemManager0) {
super(itemManager0);
}
@Override
public ItemFilter<PaperCard> createCopy() {
return new CardCMCRangeFilter(itemManager);
}
@Override
protected String getCaption() {
return "CMC";
}
@Override
protected Predicate<PaperCard> buildPredicate() {
Predicate<CardRules> predicate = getCardRulesFieldPredicate(CardRulesPredicates.LeafNumber.CardField.CMC);
if (predicate == null) {
return Predicates.alwaysTrue();
}
return Predicates.compose(predicate, PaperCard.FN_GET_RULES);
}
}

View File

@@ -0,0 +1,42 @@
package forge.itemmanager.filters;
import com.google.common.base.Predicate;
import forge.item.PaperCard;
import forge.itemmanager.ItemManager;
import forge.itemmanager.SFilterUtil;
import forge.itemmanager.SpellShopManager;
import forge.itemmanager.SItemManagerUtil.StatTypes;
import javax.swing.*;
public class CardColorFilter extends StatTypeFilter<PaperCard> {
public CardColorFilter(ItemManager<? super PaperCard> itemManager0) {
super(itemManager0);
}
@Override
public ItemFilter<PaperCard> createCopy() {
return new CardColorFilter(itemManager);
}
@Override
protected void buildWidget(JPanel widget) {
if (itemManager instanceof SpellShopManager) {
addToggleButton(widget, StatTypes.PACK_OR_DECK);
}
addToggleButton(widget, StatTypes.WHITE);
addToggleButton(widget, StatTypes.BLUE);
addToggleButton(widget, StatTypes.BLACK);
addToggleButton(widget, StatTypes.RED);
addToggleButton(widget, StatTypes.GREEN);
addToggleButton(widget, StatTypes.COLORLESS);
addToggleButton(widget, StatTypes.MULTICOLOR);
}
@Override
protected final Predicate<PaperCard> buildPredicate() {
return SFilterUtil.buildColorFilter(buttonMap);
}
}

View File

@@ -0,0 +1,30 @@
package forge.itemmanager.filters;
import com.google.common.base.Predicate;
import forge.game.GameFormat;
import forge.item.PaperCard;
import forge.itemmanager.ItemManager;
import forge.itemmanager.SFilterUtil;
public class CardFormatFilter extends FormatFilter<PaperCard> {
public CardFormatFilter(ItemManager<? super PaperCard> itemManager0) {
super(itemManager0);
}
public CardFormatFilter(ItemManager<? super PaperCard> itemManager0, GameFormat format0) {
super(itemManager0, format0);
}
@Override
public ItemFilter<PaperCard> createCopy() {
CardFormatFilter copy = new CardFormatFilter(itemManager);
copy.formats.addAll(this.formats);
return copy;
}
@Override
protected final Predicate<PaperCard> buildPredicate() {
return SFilterUtil.buildFormatFilter(this.formats, this.allowReprints);
}
}

View File

@@ -0,0 +1,39 @@
package forge.itemmanager.filters;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.card.CardRules;
import forge.card.CardRulesPredicates;
import forge.item.PaperCard;
import forge.itemmanager.ItemManager;
/**
* TODO: Write javadoc for this type.
*
*/
public class CardPowerFilter extends ValueRangeFilter<PaperCard> {
public CardPowerFilter(ItemManager<? super PaperCard> itemManager0) {
super(itemManager0);
}
@Override
public ItemFilter<PaperCard> createCopy() {
return new CardPowerFilter(itemManager);
}
@Override
protected String getCaption() {
return "Power";
}
@Override
protected Predicate<PaperCard> buildPredicate() {
Predicate<CardRules> predicate = getCardRulesFieldPredicate(CardRulesPredicates.LeafNumber.CardField.POWER);
if (predicate == null) {
return Predicates.alwaysTrue();
}
predicate = Predicates.and(predicate, CardRulesPredicates.Presets.IS_CREATURE);
return Predicates.compose(predicate, PaperCard.FN_GET_RULES);
}
}

View File

@@ -0,0 +1,90 @@
package forge.itemmanager.filters;
import forge.game.GameFormat;
import forge.item.PaperCard;
import forge.itemmanager.ItemManager;
import forge.model.FModel;
import forge.quest.QuestWorld;
import java.util.HashSet;
import java.util.Set;
public class CardQuestWorldFilter extends CardFormatFilter {
private final Set<QuestWorld> questWorlds = new HashSet<QuestWorld>();
public CardQuestWorldFilter(ItemManager<? super PaperCard> itemManager0) {
super(itemManager0);
}
public CardQuestWorldFilter(ItemManager<? super PaperCard> itemManager0, QuestWorld questWorld0) {
super(itemManager0);
this.questWorlds.add(questWorld0);
this.formats.add(getQuestWorldFormat(questWorld0));
}
@Override
public ItemFilter<PaperCard> createCopy() {
CardQuestWorldFilter copy = new CardQuestWorldFilter(itemManager);
copy.questWorlds.addAll(this.questWorlds);
for (QuestWorld w : this.questWorlds) {
copy.formats.add(getQuestWorldFormat(w));
}
return copy;
}
@Override
public void reset() {
this.questWorlds.clear();
super.reset();
}
public static boolean canAddQuestWorld(QuestWorld questWorld, ItemFilter<PaperCard> existingFilter) {
if (questWorld.getFormat() == null && FModel.getQuest().getMainFormat() == null) {
return false; //must have format
}
return existingFilter == null || !((CardQuestWorldFilter)existingFilter).questWorlds.contains(questWorld);
}
/**
* Merge the given filter with this filter if possible
* @param filter
* @return true if filter merged in or to suppress adding a new filter, false to allow adding new filter
*/
@Override
public boolean merge(ItemFilter<?> filter) {
CardQuestWorldFilter cardQuestWorldFilter = (CardQuestWorldFilter)filter;
this.questWorlds.addAll(cardQuestWorldFilter.questWorlds);
for (QuestWorld w : cardQuestWorldFilter.questWorlds) {
this.formats.add(getQuestWorldFormat(w));
}
return true;
}
@Override
protected String getCaption() {
return "Quest World";
}
@Override
protected int getCount() {
return this.questWorlds.size();
}
@Override
protected Iterable<String> getList() {
Set<String> strings = new HashSet<String>();
for (QuestWorld w : this.questWorlds) {
strings.add(w.getName());
}
return strings;
}
private GameFormat getQuestWorldFormat(QuestWorld w) {
GameFormat format = w.getFormat();
if (format == null) {
//assumes that no world other than the main world will have a null format
format = FModel.getQuest().getMainFormat();
}
return format;
}
}

View File

@@ -0,0 +1,118 @@
package forge.itemmanager.filters;
import com.google.common.base.Predicate;
import forge.UiCommand;
import forge.item.InventoryItem;
import forge.item.PaperCard;
import forge.itemmanager.ItemManager;
import forge.itemmanager.SFilterUtil;
import forge.toolbox.FComboBoxWrapper;
import forge.toolbox.FLabel;
import forge.toolbox.FTextField;
import forge.toolbox.LayoutHelper;
import javax.swing.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
public class CardSearchFilter extends TextSearchFilter<PaperCard> {
private FComboBoxWrapper<String> cbSearchMode;
private FLabel btnName, btnType, btnText;
public CardSearchFilter(ItemManager<? super PaperCard> itemManager0) {
super(itemManager0);
}
@Override
public ItemFilter<PaperCard> createCopy() {
CardSearchFilter copy = new CardSearchFilter(itemManager);
copy.getWidget(); //initialize widget
copy.txtSearch.setText(this.txtSearch.getText());
copy.cbSearchMode.setSelectedIndex(this.cbSearchMode.getSelectedIndex());
copy.btnName.setSelected(this.btnName.isSelected());
copy.btnType.setSelected(this.btnType.isSelected());
copy.btnText.setSelected(this.btnText.isSelected());
return copy;
}
@Override
public void reset() {
super.reset();
this.cbSearchMode.setSelectedIndex(0);
this.btnName.setSelected(true);
this.btnType.setSelected(true);
this.btnText.setSelected(true);
}
@Override
protected final void buildWidget(JPanel widget) {
super.buildWidget(widget);
cbSearchMode = new FComboBoxWrapper<String>();
cbSearchMode.addItem("in");
cbSearchMode.addItem("not in");
cbSearchMode.addTo(widget);
cbSearchMode.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent arg0) {
if (!txtSearch.isEmpty()) {
applyChange();
}
}
});
btnName = addButton(widget, "Name");
btnType = addButton(widget, "Type");
btnText = addButton(widget, "Text");
}
@Override
protected void doWidgetLayout(LayoutHelper helper) {
final int comboBoxWidth = 61;
final int buttonWidth = 51;
helper.fillLine(txtSearch, FTextField.HEIGHT, comboBoxWidth + buttonWidth * 3 + 12); //leave space for combo box and buttons
helper.include(cbSearchMode.getComponent(), comboBoxWidth, FTextField.HEIGHT);
helper.include(btnName, buttonWidth, FTextField.HEIGHT);
helper.include(btnType, buttonWidth, FTextField.HEIGHT);
helper.include(btnText, buttonWidth, FTextField.HEIGHT);
}
@SuppressWarnings("serial")
private FLabel addButton(JPanel widget, String text) {
FLabel button = new FLabel.Builder().text(text).hoverable().selectable().selected().build();
button.setCommand(new UiCommand() {
@Override
public void run() {
applyChange();
}
});
widget.add(button);
return button;
}
@Override
protected Predicate<PaperCard> buildPredicate() {
return SFilterUtil.buildTextFilter(
txtSearch.getText(),
cbSearchMode.getSelectedIndex() != 0,
btnName.isSelected(),
btnType.isSelected(),
btnText.isSelected());
}
@Override
protected <U extends InventoryItem> boolean showUnsupportedItem(U item) {
//fallback to regular item text filter if item not PaperCard
boolean result = btnName.isSelected() && SFilterUtil.buildItemTextFilter(txtSearch.getText()).apply(item);
if (cbSearchMode.getSelectedIndex() != 0) { //invert result if needed
result = !result;
}
return result;
}
}

View File

@@ -0,0 +1,80 @@
package forge.itemmanager.filters;
import forge.game.GameFormat;
import forge.item.PaperCard;
import forge.itemmanager.ItemManager;
import forge.screens.home.quest.DialogChooseSets;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* TODO: Write javadoc for this type.
*
*/
public class CardSetFilter extends CardFormatFilter {
private final Set<String> sets = new HashSet<String>();
public CardSetFilter(ItemManager<? super PaperCard> itemManager0, Collection<String> sets0, boolean allowReprints0) {
super(itemManager0);
this.sets.addAll(sets0);
this.formats.add(new GameFormat(null, this.sets, null));
this.allowReprints = allowReprints0;
}
@Override
public ItemFilter<PaperCard> createCopy() {
return new CardSetFilter(itemManager, this.sets, this.allowReprints);
}
@Override
public void reset() {
this.sets.clear();
super.reset();
}
/**
* Merge the given filter with this filter if possible
* @param filter
* @return true if filter merged in or to suppress adding a new filter, false to allow adding new filter
*/
@Override
public boolean merge(ItemFilter<?> filter) {
CardSetFilter cardSetFilter = (CardSetFilter)filter;
this.sets.addAll(cardSetFilter.sets);
this.allowReprints = cardSetFilter.allowReprints;
this.formats.clear();
this.formats.add(new GameFormat(null, this.sets, null));
return true;
}
public void edit() {
final DialogChooseSets dialog = new DialogChooseSets(this.sets, null, true);
dialog.setOkCallback(new Runnable() {
@Override
public void run() {
sets.clear();
sets.addAll(dialog.getSelectedSets());
allowReprints = dialog.getWantReprints();
formats.clear();
formats.add(new GameFormat(null, sets, null));
}
});
}
@Override
protected String getCaption() {
return "Set";
}
@Override
protected int getCount() {
return this.sets.size();
}
@Override
protected Iterable<String> getList() {
return this.sets;
}
}

View File

@@ -0,0 +1,39 @@
package forge.itemmanager.filters;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.card.CardRules;
import forge.card.CardRulesPredicates;
import forge.item.PaperCard;
import forge.itemmanager.ItemManager;
/**
* TODO: Write javadoc for this type.
*
*/
public class CardToughnessFilter extends ValueRangeFilter<PaperCard> {
public CardToughnessFilter(ItemManager<? super PaperCard> itemManager0) {
super(itemManager0);
}
@Override
public ItemFilter<PaperCard> createCopy() {
return new CardToughnessFilter(itemManager);
}
@Override
protected String getCaption() {
return "Toughness";
}
@Override
protected Predicate<PaperCard> buildPredicate() {
Predicate<CardRules> predicate = getCardRulesFieldPredicate(CardRulesPredicates.LeafNumber.CardField.TOUGHNESS);
if (predicate == null) {
return Predicates.alwaysTrue();
}
predicate = Predicates.and(predicate, CardRulesPredicates.Presets.IS_CREATURE);
return Predicates.compose(predicate, PaperCard.FN_GET_RULES);
}
}

View File

@@ -0,0 +1,62 @@
package forge.itemmanager.filters;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.card.CardRules;
import forge.item.PaperCard;
import forge.itemmanager.ItemManager;
import forge.itemmanager.SpellShopManager;
import forge.itemmanager.SItemManagerUtil.StatTypes;
import javax.swing.*;
import java.util.ArrayList;
import java.util.List;
public class CardTypeFilter extends StatTypeFilter<PaperCard> {
public CardTypeFilter(ItemManager<? super PaperCard> itemManager0) {
super(itemManager0);
}
@Override
public ItemFilter<PaperCard> createCopy() {
return new CardTypeFilter(itemManager);
}
@Override
protected void buildWidget(JPanel widget) {
if (itemManager instanceof SpellShopManager) {
addToggleButton(widget, StatTypes.PACK_OR_DECK);
}
addToggleButton(widget, StatTypes.LAND);
addToggleButton(widget, StatTypes.ARTIFACT);
addToggleButton(widget, StatTypes.CREATURE);
addToggleButton(widget, StatTypes.ENCHANTMENT);
addToggleButton(widget, StatTypes.PLANESWALKER);
addToggleButton(widget, StatTypes.INSTANT);
addToggleButton(widget, StatTypes.SORCERY);
}
@Override
protected final Predicate<PaperCard> buildPredicate() {
final List<Predicate<CardRules>> types = new ArrayList<Predicate<CardRules>>();
for (StatTypes s : buttonMap.keySet()) {
if (s.predicate != null && buttonMap.get(s).isSelected()) {
types.add(s.predicate);
}
}
if (types.size() == buttonMap.size()) {
return new Predicate<PaperCard>() { //use custom return true delegate to validate the item is a card
@Override
public boolean apply(PaperCard card) {
return true;
}
};
}
return Predicates.compose(Predicates.or(types), PaperCard.FN_GET_RULES);
}
}

View File

@@ -0,0 +1,142 @@
package forge.itemmanager.filters;
import com.google.common.base.Predicate;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.deck.DeckProxy;
import forge.itemmanager.ItemManager;
import forge.itemmanager.SItemManagerUtil.StatTypes;
import forge.util.BinaryUtil;
import forge.util.ItemPool;
import javax.swing.*;
public class DeckColorFilter extends StatTypeFilter<DeckProxy> {
public DeckColorFilter(ItemManager<? super DeckProxy> itemManager0) {
super(itemManager0);
}
@Override
public ItemFilter<DeckProxy> createCopy() {
return new DeckColorFilter(itemManager);
}
@Override
protected void buildWidget(JPanel widget) {
addToggleButton(widget, StatTypes.DECK_WHITE);
addToggleButton(widget, StatTypes.DECK_BLUE);
addToggleButton(widget, StatTypes.DECK_BLACK);
addToggleButton(widget, StatTypes.DECK_RED);
addToggleButton(widget, StatTypes.DECK_GREEN);
addToggleButton(widget, StatTypes.DECK_COLORLESS);
addToggleButton(widget, StatTypes.DECK_MULTICOLOR);
}
@Override
protected final Predicate<DeckProxy> buildPredicate() {
return new Predicate<DeckProxy>() {
@Override
public boolean apply(DeckProxy input) {
byte colorProfile = input.getColor().getColor();
if (colorProfile == 0) {
return buttonMap.get(StatTypes.DECK_COLORLESS).isSelected();
}
boolean wantMulticolor = buttonMap.get(StatTypes.DECK_MULTICOLOR).isSelected();
if (!wantMulticolor && BinaryUtil.bitCount(colorProfile) > 1) {
return false;
}
byte colors = 0;
if (buttonMap.get(StatTypes.DECK_WHITE).isSelected()) {
colors |= MagicColor.WHITE;
}
if (buttonMap.get(StatTypes.DECK_BLUE).isSelected()) {
colors |= MagicColor.BLUE;
}
if (buttonMap.get(StatTypes.DECK_BLACK).isSelected()) {
colors |= MagicColor.BLACK;
}
if (buttonMap.get(StatTypes.DECK_RED).isSelected()) {
colors |= MagicColor.RED;
}
if (buttonMap.get(StatTypes.DECK_GREEN).isSelected()) {
colors |= MagicColor.GREEN;
}
if (colors == 0 && wantMulticolor && BinaryUtil.bitCount(colorProfile) > 1) {
return true;
}
return (colorProfile & colors) == colorProfile;
}
};
}
private static final Predicate<DeckProxy> IS_WHITE = new Predicate<DeckProxy>() {
@Override
public boolean apply(final DeckProxy deck) {
ColorSet cs = deck.getColor();
return cs != null && cs.hasAnyColor(MagicColor.WHITE);
}
};
private static final Predicate<DeckProxy> IS_BLUE = new Predicate<DeckProxy>() {
@Override
public boolean apply(final DeckProxy deck) {
ColorSet cs = deck.getColor();
return cs != null && cs.hasAnyColor(MagicColor.BLUE);
}
};
public static final Predicate<DeckProxy> IS_BLACK = new Predicate<DeckProxy>() {
@Override
public boolean apply(final DeckProxy deck) {
ColorSet cs = deck.getColor();
return cs != null && cs.hasAnyColor(MagicColor.BLACK);
}
};
public static final Predicate<DeckProxy> IS_RED = new Predicate<DeckProxy>() {
@Override
public boolean apply(final DeckProxy deck) {
ColorSet cs = deck.getColor();
return cs != null && cs.hasAnyColor(MagicColor.RED);
}
};
public static final Predicate<DeckProxy> IS_GREEN = new Predicate<DeckProxy>() {
@Override
public boolean apply(final DeckProxy deck) {
ColorSet cs = deck.getColor();
return cs != null && cs.hasAnyColor(MagicColor.GREEN);
}
};
private static final Predicate<DeckProxy> IS_COLORLESS = new Predicate<DeckProxy>() {
@Override
public boolean apply(final DeckProxy deck) {
ColorSet cs = deck.getColor();
return cs != null && cs.getColor() == 0;
}
};
private static final Predicate<DeckProxy> IS_MULTICOLOR = new Predicate<DeckProxy>() {
@Override
public boolean apply(final DeckProxy deck) {
ColorSet cs = deck.getColor();
return cs != null && BinaryUtil.bitCount(cs.getColor()) > 1;
}
};
@Override
public void afterFiltersApplied() {
final ItemPool<? super DeckProxy> items = itemManager.getFilteredItems();
buttonMap.get(StatTypes.DECK_WHITE).setText(String.valueOf(items.countAll(IS_WHITE, DeckProxy.class)));
buttonMap.get(StatTypes.DECK_BLUE).setText(String.valueOf(items.countAll(IS_BLUE, DeckProxy.class)));
buttonMap.get(StatTypes.DECK_BLACK).setText(String.valueOf(items.countAll(IS_BLACK, DeckProxy.class)));
buttonMap.get(StatTypes.DECK_RED).setText(String.valueOf(items.countAll(IS_RED, DeckProxy.class)));
buttonMap.get(StatTypes.DECK_GREEN).setText(String.valueOf(items.countAll(IS_GREEN, DeckProxy.class)));
buttonMap.get(StatTypes.DECK_COLORLESS).setText(String.valueOf(items.countAll(IS_COLORLESS, DeckProxy.class)));
buttonMap.get(StatTypes.DECK_MULTICOLOR).setText(String.valueOf(items.countAll(IS_MULTICOLOR, DeckProxy.class)));
getWidget().revalidate();
}
}

View File

@@ -0,0 +1,82 @@
package forge.itemmanager.filters;
import com.google.common.base.Predicate;
import forge.deck.DeckProxy;
import forge.itemmanager.ItemManager;
import java.util.HashSet;
import java.util.Set;
/**
* TODO: Write javadoc for this type.
*
*/
public class DeckFolderFilter extends ListLabelFilter<DeckProxy> {
protected final Set<String> folders = new HashSet<String>();
public DeckFolderFilter(ItemManager<? super DeckProxy> itemManager0) {
super(itemManager0);
}
public DeckFolderFilter(ItemManager<? super DeckProxy> itemManager0, String folder0) {
super(itemManager0);
this.folders.add(folder0);
}
@Override
public ItemFilter<DeckProxy> createCopy() {
DeckFolderFilter copy = new DeckFolderFilter(itemManager);
copy.folders.addAll(this.folders);
return copy;
}
@Override
protected final Predicate<DeckProxy> buildPredicate() {
return new Predicate<DeckProxy>() {
@Override
public boolean apply(DeckProxy input) {
String path = input.getPath();
for (String folder : folders) {
if (path.startsWith(folder)) {
return true;
}
}
return false;
}
};
}
@Override
protected String getCaption() {
return "Folder";
}
@Override
protected Iterable<String> getList() {
return this.folders;
}
@Override
protected String getTooltip() {
return null;
}
@Override
protected int getCount() {
return this.folders.size();
}
@Override
public void reset() {
this.folders.clear();
this.updateLabel();
}
@Override
public boolean merge(ItemFilter<?> filter) {
DeckFolderFilter formatFilter = (DeckFolderFilter)filter;
this.folders.addAll(formatFilter.folders);
return true;
}
}

View File

@@ -0,0 +1,30 @@
package forge.itemmanager.filters;
import com.google.common.base.Predicate;
import forge.game.GameFormat;
import forge.deck.DeckProxy;
import forge.itemmanager.ItemManager;
import forge.itemmanager.SFilterUtil;
public class DeckFormatFilter extends FormatFilter<DeckProxy> {
public DeckFormatFilter(ItemManager<? super DeckProxy> itemManager0) {
super(itemManager0);
}
public DeckFormatFilter(ItemManager<? super DeckProxy> itemManager0, GameFormat format0) {
super(itemManager0, format0);
}
@Override
public ItemFilter<DeckProxy> createCopy() {
DeckFormatFilter copy = new DeckFormatFilter(itemManager);
copy.formats.addAll(this.formats);
return copy;
}
@Override
protected final Predicate<DeckProxy> buildPredicate() {
return DeckProxy.createPredicate(SFilterUtil.buildFormatFilter(this.formats, this.allowReprints));
}
}

View File

@@ -0,0 +1,90 @@
package forge.itemmanager.filters;
import forge.game.GameFormat;
import forge.itemmanager.ItemManager;
import forge.deck.DeckProxy;
import forge.model.FModel;
import forge.quest.QuestWorld;
import java.util.HashSet;
import java.util.Set;
public class DeckQuestWorldFilter extends DeckFormatFilter {
private final Set<QuestWorld> questWorlds = new HashSet<QuestWorld>();
public DeckQuestWorldFilter(ItemManager<? super DeckProxy> itemManager0) {
super(itemManager0);
}
public DeckQuestWorldFilter(ItemManager<? super DeckProxy> itemManager0, QuestWorld questWorld0) {
super(itemManager0);
this.questWorlds.add(questWorld0);
this.formats.add(getQuestWorldFormat(questWorld0));
}
@Override
public ItemFilter<DeckProxy> createCopy() {
DeckQuestWorldFilter copy = new DeckQuestWorldFilter(itemManager);
copy.questWorlds.addAll(this.questWorlds);
for (QuestWorld w : this.questWorlds) {
copy.formats.add(getQuestWorldFormat(w));
}
return copy;
}
@Override
public void reset() {
this.questWorlds.clear();
super.reset();
}
public static boolean canAddQuestWorld(QuestWorld questWorld, ItemFilter<DeckProxy> existingFilter) {
if (questWorld.getFormat() == null && FModel.getQuest().getMainFormat() == null) {
return false; //must have format
}
return existingFilter == null || !((DeckQuestWorldFilter)existingFilter).questWorlds.contains(questWorld);
}
/**
* Merge the given filter with this filter if possible
* @param filter
* @return true if filter merged in or to suppress adding a new filter, false to allow adding new filter
*/
@Override
public boolean merge(ItemFilter<?> filter) {
DeckQuestWorldFilter cardQuestWorldFilter = (DeckQuestWorldFilter)filter;
this.questWorlds.addAll(cardQuestWorldFilter.questWorlds);
for (QuestWorld w : cardQuestWorldFilter.questWorlds) {
this.formats.add(getQuestWorldFormat(w));
}
return true;
}
@Override
protected String getCaption() {
return "Quest World";
}
@Override
protected int getCount() {
return this.questWorlds.size();
}
@Override
protected Iterable<String> getList() {
Set<String> strings = new HashSet<String>();
for (QuestWorld w : this.questWorlds) {
strings.add(w.getName());
}
return strings;
}
private GameFormat getQuestWorldFormat(QuestWorld w) {
GameFormat format = w.getFormat();
if (format == null) {
//assumes that no world other than the main world will have a null format
format = FModel.getQuest().getMainFormat();
}
return format;
}
}

View File

@@ -0,0 +1,22 @@
package forge.itemmanager.filters;
import forge.deck.DeckProxy;
import forge.itemmanager.ItemManager;
/**
* TODO: Write javadoc for this type.
*
*/
public class DeckSearchFilter extends TextSearchFilter<DeckProxy> {
public DeckSearchFilter(ItemManager<? super DeckProxy> itemManager0) {
super(itemManager0);
}
@Override
public ItemFilter<DeckProxy> createCopy() {
DeckSearchFilter copy = new DeckSearchFilter(itemManager);
copy.getWidget(); //initialize widget
copy.txtSearch.setText(this.txtSearch.getText());
return copy;
}
}

View File

@@ -0,0 +1,77 @@
package forge.itemmanager.filters;
import forge.deck.DeckProxy;
import forge.game.GameFormat;
import forge.itemmanager.ItemManager;
import forge.screens.home.quest.DialogChooseSets;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
public class DeckSetFilter extends DeckFormatFilter {
private final Set<String> sets = new HashSet<String>();
public DeckSetFilter(ItemManager<? super DeckProxy> itemManager0, Collection<String> sets0, boolean allowReprints0) {
super(itemManager0);
this.sets.addAll(sets0);
this.formats.add(new GameFormat(null, this.sets, null));
this.allowReprints = allowReprints0;
}
@Override
public ItemFilter<DeckProxy> createCopy() {
return new DeckSetFilter(itemManager, this.sets, this.allowReprints);
}
@Override
public void reset() {
this.sets.clear();
super.reset();
}
/**
* Merge the given filter with this filter if possible
* @param filter
* @return true if filter merged in or to suppress adding a new filter, false to allow adding new filter
*/
@Override
public boolean merge(ItemFilter<?> filter) {
DeckSetFilter cardSetFilter = (DeckSetFilter)filter;
this.sets.addAll(cardSetFilter.sets);
this.allowReprints = cardSetFilter.allowReprints;
this.formats.clear();
this.formats.add(new GameFormat(null, this.sets, null));
return true;
}
public void edit() {
final DialogChooseSets dialog = new DialogChooseSets(this.sets, null, true);
dialog.setOkCallback(new Runnable() {
@Override
public void run() {
sets.clear();
sets.addAll(dialog.getSelectedSets());
allowReprints = dialog.getWantReprints();
formats.clear();
formats.add(new GameFormat(null, sets, null));
}
});
}
@Override
protected String getCaption() {
return "Set";
}
@Override
protected int getCount() {
return this.sets.size();
}
@Override
protected Iterable<String> getList() {
return this.sets;
}
}

Some files were not shown because too many files have changed in this diff Show More