diff --git a/.github/workflows/copy-label-to-pr.yml b/.github/workflows/copy-label-to-pr.yml new file mode 100644 index 0000000000..6674392ea9 --- /dev/null +++ b/.github/workflows/copy-label-to-pr.yml @@ -0,0 +1,30 @@ +name: Copy Labels to PR +on: + pull_request: + types: [opened] + +jobs: + copy-labels: + permissions: + contents: read + issues: write + pull-requests: write + runs-on: ubuntu-latest + name: Copy labels from linked issues + steps: + - name: copy-labels + uses: michalvankodev/copy-issue-labels@v1.3.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + labels-to-include: | + bug + build-configuration + breaking-change + code-maintenance + dependencies + feature + ignore-for-release-note + performance + refactor + repo-admin + translations diff --git a/build.gradle b/build.gradle index c76862707b..b3249fb704 100644 --- a/build.gradle +++ b/build.gradle @@ -18,21 +18,23 @@ plugins { id "base" id "java" id "org.ajoberstar.grgit" version "5.2.1" - id "com.diffplug.spotless" version "6.25.0" id 'org.openjfx.javafxplugin' version '0.0.13' id 'org.beryx.runtime' version '1.13.0' id "com.google.protobuf" version "0.9.4" id 'com.github.johnrengelman.shadow' version '8.1.1' + + id "com.diffplug.spotless" version "6.25.0" apply false +} + +allprojects { + apply plugin: "com.diffplug.spotless" } +apply from: rootProject.file('buildSrc/shared.gradle') + // Definitions defaultTasks 'clean', 'build' -java { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 -} - def javaArgs = [ "-Xss8M", "-Dsun.java2d.d3d=false", "-Dsentry.environment=Production", "-Dfile.encoding=UTF-8", "-Dpolyglot.engine.WarnInterpreterOnly=false", @@ -112,35 +114,21 @@ ext { println "OS Detected: " + osdetector.os } + spotless { java { - target project.fileTree(project.rootDir) { + target project.fileTree(project.projectDir) { include 'src/**/*.java' exclude '**/JTextAreaAppender.java' exclude 'src/main/java/net/rptools/maptool/client/ui/themes/Flat*ContrastIJTheme.java' exclude 'src/main/java/net/rptools/maptool/client/ui/themes/Utils.java' } - licenseHeaderFile 'spotless.license.java' - toggleOffOn() - - // Now using the Google Java style guide - googleJavaFormat("1.17.0") - } - - format 'misc', { - target '**/*.gradle', '**/.gitignore' - - // spotless has built-in rules for most basic formatting tasks - trimTrailingWhitespace() - // or spaces. Takes an integer argument if you don't like 4 - indentWithSpaces(4) } } - // org.openjfx.javafxplugin javafx { - version = '21' + version = '22' // modules = ['javafx.fxml', 'javafx.graphics'] modules = ['javafx.base', 'javafx.controls', 'javafx.media', 'javafx.swing', 'javafx.web', 'javafx.fxml', 'javafx.graphics'] } @@ -325,23 +313,13 @@ runtime { } } } -// In this section you declare where to find the dependencies of your project -repositories { - // Use 'jcenter' for resolving your dependencies. - // You can declare any Maven/Ivy/file repository here. - mavenLocal() - mavenCentral() - maven { url = 'https://maptool.craigs-stuff.net/repo/' } - maven { url = 'https://jitpack.io' } - maven { url "https://www.jetbrains.com/intellij-repository/releases" } - maven { url "https://cache-redirector.jetbrains.com/intellij-dependencies" } -} - // In this section you declare the dependencies for your production and test code dependencies { forms group: 'com.jetbrains.intellij.java', name: 'java-compiler-ant-tasks', version: '233.14475.56' + implementation project(':clientserver') + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.22.1' implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.22.1' implementation group: 'org.apache.logging.log4j', name: 'log4j-1.2-api', version: '2.20.0' // Bridges v1 to v2 for other code in other libs @@ -365,7 +343,7 @@ dependencies { implementation group: 'io.sentry', name: 'sentry-log4j2', version: '1.7.29' // parsing of configuration data - implementation group: 'org.apache.commons', name: 'commons-configuration2', version: '2.9.0' + implementation group: 'org.apache.commons', name: 'commons-configuration2', version: '2.10.1' // Specialized collections: ReferenceMap, LinkedMap. implementation 'org.apache.commons:commons-collections4:4.4' // Various file utilities @@ -394,17 +372,6 @@ dependencies { implementation group: 'com.jidesoft', name: 'jide-properties', version: '3.7.9' implementation group: 'com.jidesoft', name: 'jide-shortcut', version: '3.7.9' - // webserver for old webapp that is scheduled for removal - implementation 'org.eclipse.jetty:jetty-server:9.4.51.v20230217' - implementation 'org.eclipse.jetty:jetty-servlet:9.4.42.v20210604' - implementation 'org.eclipse.jetty:jetty-webapp:9.4.44.v20210927' - implementation 'org.eclipse.jetty:jetty-continuation:9.4.42.v20210604' - implementation 'org.eclipse.jetty.websocket:websocket-client:9.4.53.v20231009' - implementation 'org.eclipse.jetty.websocket:websocket-server:9.4.54.v20240208' - implementation 'org.eclipse.jetty.websocket:websocket-servlet:9.4.42.v20210604' - implementation 'org.eclipse.jetty.websocket:websocket-api:9.4.42.v20210604' - - // old json lib only used for one macro function. Use gson instead implementation 'net.sf.json-lib:json-lib:2.4:jdk15' @@ -429,7 +396,7 @@ dependencies { // themes implementation 'com.formdev:flatlaf:3.3' implementation 'com.formdev:flatlaf-intellij-themes:3.3' - implementation 'com.formdev:flatlaf-extras:3.3' + implementation 'com.formdev:flatlaf-extras:3.4.1' implementation 'com.github.weisj:jsvg:1.4.0' implementation 'com.formdev:flatlaf-jide-oss:3.3' @@ -449,7 +416,7 @@ dependencies { implementation 'com.github.gotson:webp-imageio:0.2.2' // webp support https://search.maven.org/artifact/com.github.gotson/webp-imageio/0.2.2/jar // For syntax highlighting in macro editor - implementation "com.fifesoft:rsyntaxtextarea:3.3.4" // https://mvnrepository.com/artifact/com.fifesoft/rsyntaxtextarea + implementation "com.fifesoft:rsyntaxtextarea:3.4.0" // https://mvnrepository.com/artifact/com.fifesoft/rsyntaxtextarea implementation "com.fifesoft:rstaui:3.3.1" // https://mvnrepository.com/artifact/com.fifesoft/rstaui implementation "com.fifesoft:autocomplete:3.3.1" // https://mvnrepository.com/artifact/com.fifesoft/autocomplete implementation "com.fifesoft:languagesupport:3.3.0" @@ -492,16 +459,6 @@ dependencies { // Noise Generator implementation 'com.github.cwisniew:NoiseLib:1.0.0' // The most recent version, 1.0.0 is build for a later java version: major version 55 is newer than 54, the highest major version supported by this compiler - // webrtc - implementation group: 'org.java-websocket', name: 'Java-WebSocket', version: '1.5.6' - implementation 'dev.onvoid.webrtc:webrtc-java:0.8.0' - if (osdetector.os.is('windows')) - implementation 'dev.onvoid.webrtc:webrtc-java:0.8.0:windows-x86_64' - else if (osdetector.os.is('osx')) - implementation 'dev.onvoid.webrtc:webrtc-java:0.8.0:macos-x86_64' - else if (osdetector.os.is('linux')) - implementation 'dev.onvoid.webrtc:webrtc-java:0.8.0:linux-x86_64' - // protobuf implementation "io.grpc:grpc-protobuf:1.61.1" implementation "com.google.protobuf:protobuf-java-util:3.25.2" @@ -516,9 +473,6 @@ dependencies { implementation 'org.jsoup:jsoup:1.17.2' // eventbus implementation 'com.google.guava:guava:33.0.0-jre' - // compression of messages between client and server - implementation 'org.apache.commons:commons-compress:1.25.0' - implementation 'com.github.luben:zstd-jni:1.5.5-11' // intellij forms runtime implementation 'com.jetbrains.intellij.java:java-gui-forms-rt:241.12019' // layout for forms created in code diff --git a/buildSrc/shared.gradle b/buildSrc/shared.gradle new file mode 100644 index 0000000000..3fd4380b24 --- /dev/null +++ b/buildSrc/shared.gradle @@ -0,0 +1,35 @@ +// In this section you declare where to find the dependencies of your project +repositories { + mavenLocal() + mavenCentral() + maven { url = 'https://maptool.craigs-stuff.net/repo/' } + maven { url = 'https://jitpack.io' } + maven { url "https://www.jetbrains.com/intellij-repository/releases" } + maven { url "https://cache-redirector.jetbrains.com/intellij-dependencies" } +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +spotless { + java { + target project.fileTree(project.projectDir) { + include 'src/**/*.java' + } + licenseHeaderFile rootProject.file('spotless.license.java') + toggleOffOn() + // Now using the Google Java style guide + googleJavaFormat("1.17.0") + } + + format 'misc', { + target '**/*.gradle', '**/.gitignore' + + // spotless has built-in rules for most basic formatting tasks + trimTrailingWhitespace() + // or spaces. Takes an integer argument if you don't like 4 + indentWithSpaces(4) + } +} \ No newline at end of file diff --git a/clientserver/build.gradle b/clientserver/build.gradle new file mode 100644 index 0000000000..74b62e7a09 --- /dev/null +++ b/clientserver/build.gradle @@ -0,0 +1,47 @@ +plugins { + id "base" + id "java-library" +} + +apply from: rootProject.file('buildSrc/shared.gradle') + +// In this section you declare the dependencies for your production and test code +dependencies { + implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' + + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.22.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.22.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-1.2-api', version: '2.20.0' // Bridges v1 to v2 for other code in other libs + implementation group: 'org.slf4j', name: 'slf4j-simple', version: '2.0.12' + implementation group: 'commons-logging', name: 'commons-logging', version: '1.3.0' + + // Better JSON functions... + implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1' // https://mvnrepository.com/artifact/com.google.code.gson/gson + + // webrtc + implementation group: 'org.java-websocket', name: 'Java-WebSocket', version: '1.5.6' + // Needs to be API since WebRTCConnection implements PeerConnectionObserver and RTCDataChannelObserver. + implementation 'dev.onvoid.webrtc:webrtc-java:0.8.0' + if (osdetector.os.is('windows')) + implementation 'dev.onvoid.webrtc:webrtc-java:0.8.0:windows-x86_64' + else if (osdetector.os.is('osx')) + implementation 'dev.onvoid.webrtc:webrtc-java:0.8.0:macos-x86_64' + else if (osdetector.os.is('linux')) + implementation 'dev.onvoid.webrtc:webrtc-java:0.8.0:linux-x86_64' + + // compression of messages between client and server + implementation 'org.apache.commons:commons-compress:1.26.0' + implementation 'com.github.luben:zstd-jni:1.5.5-11' + + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2' + // For mocking features during unit tests + testImplementation group: 'org.mockito', name: 'mockito-core', version: '5.10.0' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/src/main/java/net/rptools/clientserver/ActivityListener.java b/clientserver/src/main/java/net/rptools/clientserver/ActivityListener.java similarity index 100% rename from src/main/java/net/rptools/clientserver/ActivityListener.java rename to clientserver/src/main/java/net/rptools/clientserver/ActivityListener.java diff --git a/src/main/java/net/rptools/clientserver/simple/DisconnectHandler.java b/clientserver/src/main/java/net/rptools/clientserver/simple/DisconnectHandler.java similarity index 100% rename from src/main/java/net/rptools/clientserver/simple/DisconnectHandler.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/DisconnectHandler.java diff --git a/src/main/java/net/rptools/clientserver/simple/MessageHandler.java b/clientserver/src/main/java/net/rptools/clientserver/simple/MessageHandler.java similarity index 100% rename from src/main/java/net/rptools/clientserver/simple/MessageHandler.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/MessageHandler.java diff --git a/src/main/java/net/rptools/clientserver/simple/connection/AbstractConnection.java b/clientserver/src/main/java/net/rptools/clientserver/simple/connection/AbstractConnection.java similarity index 79% rename from src/main/java/net/rptools/clientserver/simple/connection/AbstractConnection.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/connection/AbstractConnection.java index aee0ff23af..4e7fb134ed 100644 --- a/src/main/java/net/rptools/clientserver/simple/connection/AbstractConnection.java +++ b/clientserver/src/main/java/net/rptools/clientserver/simple/connection/AbstractConnection.java @@ -16,13 +16,12 @@ import java.io.*; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; import java.util.List; -import java.util.Map; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import net.rptools.clientserver.ActivityListener; import net.rptools.clientserver.simple.DisconnectHandler; import net.rptools.clientserver.simple.MessageHandler; @@ -34,26 +33,26 @@ public abstract class AbstractConnection implements Connection { private static final Logger log = LogManager.getLogger(AbstractConnection.class); - private final Map> outQueueMap = new HashMap<>(); - private final List> outQueueList = new LinkedList<>(); + private final AtomicBoolean closed = new AtomicBoolean(false); + private final BlockingQueue outQueue = new LinkedBlockingQueue<>(); + private final List disconnectHandlers = new CopyOnWriteArrayList<>(); private final List listeners = new CopyOnWriteArrayList<>(); private final List messageHandlers = new CopyOnWriteArrayList<>(); - private List getOutQueue(Object channel) { - // Ordinarily I would synchronize this method, but I imagine the channels will be initialized - // once - // at the beginning of execution. Thus get(channel) will only return once right at the - // beginning - // no sense incurring the cost of synchronizing the method on the class for that. - List queue = outQueueMap.get(channel); - if (queue == null) { - queue = Collections.synchronizedList(new ArrayList()); - outQueueMap.put(channel, queue); + @Override + public final void close() { + if (closed.compareAndSet(false, true)) { + onClose(); } - return queue; } + protected final boolean isClosed() { + return closed.get(); + } + + protected abstract void onClose(); + private byte[] compress(byte[] message) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(message.length); @@ -79,30 +78,17 @@ private byte[] inflate(byte[] compressedMessage) { } } - protected synchronized void addMessage(Object channel, byte[] message) { - List queue = getOutQueue(channel); - queue.add(compress(message)); - // Queue up for sending - outQueueList.add(queue); - } - - protected synchronized boolean hasMoreMessages() { - return !outQueueList.isEmpty(); + protected void addMessage(Object channel, byte[] message) { + outQueue.add(compress(message)); } - protected synchronized byte[] nextMessage() { - if (!hasMoreMessages()) { + protected byte[] nextMessage() { + try { + // Bit paranoid, but don't wait forever for a message - that can perpetually block the thread. + return outQueue.poll(10, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { return null; } - List queue = outQueueList.remove(0); - - if (queue.isEmpty()) return null; - - byte[] message = queue.remove(0); - if (!queue.isEmpty()) { - outQueueList.add(queue); - } - return message; } public final void addMessageHandler(MessageHandler handler) { @@ -113,9 +99,10 @@ public final void removeMessageHandler(MessageHandler handler) { messageHandlers.remove(handler); } - private void dispatchMessage(String id, byte[] message) { - if (messageHandlers.size() == 0) { - log.warn("message received but not messageHandlers registered."); + protected void dispatchMessage(byte[] message) { + var id = getId(); + if (messageHandlers.isEmpty()) { + log.warn("message received but not messageHandlers registered for {}.", id); } for (MessageHandler handler : messageHandlers) { @@ -123,9 +110,9 @@ private void dispatchMessage(String id, byte[] message) { } } - protected final void dispatchCompressedMessage(String id, byte[] compressedMessage) { + protected final void dispatchCompressedMessage(byte[] compressedMessage) { var message = inflate(compressedMessage); - dispatchMessage(id, message); + dispatchMessage(message); } protected final void writeMessage(OutputStream out, byte[] message) throws IOException { @@ -239,7 +226,7 @@ public final void removeActivityListener(ActivityListener listener) { listeners.remove(listener); } - private void notifyListeners( + protected void notifyListeners( ActivityListener.Direction direction, ActivityListener.State state, int totalTransferSize, diff --git a/src/main/java/net/rptools/clientserver/simple/connection/Connection.java b/clientserver/src/main/java/net/rptools/clientserver/simple/connection/Connection.java similarity index 100% rename from src/main/java/net/rptools/clientserver/simple/connection/Connection.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/connection/Connection.java diff --git a/clientserver/src/main/java/net/rptools/clientserver/simple/connection/DirectConnection.java b/clientserver/src/main/java/net/rptools/clientserver/simple/connection/DirectConnection.java new file mode 100644 index 0000000000..5b589c46b8 --- /dev/null +++ b/clientserver/src/main/java/net/rptools/clientserver/simple/connection/DirectConnection.java @@ -0,0 +1,138 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code 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. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.clientserver.simple.connection; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class DirectConnection extends AbstractConnection { + private static final Logger log = LogManager.getLogger(DirectConnection.class); + + public record Pair(DirectConnection clientSide, DirectConnection serverSide) {} + + public static Pair create(String id) { + var closed = new AtomicBoolean(false); + var clientToServerQueue = new ArrayBlockingQueue(128); + var serverToClientQueue = new ArrayBlockingQueue(128); + + var clientSide = + new DirectConnection(closed, id + "-client", clientToServerQueue, serverToClientQueue); + var serverSide = + new DirectConnection(closed, id + "-server", serverToClientQueue, clientToServerQueue); + + return new Pair(clientSide, serverSide); + } + + private final AtomicBoolean sharedClosedFlag; + private final String id; + private final BlockingQueue writeQueue; + private final ReceiveThread receiveThread; + + private DirectConnection( + AtomicBoolean sharedClosedFlag, + String id, + BlockingQueue writeQueue, + BlockingQueue readQueue) { + this.sharedClosedFlag = sharedClosedFlag; + this.id = id; + this.writeQueue = writeQueue; + this.receiveThread = new ReceiveThread(readQueue); + } + + @Override + public void open() { + receiveThread.start(); + } + + @Override + protected void onClose() { + // Tell the other end about our closure. + sharedClosedFlag.set(true); + receiveThread.interrupt(); + } + + @Override + public void sendMessage(Object channel, byte[] message) { + if (message.length == 0) { + return; + } + + boolean written = false; + while (!written) { + try { + // Set a timeout so we have a chance to escape in case we weren't notified properly. + written = writeQueue.offer(message, 10, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // Just try again. + } + } + } + + @Override + public boolean isAlive() { + return !sharedClosedFlag.get(); + } + + @Override + public String getId() { + return id; + } + + @Override + public String getError() { + return null; + } + + private final class ReceiveThread extends Thread { + private final BlockingQueue readQueue; + + public ReceiveThread(BlockingQueue readQueue) { + super("DirectConnection.ReceiveThread"); + this.readQueue = readQueue; + } + + @Override + public void run() { + try { + while (!DirectConnection.this.isClosed() && DirectConnection.this.isAlive()) { + try { + // Set a timeout so we have a chance to escape in case we weren't notified properly. + byte[] message; + try { + message = readQueue.poll(10, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // Just try again. + continue; + } + + if (message != null) { + DirectConnection.this.dispatchMessage(message); + } + } catch (Throwable t) { + // don't let anything kill this thread via exception + log.error("Unexpected error in receive thread", t); + } + } + } finally { + DirectConnection.this.close(); + fireDisconnect(); + } + } + } +} diff --git a/src/main/java/net/rptools/clientserver/simple/connection/SocketConnection.java b/clientserver/src/main/java/net/rptools/clientserver/simple/connection/SocketConnection.java similarity index 51% rename from src/main/java/net/rptools/clientserver/simple/connection/SocketConnection.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/connection/SocketConnection.java index 418c8d7625..42124691c5 100644 --- a/src/main/java/net/rptools/clientserver/simple/connection/SocketConnection.java +++ b/clientserver/src/main/java/net/rptools/clientserver/simple/connection/SocketConnection.java @@ -16,13 +16,12 @@ import java.io.*; import java.net.Socket; +import java.net.SocketTimeoutException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * @author drice - *

TODO To change the template for this generated type comment go to Window - Preferences - - * Java - Code Style - Code Templates */ public class SocketConnection extends AbstractConnection implements Connection { /** Instance used for log messages. */ @@ -35,61 +34,56 @@ public class SocketConnection extends AbstractConnection implements Connection { private String hostName; private int port; - public SocketConnection(String id, String hostName, int port) throws IOException { + public SocketConnection(String id, String hostName, int port) { this.id = id; this.hostName = hostName; this.port = port; } - public SocketConnection(String id, Socket socket) throws IOException { + public SocketConnection(String id, Socket socket) { this.id = id; - this.hostName = socket.getInetAddress().getHostName(); - this.port = socket.getPort(); + this.socket = socket; + initialize(socket); } - private void initialize(Socket socket) throws IOException { + @Override + public String getId() { + return id; + } + + private void initialize(Socket socket) { this.socket = socket; - this.send = new SendThread(new BufferedOutputStream(socket.getOutputStream())); - this.receive = new ReceiveThread(this, socket.getInputStream()); + this.send = new SendThread(socket); + this.receive = new ReceiveThread(socket); + this.send.start(); this.receive.start(); } - public String getId() { - return id; - } - @Override public void open() throws IOException { initialize(new Socket(hostName, port)); } + @Override public void sendMessage(Object channel, byte[] message) { addMessage(channel, message); - synchronized (send) { - send.notify(); - } } - protected boolean isStopRequested() { - return send.stopRequested; - } - - public synchronized void close() { - if (isStopRequested()) { - return; - } - send.requestStop(); - receive.requestStop(); + @Override + protected void onClose() { + receive.interrupt(); + send.interrupt(); try { socket.close(); } catch (IOException e) { - log.warn(e.toString()); + log.warn("Failed to close socket", e); } } + @Override public boolean isAlive() { return !socket.isClosed(); } @@ -103,49 +97,41 @@ public String getError() { // send thread // ///////////////////////////////////////////////////////////////////////// private class SendThread extends Thread { - private final OutputStream out; - private boolean stopRequested = false; + private final Socket socket; - public SendThread(OutputStream out) { + public SendThread(Socket socket) { setName("SocketConnection.SendThread"); - this.out = out; - } - - public void requestStop() { - this.stopRequested = true; - synchronized (this) { - this.notify(); - } + this.socket = socket; } @Override public void run() { try { - while (!stopRequested && SocketConnection.this.isAlive()) { + final OutputStream out; + try { + out = new BufferedOutputStream(socket.getOutputStream()); + } catch (IOException e) { + log.error("Unable to get socket output stream", e); + return; + } + + while (!SocketConnection.this.isClosed() && SocketConnection.this.isAlive()) { + // Blocks for a time until a message is received. + byte[] message = SocketConnection.this.nextMessage(); + if (message == null) { + // No message available. Thread may also have been interrupted as part of stopping. + continue; + } + try { - while (SocketConnection.this.hasMoreMessages()) { - try { - byte[] message = SocketConnection.this.nextMessage(); - if (message == null) { - continue; - } - SocketConnection.this.writeMessage(out, message); - } catch (IndexOutOfBoundsException e) { - // just ignore and wait - } - } - synchronized (this) { - if (!stopRequested) { - this.wait(); - } - } - } catch (InterruptedException e) { - // do nothing + SocketConnection.this.writeMessage(out, message); + } catch (IOException e) { + log.error("Error while writing message. Closing connection.", e); + return; } } - } catch (IOException e) { - log.error(e); - fireDisconnect(); + } finally { + SocketConnection.this.close(); } } } @@ -154,35 +140,42 @@ public void run() { // receive thread // ///////////////////////////////////////////////////////////////////////// private class ReceiveThread extends Thread { - private final SocketConnection conn; - private final InputStream in; - private boolean stopRequested = false; + private final Socket socket; - public ReceiveThread(SocketConnection conn, InputStream in) { + public ReceiveThread(Socket socket) { setName("SocketConnection.ReceiveThread"); - this.conn = conn; - this.in = in; - } - - public void requestStop() { - stopRequested = true; + this.socket = socket; } @Override public void run() { - while (!stopRequested && conn.isAlive()) { + try { + final InputStream in; try { - byte[] message = conn.readMessage(in); - conn.dispatchCompressedMessage(conn.id, message); + in = socket.getInputStream(); } catch (IOException e) { - log.error(e); - fireDisconnect(); - break; - } catch (Throwable t) { - log.error(t); - // don't let anything kill this thread via exception - t.printStackTrace(); + log.error("Unable to get socket input stream", e); + return; } + + while (!SocketConnection.this.isClosed() && SocketConnection.this.isAlive()) { + try { + byte[] message = SocketConnection.this.readMessage(in); + SocketConnection.this.dispatchCompressedMessage(message); + } catch (SocketTimeoutException e) { + log.warn("Lost client {}", SocketConnection.this.getId(), e); + return; + } catch (IOException e) { + log.error(e); + return; + } catch (Throwable t) { + // don't let anything kill this thread via exception + log.error("Unexpected error", t); + } + } + } finally { + SocketConnection.this.close(); + fireDisconnect(); } } } diff --git a/src/main/java/net/rptools/clientserver/simple/connection/WebRTCConnection.java b/clientserver/src/main/java/net/rptools/clientserver/simple/connection/WebRTCConnection.java similarity index 56% rename from src/main/java/net/rptools/clientserver/simple/connection/WebRTCConnection.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/connection/WebRTCConnection.java index 3c71a080bb..124c6d732b 100644 --- a/src/main/java/net/rptools/clientserver/simple/connection/WebRTCConnection.java +++ b/clientserver/src/main/java/net/rptools/clientserver/simple/connection/WebRTCConnection.java @@ -20,23 +20,28 @@ import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; import net.rptools.clientserver.simple.server.WebRTCServer; import net.rptools.clientserver.simple.webrtc.*; -import net.rptools.maptool.client.MapTool; -import net.rptools.maptool.server.ServerConfig; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake; -public class WebRTCConnection extends AbstractConnection - implements Connection, PeerConnectionObserver, RTCDataChannelObserver { +public class WebRTCConnection extends AbstractConnection implements Connection { + public interface Listener { + void onLoginError(); + } + private static final Logger log = LogManager.getLogger(WebRTCConnection.class); + private final PeerConnectionObserver peerConnectionObserver = new PeerConnectionObserverImpl(); + private final RTCDataChannelObserver rtcDataChannelObserver = new RTCDataChannelObserverImpl(); private final PeerConnectionFactory factory = new PeerConnectionFactory(); - private final ServerConfig config; + private final String serverName; private final String id; private final Gson gson = new Gson(); + private final Listener listener; private WebSocketClient signalingClient; // only set on server side private WebRTCServer server; @@ -48,10 +53,13 @@ public class WebRTCConnection extends AbstractConnection private final SendThread sendThread = new SendThread(); private Thread handleDisconnect; + private final AtomicBoolean closed = new AtomicBoolean(false); + // used from client side - public WebRTCConnection(String id, ServerConfig config) { + public WebRTCConnection(String id, String serverName, Listener listener) { this.id = id; - this.config = config; + this.serverName = serverName; + this.listener = listener; init(); } @@ -59,11 +67,12 @@ public WebRTCConnection(String id, ServerConfig config) { public WebRTCConnection(OfferMessageDto message, WebRTCServer webRTCServer) { this.id = message.source; this.server = webRTCServer; - this.config = server.getConfig(); + this.serverName = server.getName(); + this.listener = () -> {}; this.signalingClient = server.getSignalingClient(); init(); - peerConnection = factory.createPeerConnection(rtcConfig, this); + peerConnection = factory.createPeerConnection(rtcConfig, peerConnectionObserver); peerConnection.setRemoteDescription( message.offer, new SetSessionDescriptionObserver() { @@ -90,7 +99,7 @@ public void onSuccess(RTCSessionDescription description) { @Override public void onSuccess() { var msg = new AnswerMessageDto(); - msg.source = server.getConfig().getServerName(); + msg.source = serverName; msg.destination = getId(); msg.answer = description; sendSignalingMessage(gson.toJson(msg)); @@ -116,9 +125,11 @@ private boolean isServerSide() { private String getSource() { // on server side the id is already user@server - if (isServerSide()) return getId(); + if (isServerSide()) { + return getId(); + } - return getId() + "@" + config.getServerName(); + return getId() + "@" + serverName; } private void startSignaling() { @@ -147,7 +158,9 @@ public void onMessage(String message) { public void onClose(int code, String reason, boolean remote) { lastError = "WebSocket closed: (" + code + ") " + reason; log.info(prefix() + lastError); - if (!isAlive()) fireDisconnectAsync(); + if (!isAlive()) { + fireDisconnectAsync(); + } } @Override @@ -198,17 +211,13 @@ private void init() { public void sendMessage(Object channel, byte[] message) { log.debug(prefix() + "added message"); addMessage(channel, message); - if (peerConnection != null - && peerConnection.getConnectionState() == RTCPeerConnectionState.CONNECTED) { - synchronized (sendThread) { - sendThread.notify(); - } - } } @Override public boolean isAlive() { - if (peerConnection == null) return false; + if (peerConnection == null) { + return false; + } return switch (peerConnection.getConnectionState()) { case CONNECTED, DISCONNECTED -> true; @@ -269,15 +278,15 @@ public void onFailure(String error) { private void onLogin(LoginMessageDto message) { if (!message.success) { - MapTool.showError("Handshake.msg.playerAlreadyConnected"); + listener.onLoginError(); return; } - peerConnection = factory.createPeerConnection(rtcConfig, this); + peerConnection = factory.createPeerConnection(rtcConfig, peerConnectionObserver); var initDict = new RTCDataChannelInit(); localDataChannel = peerConnection.createDataChannel("myDataChannel", initDict); - localDataChannel.registerObserver(this); + localDataChannel.registerObserver(rtcDataChannelObserver); var offerOptions = new RTCOfferOptions(); peerConnection.createOffer( @@ -293,7 +302,7 @@ public void onSuccess() { var msg = new OfferMessageDto(); msg.offer = description; msg.source = getSource(); - msg.destination = config.getServerName(); + msg.destination = serverName; sendSignalingMessage(gson.toJson(msg)); } @@ -315,274 +324,253 @@ private String prefix() { return isServerSide() ? "S " : "C "; } - @Override - public void onSignalingChange(RTCSignalingState state) { - // set thread name for better logs. - Thread.currentThread().setName("WebRTCConnection.WebRTCThread_" + getId()); - log.info(prefix() + "PeerConnection.onSignalingChange: " + state); + public void addIceCandidate(RTCIceCandidate candidate) { + log.info(prefix() + "PeerConnection.addIceCandidate: " + candidate.toString()); + peerConnection.addIceCandidate(candidate); } - @Override - public void onConnectionChange(RTCPeerConnectionState state) { - log.info(prefix() + "PeerConnection.onConnectionChange " + state); - switch (state) { - case FAILED -> { - lastError = "PeerConnection failed"; - peerConnection = null; - fireDisconnectAsync(); - } - case CONNECTED -> { - if (hasMoreMessages()) { - synchronized (sendThread) { - sendThread.notify(); - } - } - } - } + private void fireDisconnectAsync() { + handleDisconnect = + new Thread( + () -> { + fireDisconnect(); + }, + "WebRTCConnection.handleDisconnect"); + handleDisconnect.start(); } @Override - public void onIceConnectionChange(RTCIceConnectionState state) { - log.info(prefix() + "PeerConnection.onIceConnectionChange " + state); - } + protected void onClose() { + // signalingClient should be closed if connection was established + if (!isServerSide() && signalingClient.isOpen()) { + signalingClient.close(); + } - @Override - public void onStandardizedIceConnectionChange(RTCIceConnectionState state) { - log.info(prefix() + "PeerConnection.onStandardizedIceConnectionChange " + state); + sendThread.interrupt(); + if (peerConnection != null) { + peerConnection.close(); + peerConnection = null; + } } @Override - public void onIceConnectionReceivingChange(boolean receiving) { - log.info(prefix() + "PeerConnection.onIceConnectionReceivingChange " + receiving); + public String getError() { + return lastError; } - @Override - public void onIceGatheringChange(RTCIceGatheringState state) { - log.info(prefix() + "PeerConnection.onIceGatheringChange " + state); - } + private class SendThread extends Thread { + public SendThread() { + super("WebRTCConnection.SendThread_" + WebRTCConnection.this.getId()); + } - @Override - public void onIceCandidate(RTCIceCandidate candidate) { - var msg = new CandidateMessageDto(); + @Override + public void run() { + log.debug(prefix() + " sendThread started"); - if (isServerSide()) { - msg.source = config.getServerName(); - msg.destination = getSource(); - } else { - msg.destination = config.getServerName(); - msg.source = getSource(); - } - msg.candidate = candidate; - sendSignalingMessage(gson.toJson(msg)); - } + while (!WebRTCConnection.this.isClosed() && WebRTCConnection.this.isAlive()) { + // Blocks for a time until a message is received. + byte[] message = WebRTCConnection.this.nextMessage(); + if (message == null) { + // No message available. Thread may also have been interrupted as part of stopping. + continue; + } - @Override - public void onIceCandidateError(RTCPeerConnectionIceErrorEvent event) { - log.debug( - prefix() - + "PeerConnection.onIceCandidateError: code:" - + event.getErrorCode() - + " url: " - + event.getUrl() - + " address/port: " - + event.getAddress() - + ":" - + event.getPort() - + " text: " - + event.getErrorText()); - } + ByteBuffer buffer = ByteBuffer.allocate(message.length + Integer.BYTES); + buffer.putInt(message.length).put(message).rewind(); - @Override - public void onIceCandidatesRemoved(RTCIceCandidate[] candidates) { - log.info(prefix() + "PeerConnection.onIceCandidatesRemoved"); - } + int chunkSize = 16 * 1024; - @Override - public void onAddStream(MediaStream stream) { - log.info(prefix() + "PeerConnection.onAddStream"); - } + while (buffer.remaining() > 0) { + var amountToSend = Math.min(buffer.remaining(), chunkSize); + ByteBuffer part = buffer; - @Override - public void onRemoveStream(MediaStream stream) { - log.info(prefix() + "PeerConnection.onRemoveStream"); - } + if (amountToSend != buffer.capacity()) { + // we need to allocation a new ByteBuffer because send calls ByteBuffer.array() + // which would return + // the whole byte[] and not only the slice. But the lib doesn't use + // ByteBuffer.arrayOffset(). + var slice = buffer.slice(buffer.position(), amountToSend); + part = ByteBuffer.allocate(amountToSend); + part.put(slice); + } - @Override - public void onDataChannel(RTCDataChannel newDataChannel) { - log.info(prefix() + "PeerConnection.onDataChannel"); - this.localDataChannel = newDataChannel; - localDataChannel.registerObserver(this); + buffer.position(buffer.position() + amountToSend); + try { + localDataChannel.send(new RTCDataChannelBuffer(part, true)); + } catch (Exception e) { + log.error(prefix() + e); + fireDisconnect(); + return; + } + log.debug(prefix() + " sent " + part.capacity() + " bytes"); + } + } - if (isServerSide()) { - server.onDataChannelOpened(this); + log.debug(prefix() + " sendThread ended"); } } - @Override - public void onRenegotiationNeeded() { - // set thread name for better logs - Thread.currentThread().setName("WebRTCConnection.WebRTCThread_" + getId()); - log.info(prefix() + "PeerConnection.onRenegotiationNeeded"); - } - - @Override - public void onAddTrack(RTCRtpReceiver receiver, MediaStream[] mediaStreams) { - log.info(prefix() + "PeerConnection.onTrack(multiple Streams)"); - } + private final class PeerConnectionObserverImpl implements PeerConnectionObserver { + @Override + public void onIceCandidate(RTCIceCandidate candidate) { + var msg = new CandidateMessageDto(); + + if (isServerSide()) { + msg.source = serverName; + msg.destination = getSource(); + } else { + msg.destination = serverName; + msg.source = getSource(); + } + msg.candidate = candidate; + sendSignalingMessage(gson.toJson(msg)); + } - @Override - public void onRemoveTrack(RTCRtpReceiver receiver) { - log.info(prefix() + "PeerConnection.onRemoveTrack"); - } + @Override + public void onAddStream(MediaStream stream) { + log.info(prefix() + "PeerConnection.onAddStream"); + } - @Override - public void onTrack(RTCRtpTransceiver transceiver) { - log.info(prefix() + "PeerConnection.onTrack"); - } + @Override + public void onAddTrack(RTCRtpReceiver receiver, MediaStream[] mediaStreams) { + log.info(prefix() + "PeerConnection.onTrack(multiple Streams)"); + } - public void addIceCandidate(RTCIceCandidate candidate) { - log.info(prefix() + "PeerConnection.addIceCandidate: " + candidate.toString()); - peerConnection.addIceCandidate(candidate); - } + @Override + public void onConnectionChange(RTCPeerConnectionState state) { + log.info(prefix() + "PeerConnection.onConnectionChange " + state); + switch (state) { + case FAILED -> { + lastError = "PeerConnection failed"; + peerConnection = null; + fireDisconnectAsync(); + } + } + } - // dataChannel - @Override - public void onBufferedAmountChange(long previousAmount) { - log.info(prefix() + "dataChannel onBufferedAmountChange " + previousAmount); - } + @Override + public void onDataChannel(RTCDataChannel newDataChannel) { + log.info(prefix() + "PeerConnection.onDataChannel"); + localDataChannel = newDataChannel; + localDataChannel.registerObserver(rtcDataChannelObserver); - // dataChannel - @Override - public void onStateChange() { - var state = localDataChannel.getState(); - log.info(prefix() + "localDataChannel onStateChange " + state); - switch (state) { - case OPEN -> { - // connection established we don't need the signaling server anymore - // for now disabled. We may get additional ice candidates. - if (!isServerSide() && signalingClient.isOpen()) signalingClient.close(); - - sendThread.start(); - } - case CLOSED -> { - close(); - fireDisconnectAsync(); + if (isServerSide()) { + server.onDataChannelOpened(WebRTCConnection.this); } } - } - // dataChannel - @Override - public void onMessage(RTCDataChannelBuffer channelBuffer) { - log.debug( - prefix() + "localDataChannel onMessage: got " + channelBuffer.data.capacity() + " bytes"); + @Override + public void onIceCandidateError(RTCPeerConnectionIceErrorEvent event) { + log.debug( + prefix() + + "PeerConnection.onIceCandidateError: code:" + + event.getErrorCode() + + " url: " + + event.getUrl() + + " address/port: " + + event.getAddress() + + ":" + + event.getPort() + + " text: " + + event.getErrorText()); + } - if (Thread.currentThread().getContextClassLoader() == null) { - ClassLoader cl = ClassLoader.getSystemClassLoader(); - Thread.currentThread().setContextClassLoader(cl); + @Override + public void onIceCandidatesRemoved(RTCIceCandidate[] candidates) { + log.info(prefix() + "PeerConnection.onIceCandidatesRemoved"); } - var message = readMessage(channelBuffer.data); - if (message != null) dispatchCompressedMessage(id, message); - } + @Override + public void onIceConnectionChange(RTCIceConnectionState state) { + log.info(prefix() + "PeerConnection.onIceConnectionChange " + state); + } - private void fireDisconnectAsync() { - handleDisconnect = - new Thread( - () -> { - fireDisconnect(); - if (isServerSide()) server.clearClients(); - }, - "WebRTCConnection.handleDisconnect"); - handleDisconnect.start(); - } + @Override + public void onIceConnectionReceivingChange(boolean receiving) { + log.info(prefix() + "PeerConnection.onIceConnectionReceivingChange " + receiving); + } - @Override - public void close() { - // signalingClient should be closed if connection was established - if (!isServerSide() && signalingClient.isOpen()) signalingClient.close(); + @Override + public void onIceGatheringChange(RTCIceGatheringState state) { + log.info(prefix() + "PeerConnection.onIceGatheringChange " + state); + } - if (sendThread.stopRequested) return; + @Override + public void onRemoveStream(MediaStream stream) { + log.info(prefix() + "PeerConnection.onRemoveStream"); + } - sendThread.requestStop(); - if (peerConnection != null) { - peerConnection.close(); - peerConnection = null; + @Override + public void onRemoveTrack(RTCRtpReceiver receiver) { + log.info(prefix() + "PeerConnection.onRemoveTrack"); } - } - @Override - public String getError() { - return lastError; - } + @Override + public void onRenegotiationNeeded() { + // set thread name for better logs + Thread.currentThread().setName("WebRTCConnection.WebRTCThread_" + getId()); + log.info(prefix() + "PeerConnection.onRenegotiationNeeded"); + } - private class SendThread extends Thread { - private boolean stopRequested = false; + @Override + public void onSignalingChange(RTCSignalingState state) { + // set thread name for better logs. + Thread.currentThread().setName("WebRTCConnection.WebRTCThread_" + getId()); + log.info(prefix() + "PeerConnection.onSignalingChange: " + state); + } - public SendThread() { - super("WebRTCConnection.SendThread_" + WebRTCConnection.this.getId()); + @Override + public void onStandardizedIceConnectionChange(RTCIceConnectionState state) { + log.info(prefix() + "PeerConnection.onStandardizedIceConnectionChange " + state); } - public void requestStop() { - this.stopRequested = true; - synchronized (this) { - this.notify(); - } + @Override + public void onTrack(RTCRtpTransceiver transceiver) { + log.info(prefix() + "PeerConnection.onTrack"); } + } + private final class RTCDataChannelObserverImpl implements RTCDataChannelObserver { @Override - public void run() { - log.debug(prefix() + " sendThread started"); - try { - while (!stopRequested && WebRTCConnection.this.isAlive()) { - while (WebRTCConnection.this.hasMoreMessages() - && peerConnection.getConnectionState() == RTCPeerConnectionState.CONNECTED) { - byte[] message = WebRTCConnection.this.nextMessage(); - if (message == null) { - continue; - } + public void onBufferedAmountChange(long previousAmount) { + log.info(prefix() + "dataChannel onBufferedAmountChange " + previousAmount); + } - ByteBuffer buffer = ByteBuffer.allocate(message.length + Integer.BYTES); - buffer.putInt(message.length).put(message).rewind(); + @Override + public void onStateChange() { + var state = localDataChannel.getState(); + log.info(prefix() + "localDataChannel onStateChange " + state); + switch (state) { + case OPEN -> { + // connection established we don't need the signaling server anymore + // for now disabled. We may get additional ice candidates. + if (!isServerSide() && signalingClient.isOpen()) { + signalingClient.close(); + } - int chunkSize = 16 * 1024; + sendThread.start(); + } + case CLOSED -> { + close(); + fireDisconnectAsync(); + } + } + } - while (buffer.remaining() > 0) { - var amountToSend = buffer.remaining() <= chunkSize ? buffer.remaining() : chunkSize; - ByteBuffer part = buffer; + @Override + public void onMessage(RTCDataChannelBuffer channelBuffer) { + log.debug( + prefix() + "localDataChannel onMessage: got " + channelBuffer.data.capacity() + " bytes"); - if (amountToSend != buffer.capacity()) { - // we need to allocation a new ByteBuffer because send calls ByteBuffer.array() - // which would return - // the whole byte[] and not only the slice. But the lib doesn't use - // ByteBuffer.arrayOffset(). - var slice = buffer.slice(buffer.position(), amountToSend); - part = ByteBuffer.allocate(amountToSend); - part.put(slice); - } + if (Thread.currentThread().getContextClassLoader() == null) { + ClassLoader cl = ClassLoader.getSystemClassLoader(); + Thread.currentThread().setContextClassLoader(cl); + } - buffer.position(buffer.position() + amountToSend); - localDataChannel.send(new RTCDataChannelBuffer(part, true)); - log.debug(prefix() + " sent " + part.capacity() + " bytes"); - } - } - synchronized (this) { - if (!stopRequested) { - try { - log.debug(prefix() + "sendThread -> sleep"); - this.wait(); - log.debug(prefix() + "sendThread -> woke up"); - } catch (InterruptedException e) { - log.debug(prefix() + "sendThread -> interrupted"); - } - } - } - } - } catch (Exception e) { - log.error(prefix() + e); - fireDisconnect(); + var message = readMessage(channelBuffer.data); + if (message != null) { + dispatchCompressedMessage(message); } - log.debug(prefix() + " sendThread ended"); } } } diff --git a/clientserver/src/main/java/net/rptools/clientserver/simple/server/AbstractServer.java b/clientserver/src/main/java/net/rptools/clientserver/simple/server/AbstractServer.java new file mode 100644 index 0000000000..7186d6c0ef --- /dev/null +++ b/clientserver/src/main/java/net/rptools/clientserver/simple/server/AbstractServer.java @@ -0,0 +1,45 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code 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. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.clientserver.simple.server; + +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; +import net.rptools.clientserver.simple.connection.Connection; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public abstract class AbstractServer implements Server { + + private static final Logger log = LogManager.getLogger(AbstractServer.class); + + private final List observerList = new CopyOnWriteArrayList<>(); + + public AbstractServer() {} + + public void addObserver(ServerObserver observer) { + observerList.add(observer); + } + + public void removeObserver(ServerObserver observer) { + observerList.remove(observer); + } + + protected void fireClientConnect(Connection conn) { + log.debug("Firing: clientConnect: {}", conn.getId()); + for (ServerObserver observer : observerList) { + observer.connectionAdded(conn); + } + } +} diff --git a/src/main/java/net/rptools/clientserver/simple/server/HandshakeProvider.java b/clientserver/src/main/java/net/rptools/clientserver/simple/server/NilServer.java similarity index 73% rename from src/main/java/net/rptools/clientserver/simple/server/HandshakeProvider.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/server/NilServer.java index e9444833fb..ad9b76bec3 100644 --- a/src/main/java/net/rptools/clientserver/simple/server/HandshakeProvider.java +++ b/clientserver/src/main/java/net/rptools/clientserver/simple/server/NilServer.java @@ -14,11 +14,11 @@ */ package net.rptools.clientserver.simple.server; -import net.rptools.clientserver.simple.connection.Connection; -import net.rptools.maptool.server.Handshake; +/** A server implementation that never receives connections */ +public class NilServer extends AbstractServer { + @Override + public void start() {} -public interface HandshakeProvider { - Handshake getConnectionHandshake(Connection conn); - - void releaseHandshake(Connection conn); + @Override + public void close() {} } diff --git a/clientserver/src/main/java/net/rptools/clientserver/simple/server/Router.java b/clientserver/src/main/java/net/rptools/clientserver/simple/server/Router.java new file mode 100644 index 0000000000..699aa86a08 --- /dev/null +++ b/clientserver/src/main/java/net/rptools/clientserver/simple/server/Router.java @@ -0,0 +1,120 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code 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. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.clientserver.simple.server; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import javax.annotation.Nullable; +import net.rptools.clientserver.simple.connection.Connection; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class Router { + private static final Logger log = LogManager.getLogger(Router.class); + + private final Map clients = Collections.synchronizedMap(new HashMap<>()); + + public Router() {} + + public Collection removeAll() { + ArrayList copy; + synchronized (clients) { + copy = new ArrayList<>(clients.values()); + clients.clear(); + } + return copy; + } + + public @Nullable Connection getConnection(String id) { + return this.clients.get(id); + } + + public void addConnection(Connection connection) { + var existingConnection = this.clients.putIfAbsent(connection.getId(), connection); + if (existingConnection != null) { + log.error( + "Failed to add connection {} because we already have a connection with that ID", + connection.getId()); + } + } + + public void removeConnection(Connection connection) { + var removed = this.clients.remove(connection.getId(), connection); + if (!removed) { + log.error( + "Failed to remove connection {} because we do not have that connection", + connection.getId()); + } + } + + public void broadcastMessage(byte[] message) { + synchronized (clients) { + for (Connection conn : clients.values()) { + conn.sendMessage(message); + } + } + } + + public void broadcastMessage(String[] exclude, byte[] message) { + // Note: although we except an exclude array, reality is that it only has one element at most. + // So don't bother setting up a hash set or anything, just loop to check if in the array. + synchronized (clients) { + for (Map.Entry entry : clients.entrySet()) { + if (!Arrays.asList(exclude).contains(entry.getKey())) { + entry.getValue().sendMessage(message); + } + } + } + } + + public void sendMessage(String id, byte[] message) { + sendMessage(id, null, message); + } + + public void sendMessage(String id, Object channel, byte[] message) { + synchronized (clients) { + var connection = clients.get(id); + if (connection != null) { + connection.sendMessage(channel, message); + } + } + } + + /** Looks for any clients that have disconnected, and removes them. */ + public Collection reapClients() { + log.debug("About to reap clients"); + var result = new ArrayList(); + synchronized (clients) { + log.debug("Reaping clients"); + + for (Iterator> i = clients.entrySet().iterator(); + i.hasNext(); ) { + Map.Entry entry = i.next(); + Connection conn = entry.getValue(); + if (!conn.isAlive()) { + log.debug("\tReaping: {}", conn.getId()); + result.add(conn); + i.remove(); + } + } + } + return result; + } +} diff --git a/src/main/java/net/rptools/clientserver/simple/server/Server.java b/clientserver/src/main/java/net/rptools/clientserver/simple/server/Server.java similarity index 77% rename from src/main/java/net/rptools/clientserver/simple/server/Server.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/server/Server.java index 171e8a57e6..00b379a7d9 100644 --- a/src/main/java/net/rptools/clientserver/simple/server/Server.java +++ b/clientserver/src/main/java/net/rptools/clientserver/simple/server/Server.java @@ -24,14 +24,4 @@ public interface Server extends AutoCloseable { void addObserver(ServerObserver observer); void removeObserver(ServerObserver observer); - - void broadcastMessage(byte[] message); - - void broadcastMessage(String[] exclude, byte[] message); - - default void sendMessage(String id, byte[] message) { - sendMessage(id, null, message); - } - - void sendMessage(String id, Object channel, byte[] message); } diff --git a/src/main/java/net/rptools/clientserver/simple/server/ServerObserver.java b/clientserver/src/main/java/net/rptools/clientserver/simple/server/ServerObserver.java similarity index 94% rename from src/main/java/net/rptools/clientserver/simple/server/ServerObserver.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/server/ServerObserver.java index 7ea52d9cc4..7dcdcdee8c 100644 --- a/src/main/java/net/rptools/clientserver/simple/server/ServerObserver.java +++ b/clientserver/src/main/java/net/rptools/clientserver/simple/server/ServerObserver.java @@ -20,6 +20,4 @@ public interface ServerObserver { public void connectionAdded(Connection conn); - - public void connectionRemoved(Connection conn); } diff --git a/src/main/java/net/rptools/clientserver/simple/server/SocketServer.java b/clientserver/src/main/java/net/rptools/clientserver/simple/server/SocketServer.java similarity index 84% rename from src/main/java/net/rptools/clientserver/simple/server/SocketServer.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/server/SocketServer.java index 633a61ebb3..2b4c0ac9c5 100644 --- a/src/main/java/net/rptools/clientserver/simple/server/SocketServer.java +++ b/clientserver/src/main/java/net/rptools/clientserver/simple/server/SocketServer.java @@ -17,8 +17,7 @@ import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; -import java.util.concurrent.ExecutionException; -import net.rptools.clientserver.simple.MessageHandler; +import java.util.concurrent.TimeUnit; import net.rptools.clientserver.simple.connection.SocketConnection; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -33,21 +32,22 @@ public class SocketServer extends AbstractServer { private ServerSocket socket; private ListeningThread listeningThread; - public SocketServer(int port, HandshakeProvider handshake, MessageHandler messageHandler) { - super(handshake, messageHandler); + public SocketServer(int port) { this.port = port; } @Override public void start() throws IOException { - socket = new ServerSocket(port); + var serverSocket = new ServerSocket(port); + // If the above throws, it will be as though we never started. + + socket = serverSocket; listeningThread = new ListeningThread(this, socket); listeningThread.start(); } @Override public void close() { - super.close(); listeningThread.suppressErrors(); log.debug("Server closing down"); @@ -104,12 +104,15 @@ public void run() { while (!stopRequested) { try { Socket s = socket.accept(); + // Client heartbeat frequency is 20 seconds, so a minute should permit two or three + // heartbeats to come in if still connected. + s.setSoTimeout((int) TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES)); log.debug("Client connecting ..."); String id = nextClientId(s); SocketConnection conn = new SocketConnection(id, s); - server.handleConnection(conn); - } catch (IOException | ExecutionException | InterruptedException e) { + server.fireClientConnect(conn); + } catch (IOException e) { if (!suppressErrors) { log.error(e.getMessage(), e); } diff --git a/src/main/java/net/rptools/clientserver/simple/server/WebRTCServer.java b/clientserver/src/main/java/net/rptools/clientserver/simple/server/WebRTCServer.java similarity index 87% rename from src/main/java/net/rptools/clientserver/simple/server/WebRTCServer.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/server/WebRTCServer.java index 5954661f99..32ad0b8c7b 100644 --- a/src/main/java/net/rptools/clientserver/simple/server/WebRTCServer.java +++ b/clientserver/src/main/java/net/rptools/clientserver/simple/server/WebRTCServer.java @@ -19,14 +19,11 @@ import java.net.URI; import java.util.HashMap; import java.util.Map; -import net.rptools.clientserver.simple.MessageHandler; import net.rptools.clientserver.simple.connection.WebRTCConnection; import net.rptools.clientserver.simple.webrtc.CandidateMessageDto; import net.rptools.clientserver.simple.webrtc.LoginMessageDto; import net.rptools.clientserver.simple.webrtc.MessageDto; import net.rptools.clientserver.simple.webrtc.OfferMessageDto; -import net.rptools.maptool.client.MapTool; -import net.rptools.maptool.server.ServerConfig; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.java_websocket.client.WebSocketClient; @@ -35,8 +32,15 @@ public class WebRTCServer extends AbstractServer { private static final Logger log = LogManager.getLogger(WebRTCServer.class); + public interface Listener { + void onLoginError(); + + void onUnexpectedClose(); + } + + private final Listener listener; private WebSocketClient signalingClient; - private final ServerConfig config; + private final String serverName; private final Gson gson = new Gson(); private String lastError = null; private URI webSocketUri = null; @@ -47,10 +51,9 @@ public class WebRTCServer extends AbstractServer { public static String WebSocketUrl = "ws://webrtc1.rptools.net:8080"; private boolean disconnectExpected; - public WebRTCServer( - ServerConfig config, HandshakeProvider handshake, MessageHandler messageHandler) { - super(handshake, messageHandler); - this.config = config; + public WebRTCServer(String serverName, Listener listener) { + this.listener = listener; + this.serverName = serverName; try { webSocketUri = new URI(WebSocketUrl); @@ -72,7 +75,7 @@ public void onOpen(ServerHandshake handshakeData) { reconnectCounter = 30; log.info("S WebSocket connected\n"); var msg = new LoginMessageDto(); - msg.source = config.getServerName(); + msg.source = serverName; sendSignalingMessage(gson.toJson(msg)); } @@ -86,12 +89,17 @@ public void onMessage(String message) { public void onClose(int code, String reason, boolean remote) { lastError = "WebSocket closed: remote:" + remote + " (" + code + ") " + reason; log.info("S " + lastError); - if (disconnectExpected) return; + if (disconnectExpected) { + return; + } // if the connection get closed remotely the rptools.net server disconnected. Try to // reconnect. - if (reconnectCounter > 0) retryConnect(); - else MapTool.stopServer(); + if (reconnectCounter > 0) { + retryConnect(); + } else { + listener.onUnexpectedClose(); + } } @Override @@ -111,7 +119,7 @@ private void handleSignalingMessage(String message) { case "login" -> { var loginMsg = gson.fromJson(message, LoginMessageDto.class); if (!loginMsg.success) { - MapTool.showError("ServerDialog.error.serverAlreadyExists"); + listener.onLoginError(); } } case "offer" -> { @@ -131,13 +139,13 @@ public WebSocketClient getSignalingClient() { return signalingClient; } - public ServerConfig getConfig() { - return config; + public String getName() { + return serverName; } public void onDataChannelOpened(WebRTCConnection connection) { try { - handleConnection(connection); + fireClientConnect(connection); } catch (Exception e) { log.error(e); } @@ -154,7 +162,6 @@ public void start() throws IOException { @Override public void close() { - super.close(); disconnectExpected = true; reconnectCounter = -1; signalingClient.close(); @@ -179,8 +186,4 @@ void retryConnect() { reconnectThread.setName("WebRTCServer.reconnectThread"); reconnectThread.start(); } - - public void clearClients() { - reapClients(); - } } diff --git a/src/main/java/net/rptools/clientserver/simple/webrtc/AnswerMessageDto.java b/clientserver/src/main/java/net/rptools/clientserver/simple/webrtc/AnswerMessageDto.java similarity index 100% rename from src/main/java/net/rptools/clientserver/simple/webrtc/AnswerMessageDto.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/webrtc/AnswerMessageDto.java diff --git a/src/main/java/net/rptools/clientserver/simple/webrtc/CandidateMessageDto.java b/clientserver/src/main/java/net/rptools/clientserver/simple/webrtc/CandidateMessageDto.java similarity index 100% rename from src/main/java/net/rptools/clientserver/simple/webrtc/CandidateMessageDto.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/webrtc/CandidateMessageDto.java diff --git a/src/main/java/net/rptools/clientserver/simple/webrtc/LoginMessageDto.java b/clientserver/src/main/java/net/rptools/clientserver/simple/webrtc/LoginMessageDto.java similarity index 100% rename from src/main/java/net/rptools/clientserver/simple/webrtc/LoginMessageDto.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/webrtc/LoginMessageDto.java diff --git a/src/main/java/net/rptools/clientserver/simple/webrtc/MessageDto.java b/clientserver/src/main/java/net/rptools/clientserver/simple/webrtc/MessageDto.java similarity index 100% rename from src/main/java/net/rptools/clientserver/simple/webrtc/MessageDto.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/webrtc/MessageDto.java diff --git a/src/main/java/net/rptools/clientserver/simple/webrtc/OfferMessageDto.java b/clientserver/src/main/java/net/rptools/clientserver/simple/webrtc/OfferMessageDto.java similarity index 100% rename from src/main/java/net/rptools/clientserver/simple/webrtc/OfferMessageDto.java rename to clientserver/src/main/java/net/rptools/clientserver/simple/webrtc/OfferMessageDto.java diff --git a/clientserver/src/test/java/net/rptools/clientserver/simple/server/RouterTest.java b/clientserver/src/test/java/net/rptools/clientserver/simple/server/RouterTest.java new file mode 100644 index 0000000000..835360002d --- /dev/null +++ b/clientserver/src/test/java/net/rptools/clientserver/simple/server/RouterTest.java @@ -0,0 +1,177 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code 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. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.clientserver.simple.server; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; +import net.rptools.clientserver.simple.connection.Connection; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class RouterTest { + private final List mockConnections = new ArrayList<>(); + private Router router; + + @BeforeEach + public void setUp() { + router = new Router(); + + for (int i = 0; i < 3; ++i) { + var connection = mock(Connection.class); + mockConnections.add(connection); + when(connection.getId()).thenReturn("connection-" + i); + when(connection.isAlive()).thenReturn(true); + + router.addConnection(connection); + } + } + + @Test + public void testGetConnection() { + var connection = router.getConnection("connection-2"); + + assertSame(mockConnections.get(2), connection); + } + + @Test + public void testSendMessage() { + var message = new byte[] {0, 1, 2, 3, 4}; + + router.sendMessage("connection-1", message); + + verify(mockConnections.get(0), never()).sendMessage(any(), any()); + verify(mockConnections.get(1)).sendMessage(null, message); + verify(mockConnections.get(2), never()).sendMessage(any(), any()); + } + + @Test + public void testSendMessageWithChannel() { + var channel = "channel"; + var message = new byte[] {0, 1, 2, 3, 4}; + + router.sendMessage("connection-1", channel, message); + + verify(mockConnections.get(0), never()).sendMessage(any(), any()); + verify(mockConnections.get(1)).sendMessage(channel, message); + verify(mockConnections.get(2), never()).sendMessage(any(), any()); + } + + @Test + public void testSendToUnknown() { + var message = new byte[] {0, 1, 2, 3, 4}; + + router.sendMessage("unknown", message); + + verify(mockConnections.get(0), never()).sendMessage(any()); + verify(mockConnections.get(1), never()).sendMessage(any()); + verify(mockConnections.get(2), never()).sendMessage(any()); + } + + @Test + public void testSimpleBroadcast() { + var message = new byte[] {0, 1, 2, 3, 4}; + + router.broadcastMessage(message); + + verify(mockConnections.get(0)).sendMessage(message); + verify(mockConnections.get(1)).sendMessage(message); + verify(mockConnections.get(2)).sendMessage(message); + } + + @Test + public void testBroadcastWithExclude() { + var message = new byte[] {0, 1, 2, 3, 4}; + + router.broadcastMessage(new String[] {mockConnections.get(1).getId()}, message); + + verify(mockConnections.get(0)).sendMessage(message); + verify(mockConnections.get(1), never()).sendMessage(any()); + verify(mockConnections.get(2)).sendMessage(message); + } + + @Test + public void testRedundantAddConnection() { + var newConnection = mock(Connection.class); + when(newConnection.getId()).thenReturn("connection-1"); + when(newConnection.isAlive()).thenReturn(true); + + router.addConnection(newConnection); + + var message = new byte[] {0, 1, 2, 3, 4}; + router.broadcastMessage(message); + verify(mockConnections.get(0)).sendMessage(message); + verify(mockConnections.get(1)).sendMessage(message); + verify(mockConnections.get(2)).sendMessage(message); + verify(newConnection, never()).sendMessage(any()); + } + + @Test + public void testRemoveConnection() { + router.removeConnection(mockConnections.get(1)); + + var message = new byte[] {0, 1, 2, 3, 4}; + router.broadcastMessage(message); + verify(mockConnections.get(0)).sendMessage(message); + verify(mockConnections.get(1), never()).sendMessage(any()); + verify(mockConnections.get(2)).sendMessage(message); + } + + @Test + public void testRedundantRemoveConnection() { + router.removeConnection(mockConnections.get(1)); + router.removeConnection(mockConnections.get(1)); + + var message = new byte[] {0, 1, 2, 3, 4}; + router.broadcastMessage(message); + verify(mockConnections.get(0)).sendMessage(message); + verify(mockConnections.get(1), never()).sendMessage(any()); + verify(mockConnections.get(2)).sendMessage(message); + } + + @Test + public void testReapClients() { + when(mockConnections.get(0).isAlive()).thenReturn(false); + when(mockConnections.get(2).isAlive()).thenReturn(false); + var message = new byte[] {0, 1, 2, 3, 4}; + + var reaped = router.reapClients(); + assert reaped.contains(mockConnections.get(0)); + assert !reaped.contains(mockConnections.get(1)); + assert reaped.contains(mockConnections.get(2)); + + router.broadcastMessage(message); + verify(mockConnections.get(0), never()).sendMessage(any()); + verify(mockConnections.get(1)).sendMessage(message); + verify(mockConnections.get(2), never()).sendMessage(any()); + } + + @Test + public void testRemoveAll() { + var message = new byte[] {0, 1, 2, 3, 4}; + + var removed = router.removeAll(); + assert removed.contains(mockConnections.get(0)); + assert removed.contains(mockConnections.get(1)); + assert removed.contains(mockConnections.get(2)); + + router.broadcastMessage(message); + verify(mockConnections.get(0), never()).sendMessage(message); + verify(mockConnections.get(1), never()).sendMessage(message); + verify(mockConnections.get(2), never()).sendMessage(message); + } +} diff --git a/crowdin.yml b/crowdin.yml index 04bbf659bf..b3c672b9d8 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,3 +1,3 @@ files: - source: /src/main/resources/net/rptools/maptool/language/i18n.properties - translation: /%original_path%/%file_name%_%two_letters_code%.%file_extension% + translation: /%original_path%/%file_name%_%locale_with_underscore%.%file_extension% diff --git a/settings.gradle b/settings.gradle index bd472eecbf..440cc0f959 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,3 +16,4 @@ include 'services:webservice' */ rootProject.name = 'MapTool' +include ':clientserver' \ No newline at end of file diff --git a/src/main/java/net/rptools/clientserver/ConnectionFactory.java b/src/main/java/net/rptools/clientserver/ConnectionFactory.java index 3a5302d2d3..ff9518ba7c 100644 --- a/src/main/java/net/rptools/clientserver/ConnectionFactory.java +++ b/src/main/java/net/rptools/clientserver/ConnectionFactory.java @@ -14,15 +14,16 @@ */ package net.rptools.clientserver; -import java.io.IOException; -import net.rptools.clientserver.simple.MessageHandler; +import java.awt.EventQueue; +import javax.annotation.Nullable; import net.rptools.clientserver.simple.connection.Connection; import net.rptools.clientserver.simple.connection.SocketConnection; import net.rptools.clientserver.simple.connection.WebRTCConnection; -import net.rptools.clientserver.simple.server.HandshakeProvider; +import net.rptools.clientserver.simple.server.NilServer; import net.rptools.clientserver.simple.server.Server; import net.rptools.clientserver.simple.server.SocketServer; import net.rptools.clientserver.simple.server.WebRTCServer; +import net.rptools.maptool.client.MapTool; import net.rptools.maptool.server.ServerConfig; public class ConnectionFactory { @@ -32,20 +33,46 @@ public static ConnectionFactory getInstance() { return instance; } - public Connection createConnection(String id, ServerConfig config) throws IOException { - if (!config.getUseWebRTC() || config.isPersonalServer()) + public Connection createConnection(String id, ServerConfig config) { + if (!config.getUseWebRTC()) { return new SocketConnection(id, config.getHostName(), config.getPort()); + } - return new WebRTCConnection(id, config); + return new WebRTCConnection( + id, + config.getServerName(), + new WebRTCConnection.Listener() { + @Override + public void onLoginError() { + MapTool.showError("Handshake.msg.playerAlreadyConnected"); + } + }); } - public Server createServer( - ServerConfig config, HandshakeProvider handshake, MessageHandler messageHandler) - throws IOException { - if (!config.getUseWebRTC() || config.isPersonalServer()) { - return new SocketServer(config.getPort(), handshake, messageHandler); + public Server createServer(@Nullable ServerConfig config) { + if (config == null) { + return new NilServer(); + } + + if (!config.getUseWebRTC()) { + return new SocketServer(config.getPort()); } - return new WebRTCServer(config, handshake, messageHandler); + return new WebRTCServer( + config.getServerName(), + new WebRTCServer.Listener() { + @Override + public void onLoginError() { + EventQueue.invokeLater( + () -> { + MapTool.showError("ServerDialog.error.serverAlreadyExists"); + }); + } + + @Override + public void onUnexpectedClose() { + MapTool.stopServer(); + } + }); } } diff --git a/src/main/java/net/rptools/clientserver/simple/server/AbstractServer.java b/src/main/java/net/rptools/clientserver/simple/server/AbstractServer.java deleted file mode 100644 index b966028f2a..0000000000 --- a/src/main/java/net/rptools/clientserver/simple/server/AbstractServer.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.clientserver.simple.server; - -import java.util.*; -import java.util.concurrent.ExecutionException; -import net.rptools.clientserver.simple.DisconnectHandler; -import net.rptools.clientserver.simple.MessageHandler; -import net.rptools.clientserver.simple.connection.Connection; -import net.rptools.maptool.server.Handshake; -import net.rptools.maptool.server.HandshakeObserver; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -public abstract class AbstractServer implements DisconnectHandler, Server, HandshakeObserver { - - private static final Logger log = LogManager.getLogger(AbstractServer.class); - // private final ReaperThread reaperThread; - - private final Map clients = - Collections.synchronizedMap(new HashMap()); - private final List observerList = - Collections.synchronizedList(new ArrayList()); - - private final HandshakeProvider handshakeProvider; - private final MessageHandler messageHandler; - - public AbstractServer(HandshakeProvider handshakeProvider, MessageHandler messageHandler) { - this.handshakeProvider = handshakeProvider; - this.messageHandler = messageHandler; - } - - public void addObserver(ServerObserver observer) { - observerList.add(observer); - } - - public void removeObserver(ServerObserver observer) { - observerList.remove(observer); - } - - public void broadcastMessage(byte[] message) { - synchronized (clients) { - for (Connection conn : clients.values()) { - conn.sendMessage(message); - } - } - } - - public void broadcastMessage(String[] exclude, byte[] message) { - Set excludeSet = new HashSet(); - for (String e : exclude) { - excludeSet.add(e); - } - synchronized (clients) { - for (Map.Entry entry : clients.entrySet()) { - if (!excludeSet.contains(entry.getKey())) { - entry.getValue().sendMessage(message); - } - } - } - } - - public void sendMessage(String id, Object channel, byte[] message) { - Connection client = clients.get(id); - client.sendMessage(channel, message); - } - - public void close() { - synchronized (clients) { - for (Connection conn : clients.values()) { - conn.close(); - } - } - } - - protected void reapClients() { - log.debug("About to reap clients"); - synchronized (clients) { - log.debug("Reaping clients"); - - for (Iterator> i = clients.entrySet().iterator(); - i.hasNext(); ) { - Map.Entry entry = i.next(); - Connection conn = entry.getValue(); - if (!conn.isAlive()) { - log.debug("\tReaping: " + conn.getId()); - i.remove(); - try { - fireClientDisconnect(conn); - conn.close(); - } catch (Exception e) { - // Don't want to raise an error if notification of removing a dead connection failed - } - } - } - } - } - - protected void fireClientConnect(Connection conn) { - log.debug("Firing: clientConnect: " + conn.getId()); - for (ServerObserver observer : observerList) { - observer.connectionAdded(conn); - } - } - - protected void fireClientDisconnect(Connection conn) { - log.debug("Firing: clientDisconnect: " + conn.getId()); - for (ServerObserver observer : observerList) { - observer.connectionRemoved(conn); - } - } - - //// - // DISCONNECT HANDLER - public void handleDisconnect(Connection conn) { - log.debug("HandleDisconnect: " + conn.getId()); - fireClientDisconnect(conn); - } - - protected void handleConnection(Connection conn) throws ExecutionException, InterruptedException { - var handshake = handshakeProvider.getConnectionHandshake(conn); - handshake.addObserver(this); - // Make sure the client is allowed - handshake.startHandshake(); - } - - public void onCompleted(Handshake handshake) { - handshake.removeObserver(this); - var conn = handshake.getConnection(); - handshakeProvider.releaseHandshake(conn); - if (handshake.isSuccessful()) { - conn.addMessageHandler(messageHandler); - conn.addDisconnectHandler(this); - - log.debug("About to add new client"); - synchronized (clients) { - reapClients(); - - log.debug("Adding new client"); - clients.put(conn.getId(), conn); - fireClientConnect(conn); - // System.out.println("new client " + conn.getId() + " added, " + server.clients.size() - // + " total"); - } - } else { - log.debug("Client closing: bad handshake"); - conn.close(); - } - } -} diff --git a/src/main/java/net/rptools/maptool/api/ApiData.java b/src/main/java/net/rptools/maptool/api/ApiData.java deleted file mode 100644 index 41a4768678..0000000000 --- a/src/main/java/net/rptools/maptool/api/ApiData.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.api; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; - -public interface ApiData { - - default JsonObject asJsonObject() { - Gson gson = new Gson(); - return gson.toJsonTree(this).getAsJsonObject(); - } -} diff --git a/src/main/java/net/rptools/maptool/api/maptool/MapToolApi.java b/src/main/java/net/rptools/maptool/api/maptool/MapToolApi.java deleted file mode 100644 index e995a1285d..0000000000 --- a/src/main/java/net/rptools/maptool/api/maptool/MapToolApi.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.api.maptool; - -import java.util.concurrent.CompletableFuture; -import net.rptools.maptool.api.util.ApiResult; - -public class MapToolApi { - - public CompletableFuture> getVersion() { - return CompletableFuture.completedFuture(new ApiResult(new MapToolInfo())); - } -} diff --git a/src/main/java/net/rptools/maptool/api/maptool/MapToolInfo.java b/src/main/java/net/rptools/maptool/api/maptool/MapToolInfo.java deleted file mode 100644 index 937902ce50..0000000000 --- a/src/main/java/net/rptools/maptool/api/maptool/MapToolInfo.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.api.maptool; - -import net.rptools.maptool.api.ApiData; -import net.rptools.maptool.client.MapTool; - -// @XmlRootElement -public record MapToolInfo( - String mapToolVersion, String webEndpointVersion, boolean developmentVersion) - implements ApiData { - - public MapToolInfo() { - this(MapTool.getVersion(), "unavailable", MapTool.isDevelopment()); - } -} diff --git a/src/main/java/net/rptools/maptool/api/util/ApiCall.java b/src/main/java/net/rptools/maptool/api/util/ApiCall.java deleted file mode 100644 index 54ad4bebed..0000000000 --- a/src/main/java/net/rptools/maptool/api/util/ApiCall.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.api.util; - -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import javax.swing.SwingUtilities; -import net.rptools.maptool.api.ApiData; -import net.rptools.maptool.api.ApiException; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -public class ApiCall { - - private static final Logger log = LogManager.getLogger(ApiCall.class); - - public CompletableFuture> runOnSwingThread(Callable callable) { - try { - if (SwingUtilities.isEventDispatchThread()) { - return CompletableFuture.completedFuture(doCall(callable)); - } else { - return CompletableFuture.supplyAsync(() -> doCall(callable)); - } - } catch (Exception e) { - log.error(e); - return CompletableFuture.completedFuture( - new ApiResult<>(new ApiException("err.internal", e))); - } - } - - private ApiResult doCall(Callable callable) { - try { - return new ApiResult(callable.call()); - } catch (Exception e) { - log.error(e); - return new ApiResult<>(new ApiException("err.internal", e)); - } - } -} diff --git a/src/main/java/net/rptools/maptool/api/util/ApiListResult.java b/src/main/java/net/rptools/maptool/api/util/ApiListResult.java deleted file mode 100644 index e6f28dcba6..0000000000 --- a/src/main/java/net/rptools/maptool/api/util/ApiListResult.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.api.util; - -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import java.util.List; -import net.rptools.maptool.api.ApiData; -import net.rptools.maptool.api.ApiException; - -public class ApiListResult { - - private final List data; - private final ApiResultStatus status; - private final ApiException exception; - - public static ApiListResult INTERNAL_ERROR_RESULT = - new ApiListResult<>(List.of(new NoData())); - - public ApiListResult(List data) { - this.data = List.copyOf(data); - this.exception = null; - this.status = data != null ? ApiResultStatus.OK : ApiResultStatus.NONE; - } - - public ApiListResult(ApiException e) { - this.data = null; - this.status = ApiResultStatus.ERROR; - this.exception = e; - } - - public List getData() { - return data; - } - - public ApiResultStatus getStatus() { - return status; - } - - public String getStatusMessage() { - if (exception == null) { - return ""; - } else { - return exception.getMessage(); - } - } - - public JsonObject asJsonObject() { - JsonObject json = new JsonObject(); - if (data != null) { - JsonArray objList = - data.stream() - .map(ApiData::asJsonObject) - .collect(JsonArray::new, JsonArray::add, JsonArray::addAll); - json.add("data", objList); - } - json.addProperty("status", status.getTextValue()); - String msg = getStatusMessage(); - if (msg != null && msg.length() > 0) { - json.addProperty("message", msg); - } - - return json; - } -} diff --git a/src/main/java/net/rptools/maptool/api/util/ApiNoResult.java b/src/main/java/net/rptools/maptool/api/util/ApiNoResult.java deleted file mode 100644 index 055c0e032d..0000000000 --- a/src/main/java/net/rptools/maptool/api/util/ApiNoResult.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.api.util; - -import com.google.gson.JsonObject; -import net.rptools.maptool.api.ApiData; -import net.rptools.maptool.api.ApiException; - -public class ApiNoResult { - - private final ApiResultStatus status; - private final ApiException exception; - private final T data; - - // public static ApiNoResult NOT_FOUND = new ApiNoResult<>(new NoData()); - - public ApiNoResult() { - this(null); - } - - public ApiNoResult(ApiException e) { - this.data = null; - this.status = ApiResultStatus.ERROR; - this.exception = e; - } - - public T getData() { - return data; - } - - public ApiResultStatus getStatus() { - return status; - } - - public String getStatusMessage() { - if (exception == null) { - return ""; - } else { - return exception.getMessage(); - } - } - - public JsonObject asJsonObject() { - JsonObject json = new JsonObject(); - if (data != null) { - json.add("data", data.asJsonObject()); - } - json.addProperty("status", status.getTextValue()); - String msg = getStatusMessage(); - if (msg != null && msg.length() > 0) { - json.addProperty("message", msg); - } - - return json; - } -} diff --git a/src/main/java/net/rptools/maptool/api/util/ApiResult.java b/src/main/java/net/rptools/maptool/api/util/ApiResult.java deleted file mode 100644 index dbac10c2db..0000000000 --- a/src/main/java/net/rptools/maptool/api/util/ApiResult.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.api.util; - -import com.google.gson.JsonObject; -import net.rptools.maptool.api.ApiData; -import net.rptools.maptool.api.ApiException; - -public class ApiResult { - - private final T data; - private final ApiResultStatus status; - private final ApiException exception; - - public static ApiResult NOT_FOUND = new ApiResult<>(new NoData()); - - public ApiResult(T data) { - this.data = data; - this.exception = null; - this.status = data != null ? ApiResultStatus.OK : ApiResultStatus.NONE; - } - - public ApiResult(ApiException e) { - this.data = null; - this.status = ApiResultStatus.ERROR; - this.exception = e; - } - - public T getData() { - return data; - } - - public ApiResultStatus getStatus() { - return status; - } - - public String getStatusMessage() { - if (exception == null) { - return ""; - } else { - return exception.getMessage(); - } - } - - public JsonObject asJsonObject() { - JsonObject json = new JsonObject(); - if (data != null) { - json.add("data", data.asJsonObject()); - } - json.addProperty("status", status.getTextValue()); - String msg = getStatusMessage(); - if (msg != null && msg.length() > 0) { - json.addProperty("message", msg); - } - - return json; - } -} diff --git a/src/main/java/net/rptools/maptool/api/util/ApiResultStatus.java b/src/main/java/net/rptools/maptool/api/util/ApiResultStatus.java deleted file mode 100644 index 2ead9c24c2..0000000000 --- a/src/main/java/net/rptools/maptool/api/util/ApiResultStatus.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.api.util; - -public enum ApiResultStatus { - OK("ok", ""), - ERROR("error", "Internal API Error"), - NONE("none", "Not Found"); - - private final String textValue; - private final String defaultMessage; - - ApiResultStatus(String val, String defaultMsg) { - textValue = val; - defaultMessage = defaultMsg; - } - - public String getTextValue() { - return textValue; - } - - public String getDefaultMessage() { - return defaultMessage; - } -} diff --git a/src/main/java/net/rptools/maptool/api/util/NoData.java b/src/main/java/net/rptools/maptool/api/util/NoData.java deleted file mode 100644 index bd9bda933f..0000000000 --- a/src/main/java/net/rptools/maptool/api/util/NoData.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.api.util; - -import net.rptools.maptool.api.ApiData; - -public record NoData() implements ApiData {} diff --git a/src/main/java/net/rptools/maptool/client/AppActions.java b/src/main/java/net/rptools/maptool/client/AppActions.java index 6c56d03f69..c49300c1b7 100644 --- a/src/main/java/net/rptools/maptool/client/AppActions.java +++ b/src/main/java/net/rptools/maptool/client/AppActions.java @@ -31,7 +31,6 @@ import java.text.SimpleDateFormat; import java.util.*; import java.util.List; -import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.zip.GZIPOutputStream; import java.util.zip.ZipEntry; @@ -78,7 +77,6 @@ import net.rptools.maptool.model.drawing.DrawableTexturePaint; import net.rptools.maptool.model.player.*; import net.rptools.maptool.model.player.Player.Role; -import net.rptools.maptool.model.player.PlayerDatabaseFactory.PlayerDatabaseType; import net.rptools.maptool.server.ServerConfig; import net.rptools.maptool.server.ServerPolicy; import net.rptools.maptool.util.*; @@ -143,7 +141,7 @@ private static int getMenuShortcutKeyMask() { public static final Action NEXT_TOKEN = new ZoneClientAction() { { - init("menu.nextToken"); + init("action.nextToken"); } @Override @@ -640,13 +638,11 @@ public boolean isAvailable() { @Override protected void executeAction() { - - if (MapTool.getServer() == null) { - return; + final var server = MapTool.getServer(); + if (server != null) { + ConnectionInfoDialog dialog = new ConnectionInfoDialog(server); + dialog.setVisible(true); } - - ConnectionInfoDialog dialog = new ConnectionInfoDialog(MapTool.getServer()); - dialog.setVisible(true); } }; @@ -1280,7 +1276,7 @@ protected void executeAction() { MapTool.showError("msg.error.cantBootSelf"); return; } - if (MapTool.isPlayerConnected(selectedPlayer.getName())) { + if (MapTool.getClient().isPlayerConnected(selectedPlayer.getName())) { String msg = I18N.getText("msg.confirm.bootPlayer", selectedPlayer.getName()); if (MapTool.confirm(msg)) { MapTool.serverCommand().bootPlayer(selectedPlayer.getName()); @@ -2108,11 +2104,13 @@ public boolean isSelected() { @Override protected void executeAction() { + var client = MapTool.getClient(); - ServerPolicy policy = MapTool.getServerPolicy(); + ServerPolicy policy = client.getServerPolicy(); policy.setIsMovementLocked(!policy.isMovementLocked()); - MapTool.updateServerPolicy(policy); + client.setServerPolicy(policy); + client.getServerCommand().setServerPolicy(policy); } }; @@ -2130,11 +2128,13 @@ public boolean isSelected() { @Override protected void executeAction() { + var client = MapTool.getClient(); - ServerPolicy policy = MapTool.getServerPolicy(); + ServerPolicy policy = client.getServerPolicy(); policy.setIsTokenEditorLocked(!policy.isTokenEditorLocked()); - MapTool.updateServerPolicy(policy); + client.setServerPolicy(policy); + client.getServerCommand().setServerPolicy(policy); } }; @@ -2162,8 +2162,9 @@ protected void executeAction() { StartServerDialog dialog = new StartServerDialog(); dialog.showDialog(); - if (!dialog.accepted()) // Results stored in Preferences.userRoot() - return; + if (!dialog.accepted()) { // Results stored in Preferences.userRoot() + return; + } StartServerDialogPreferences serverProps = new StartServerDialogPreferences(); // data retrieved from @@ -2227,26 +2228,19 @@ protected void executeAction() { boolean failed = false; try { - ServerDisconnectHandler.disconnectExpected = true; + MapTool.disconnect(); MapTool.stopServer(); - // Use UPnP to open port in router - if (serverProps.getUseUPnP()) { - UPnPUtil.openPort(serverProps.getPort()); - } // Right now set this is set to whatever the last server settings were. If we // wanted to turn it on and // leave it turned on, the line would change to: // campaign.setHasUsedFogToolbar(useIF || campaign.hasUsedFogToolbar()); campaign.setHasUsedFogToolbar(useIF); - PlayerDatabaseFactory.setServerConfig(config); + ServerSidePlayerDatabase playerDatabase; if (serverProps.getUsePasswordFile()) { - PlayerDatabaseFactory.setCurrentPlayerDatabase( - PlayerDatabaseType.PASSWORD_FILE); PasswordFilePlayerDatabase db = - (PasswordFilePlayerDatabase) - PlayerDatabaseFactory.getCurrentPlayerDatabase(); + PlayerDatabaseFactory.getPasswordFilePlayerDatabase(); db.initialize(); if (serverProps.getRole() == Role.GM) { db.addTemporaryPlayer( @@ -2255,13 +2249,22 @@ protected void executeAction() { db.addTemporaryPlayer( dialog.getUsernameTextField().getText(), Role.PLAYER, playerPassword); } + playerDatabase = db; } else { - PlayerDatabaseFactory.setCurrentPlayerDatabase(PlayerDatabaseType.DEFAULT); + playerDatabase = + PlayerDatabaseFactory.getDefaultPlayerDatabase( + config.getPlayerPassword(), config.getGmPassword()); } - PlayerDatabase playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); // Make a copy of the campaign since we don't coordinate local changes well ... // yet + Player.Role playerType = (Player.Role) dialog.getRoleCombo().getSelectedItem(); + final var player = + new LocalPlayer( + dialog.getUsernameTextField().getText(), + playerType, + (playerType == Role.GM) ? gmPassword : playerPassword); + /* * JFJ 2010-10-27 The below creates a NEW campaign with a copy of the existing campaign. However, this is NOT a full copy. In the constructor called below, each zone from the * previous campaign(ie, the one passed in) is recreated. This means that only some items for that campaign, zone(s), and token's are copied over when you start a new server @@ -2272,37 +2275,11 @@ protected void executeAction() { MapTool.startServer( dialog.getUsernameTextField().getText(), config, + serverProps.getUseUPnP(), policy, campaign, playerDatabase, - true); - - // Connect to server - Player.Role playerType = (Player.Role) dialog.getRoleCombo().getSelectedItem(); - Runnable onConnected = - () -> { - // connecting - MapTool.getFrame() - .getConnectionStatusPanel() - .setStatus(ConnectionStatusPanel.Status.server); - MapTool.addLocalMessage( - MessageUtil.getFormattedSystemMsg( - I18N.getText("msg.info.startServer"))); - }; - - if (playerType == Player.Role.GM) { - MapTool.createConnection( - config, - new LocalPlayer( - dialog.getUsernameTextField().getText(), playerType, gmPassword), - onConnected); - } else { - MapTool.createConnection( - config, - new LocalPlayer( - dialog.getUsernameTextField().getText(), playerType, playerPassword), - onConnected); - } + player); } catch (UnknownHostException uh) { MapTool.showError("msg.error.invalidLocalhost", uh); failed = true; @@ -2313,9 +2290,7 @@ protected void executeAction() { | InvalidAlgorithmParameterException | InvalidKeySpecException | NoSuchPaddingException - | InvalidKeyException - | ExecutionException - | InterruptedException e) { + | InvalidKeyException e) { MapTool.showError("msg.error.initializeCrypto", e); failed = true; } catch (PasswordDatabaseException pwde) { @@ -2326,11 +2301,7 @@ protected void executeAction() { if (failed) { try { MapTool.startPersonalServer(campaign); - } catch (IOException - | NoSuchAlgorithmException - | InvalidKeySpecException - | ExecutionException - | InterruptedException e) { + } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) { MapTool.showError("msg.error.failedStartPersonalServer", e); } } @@ -2359,8 +2330,9 @@ protected void executeAction() { return; } - ServerDisconnectHandler.disconnectExpected = true; LOAD_MAP.setSeenWarning(false); + + MapTool.disconnect(); MapTool.stopServer(); // Install a temporary gimped campaign until we get the one from the @@ -2373,9 +2345,8 @@ protected void executeAction() { .getConnectionStatusPanel() .setStatus(ConnectionStatusPanel.Status.connected); - // Show the user something interesting until we've got the campaign - // Look in ClientMethodHandler.setCampaign() for the corresponding - // hideGlassPane + // Show the user something interesting while we're connecting. Look below for the + // corresponding hideGlassPane StaticMessageDialog progressDialog = new StaticMessageDialog(I18N.getText("msg.info.connecting")); MapTool.getFrame().showFilledGlassPane(progressDialog); @@ -2393,20 +2364,30 @@ protected void executeAction() { dialog.getPort(), prefs.getServerName(), dialog.getServer(), + false, dialog.getUseWebRTC()); String password = prefs.getUsePublicKey() ? new PasswordGenerator().getPassword() : prefs.getPassword(); - MapTool.createConnection( + MapTool.connectToRemoteServer( config, new LocalPlayer(prefs.getUsername(), prefs.getRole(), password), - () -> { - MapTool.getFrame().hideGlassPane(); - MapTool.getFrame() - .showFilledGlassPane( - new StaticMessageDialog(I18N.getText("msg.info.campaignLoading"))); + (success) -> { + EventQueue.invokeLater( + () -> { + MapTool.getFrame().hideGlassPane(); + if (success) { + // Show the user something interesting until we've got the campaign + // Look in ClientMethodHandler.setCampaign() for the corresponding + // hideGlassPane + MapTool.getFrame() + .showFilledGlassPane( + new StaticMessageDialog( + I18N.getText("msg.info.campaignLoading"))); + } + }); }); } catch (UnknownHostException e1) { @@ -2415,10 +2396,7 @@ protected void executeAction() { } catch (IOException e1) { MapTool.showError("msg.error.failedLoadCampaign", e1); failed = true; - } catch (NoSuchAlgorithmException - | InvalidKeySpecException - | ExecutionException - | InterruptedException e1) { + } catch (NoSuchAlgorithmException | InvalidKeySpecException e1) { MapTool.showError("msg.error.initializeCrypto", e1); failed = true; } @@ -2426,11 +2404,7 @@ protected void executeAction() { MapTool.getFrame().hideGlassPane(); try { MapTool.startPersonalServer(oldCampaign); - } catch (IOException - | NoSuchAlgorithmException - | InvalidKeySpecException - | ExecutionException - | InterruptedException e) { + } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) { MapTool.showError("msg.error.failedStartPersonalServer", e); } } @@ -2457,7 +2431,16 @@ protected void executeAction() { } }; + /** + * Disconnects the client and starts a personal server. + * + *

If we are hosting the server, the personal server will have the same campaign as the server. + * Otherwise a new basic campaign will be created. + */ public static void disconnectFromServer() { + // hide map so player doesn't get a brief GM view + MapTool.getFrame().setCurrentZoneRenderer(null); + Campaign campaign; if (MapTool.isHostingServer()) { campaign = MapTool.getCampaign(); @@ -2465,20 +2448,18 @@ public static void disconnectFromServer() { campaign = CampaignFactory.createBasicCampaign(); new CampaignManager().clearCampaignData(); } - ServerDisconnectHandler.disconnectExpected = true; + LOAD_MAP.setSeenWarning(false); - MapTool.stopServer(); + MapTool.disconnect(); + MapTool.stopServer(); + MapTool.getFrame().getToolbarPanel().getMapselect().setVisible(true); MapTool.getFrame().getToolbarPanel().setTokenSelectionGroupEnabled(true); try { MapTool.startPersonalServer(campaign); - } catch (IOException - | NoSuchAlgorithmException - | InvalidKeySpecException - | ExecutionException - | InterruptedException e) { + } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) { MapTool.showError("msg.error.failedStartPersonalServer", e); } } @@ -2491,8 +2472,7 @@ public static void disconnectFromServer() { @Override public boolean isAvailable() { - return PlayerDatabaseFactory.getCurrentPlayerDatabase() - instanceof PersistedPlayerDatabase; + return MapTool.getClient().getPlayerDatabase() instanceof PersistedPlayerDatabase; } @Override @@ -3359,7 +3339,7 @@ public ToggleWindowAction(MTFrame mtFrame) { @Override public boolean isSelected() { - return MapTool.getFrame().getFrame(mtFrame).isShowing(); + return !MapTool.getFrame().getFrame(mtFrame).isHidden(); } @Override @@ -3370,7 +3350,7 @@ public boolean isAvailable() { @Override protected void executeAction() { DockableFrame frame = MapTool.getFrame().getFrame(mtFrame); - if (frame.isShowing()) { + if (!frame.isHidden()) { MapTool.getFrame().getDockingManager().hideFrame(mtFrame.name()); } else { MapTool.getFrame().getDockingManager().showFrame(mtFrame.name()); diff --git a/src/main/java/net/rptools/maptool/client/ClientMessageHandler.java b/src/main/java/net/rptools/maptool/client/ClientMessageHandler.java index 3e0d16246d..9b66a3a4e0 100644 --- a/src/main/java/net/rptools/maptool/client/ClientMessageHandler.java +++ b/src/main/java/net/rptools/maptool/client/ClientMessageHandler.java @@ -18,6 +18,7 @@ import java.awt.Point; import java.awt.geom.Area; import java.io.IOException; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -73,6 +74,7 @@ import net.rptools.maptool.server.proto.*; import net.rptools.maptool.transfer.AssetConsumer; import net.rptools.maptool.transfer.AssetHeader; +import net.rptools.maptool.util.MessageUtil; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -85,14 +87,18 @@ public class ClientMessageHandler implements MessageHandler { private static final Logger log = LogManager.getLogger(ClientMessageHandler.class); - public ClientMessageHandler() {} + private final MapToolClient client; + + public ClientMessageHandler(MapToolClient client) { + this.client = client; + } @Override public void handleMessage(String id, byte[] message) { try { var msg = Message.parseFrom(message); var msgType = msg.getMessageTypeCase(); - log.debug(id + " got: " + msgType); + log.debug("{} got: {}", id, msgType); switch (msgType) { case ADD_TOPOLOGY_MSG -> handle(msg.getAddTopologyMsg()); @@ -189,7 +195,7 @@ private void handle(UpdateExposedAreaMetaMsg msg) { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); var tokenGUID = msg.hasTokenGuid() ? GUID.valueOf(msg.getTokenGuid().getValue()) : null; ExposedAreaMetaData meta = new ExposedAreaMetaData(Mapper.map(msg.getArea())); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); zone.setExposedAreaMetaData(tokenGUID, meta); }); } @@ -201,7 +207,7 @@ private void handle(UpdateGmMacrosMsg msg) { msg.getMacrosList().stream() .map(MacroButtonProperties::fromDto) .collect(Collectors.toList()); - MapTool.getCampaign().setGmMacroButtonPropertiesArray(macros); + client.getCampaign().setGmMacroButtonPropertiesArray(macros); MapTool.getFrame().getGmPanel().reset(); }); } @@ -213,7 +219,7 @@ private void handle(UpdateCampaignMacrosMsg msg) { msg.getMacrosList().stream() .map(MacroButtonProperties::fromDto) .collect(Collectors.toList()); - MapTool.getCampaign().setMacroButtonPropertiesArray(macros); + client.getCampaign().setMacroButtonPropertiesArray(macros); MapTool.getFrame().getCampaignPanel().reset(); }); } @@ -223,7 +229,7 @@ private void handle(UpdateTokenInitiativeMsg msg) { () -> { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); var tokenGUID = GUID.valueOf(msg.getTokenGuid()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); var list = zone.getInitiativeList(); TokenInitiative ti = list.getTokenInitiative(msg.getIndex()); if (!ti.getId().equals(tokenGUID)) { @@ -260,7 +266,7 @@ private void handle(UpdateCampaignMsg msg) { () -> { CampaignProperties properties = CampaignProperties.fromDto(msg.getProperties()); - MapTool.getCampaign().replaceCampaignProperties(properties); + client.getCampaign().replaceCampaignProperties(properties); MapToolFrame frame = MapTool.getFrame(); ZoneRenderer zr = frame.getCurrentZoneRenderer(); if (zr != null) { @@ -283,7 +289,7 @@ private void handle(SetServerPolicyMsg msg) { EventQueue.invokeLater( () -> { ServerPolicy policy = ServerPolicy.fromDto(msg.getPolicy()); - MapTool.setServerPolicy(policy); + client.setServerPolicy(policy); MapTool.getFrame().getToolbox().updateTools(); }); } @@ -341,11 +347,11 @@ private void handle(SetZoneVisibilityMsg msg) { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); boolean visible = msg.getIsVisible(); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); zone.setVisible(visible); ZoneRenderer currentRenderer = MapTool.getFrame().getCurrentZoneRenderer(); if (!visible - && !MapTool.getPlayer().isGM() + && !client.getPlayer().isGM() && currentRenderer != null && currentRenderer.getZone().getId().equals(zoneGUID)) { Collection AllTokenIDs = new ArrayList<>(); @@ -369,7 +375,7 @@ private void handle(UndoDrawMsg msg) { () -> { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); GUID drawableId = GUID.valueOf(msg.getDrawableGuid()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); if (zone == null) { return; } @@ -387,7 +393,7 @@ private void handle(UpdateDrawingMsg msg) { Pen p = Pen.fromDto(msg.getPen()); DrawnElement de = DrawnElement.fromDto(msg.getDrawing()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); zone.updateDrawable(de, p); MapTool.getFrame().refresh(); }); @@ -397,7 +403,7 @@ private void handle(UpdateTokenPropertyMsg msg) { EventQueue.invokeLater( () -> { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); var tokenGUID = GUID.valueOf(msg.getTokenGuid()); var token = zone.getToken(tokenGUID); if (token != null) { @@ -518,7 +524,7 @@ private void handle(SetZoneHasFowMsg msg) { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); boolean hasFog = msg.getHasFow(); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); zone.setHasFog(hasFog); // In case we're looking at the zone @@ -535,10 +541,14 @@ private void handle(SetZoneGridSizeMsg msg) { int size = msg.getSize(); int color = msg.getColor(); - var zone = MapTool.getCampaign().getZone(zoneGUID); - zone.getGrid().setSize(size); - zone.getGrid().setOffset(xOffset, yOffset); - zone.setGridColor(color); + var zone = client.getCampaign().getZone(zoneGUID); + // Sometimes these messages can come in as a zone is being removed, so we can't rely on + // its existence + if (zone != null) { + zone.getGrid().setSize(size); + zone.getGrid().setOffset(xOffset, yOffset); + zone.setGridColor(color); + } MapTool.getFrame().refresh(); }); @@ -549,7 +559,7 @@ private void handle(SetVisionTypeMsg msg) { () -> { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); VisionType visionType = VisionType.valueOf(msg.getVision().name()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); if (zone != null) { zone.setVisionType(visionType); if (MapTool.getFrame().getCurrentZoneRenderer() != null) { @@ -565,7 +575,7 @@ private void handle(SetTokenLocationMsg msg) { EventQueue.invokeLater( () -> { // Only the table should process this - if (MapTool.getPlayer().getName().equalsIgnoreCase("Table")) { + if (client.getPlayer().getName().equalsIgnoreCase("Table")) { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); var keyToken = GUID.valueOf(msg.getTokenGuid()); @@ -576,7 +586,7 @@ private void handle(SetTokenLocationMsg msg) { var y = msg.getLocation().getY(); // Get the zone - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); // Get the token var token = zone.getToken(keyToken); @@ -589,7 +599,7 @@ private void handle(SetTokenLocationMsg msg) { token.setX(zp2.x); token.setY(zp2.y); - MapTool.serverCommand().putToken(zoneGUID, token); + client.getServerCommand().putToken(zoneGUID, token); } }); } @@ -615,7 +625,7 @@ private void handle(SetFowMsg msg) { var selectedTokens = msg.getSelectedTokensList().stream().map(GUID::valueOf).collect(Collectors.toSet()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); zone.setFogArea(area, selectedTokens); MapTool.getFrame().refresh(); }); @@ -624,7 +634,7 @@ private void handle(SetFowMsg msg) { private void handle(SetCampaignNameMsg msg) { EventQueue.invokeLater( () -> { - MapTool.getCampaign().setName(msg.getName()); + client.getCampaign().setName(msg.getName()); MapTool.getFrame().setTitle(); }); } @@ -644,7 +654,7 @@ private void handle(SetBoardMsg msg) { EventQueue.invokeLater( () -> { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); Point boardXY = Mapper.map(msg.getPoint()); var assetId = new MD5Key(msg.getAssetId()); @@ -666,7 +676,7 @@ private void handle(RenameZoneMsg msg) { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); String name = msg.getName(); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); if (zone != null) { zone.setName(name); } @@ -680,7 +690,7 @@ private void handle(RemoveZoneMsg msg) { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); final var renderer = MapTool.getFrame().getZoneRenderer(zoneGUID); final var zone = renderer.getZone(); - MapTool.getCampaign().removeZone(zoneGUID); + client.getCampaign().removeZone(zoneGUID); MapTool.getFrame().removeZoneRenderer(renderer); // Now we have fire off adding the tokens in the zone @@ -698,7 +708,7 @@ private void handle(RemoveTopologyMsg msg) { var area = Mapper.map(msg.getArea()); var topologyType = Zone.TopologyType.valueOf(msg.getType().name()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); zone.removeTopology(area, topologyType); MapTool.getFrame().getZoneRenderer(zoneGUID).repaint(); @@ -709,7 +719,7 @@ private void handle(RemoveTokensMsg msg) { EventQueue.invokeLater( () -> { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); var tokenGUIDs = msg.getTokenGuidList().stream().map(GUID::valueOf).collect(Collectors.toList()); zone.removeTokens(tokenGUIDs); @@ -721,7 +731,7 @@ private void handle(RemoveTokenMsg msg) { EventQueue.invokeLater( () -> { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); var tokenGUID = GUID.valueOf(msg.getTokenGuid()); zone.removeToken(tokenGUID); MapTool.getFrame().refresh(); @@ -732,7 +742,7 @@ private void handle(RemoveLabelMsg msg) { EventQueue.invokeLater( () -> { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); GUID labelGUID = GUID.valueOf(msg.getLabelGuid()); zone.removeLabel(labelGUID); MapTool.getFrame().refresh(); @@ -743,7 +753,7 @@ private void handle(PutZoneMsg msg) { EventQueue.invokeLater( () -> { Zone zone = Zone.fromDto(msg.getZone()); - MapTool.getCampaign().putZone(zone); + client.getCampaign().putZone(zone); // TODO: combine this with MapTool.addZone() var renderer = ZoneRendererFactory.newRenderer(zone); @@ -762,7 +772,7 @@ private void handle(PutLabelMsg msg) { EventQueue.invokeLater( () -> { var zoneGUID = GUID.valueOf(msg.getZoneGuid()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); Label label = Label.fromDto(msg.getLabel()); zone.putLabel(label); MapTool.getFrame().refresh(); @@ -781,7 +791,16 @@ private void handle(PutAssetMsg msg) { private void handle(PlayerDisconnectedMsg msg) { EventQueue.invokeLater( () -> { - MapTool.removePlayer(Player.fromDto(msg.getPlayer())); + var player = Player.fromDto(msg.getPlayer()); + client.removePlayer(player); + + if (!player.equals(client.getPlayer())) { + MapTool.addLocalMessage( + MessageUtil.getFormattedSystemMsg( + MessageFormat.format( + I18N.getText("msg.info.playerDisconnected"), player.getName()))); + } + MapTool.getFrame().refresh(); }); } @@ -789,7 +808,16 @@ private void handle(PlayerDisconnectedMsg msg) { private void handle(PlayerConnectedMsg msg) { EventQueue.invokeLater( () -> { - MapTool.addPlayer(Player.fromDto(msg.getPlayer())); + var player = Player.fromDto(msg.getPlayer()); + client.addPlayer(player); + + if (!player.equals(client.getPlayer())) { + MapTool.addLocalMessage( + MessageUtil.getFormattedSystemMsg( + MessageFormat.format( + I18N.getText("msg.info.playerConnected"), player.getName()))); + } + MapTool.getFrame().refresh(); }); } @@ -832,7 +860,7 @@ private void handle(HideFowMsg msg) { var selectedTokens = msg.getTokenGuidList().stream().map(GUID::valueOf).collect(Collectors.toSet()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); zone.hideArea(area, selectedTokens); MapTool.getFrame().refresh(); }); @@ -854,7 +882,7 @@ private void handle(ExposeFowMsg msg) { Area area = Mapper.map(msg.getArea()); var selectedTokens = msg.getTokenGuidList().stream().map(GUID::valueOf).collect(Collectors.toSet()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); zone.exposeArea(area, selectedTokens); MapTool.getFrame().refresh(); }); @@ -906,7 +934,7 @@ private void handle(EnforceZoneMsg msg) { if (renderer != null && renderer != MapTool.getFrame().getCurrentZoneRenderer() - && (renderer.getZone().isVisible() || MapTool.getPlayer().isGM())) { + && (renderer.getZone().isVisible() || client.getPlayer().isGM())) { MapTool.getFrame().setCurrentZoneRenderer(renderer); } }); @@ -921,7 +949,7 @@ private void handle(PutTokenMsg putTokenMsg) { EventQueue.invokeLater( () -> { var zoneGUID = GUID.valueOf(putTokenMsg.getZoneGuid()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); var token = Token.fromDto(putTokenMsg.getToken()); zone.putToken(token); MapTool.getFrame().refresh(); @@ -932,7 +960,7 @@ private void handle(EditTokenMsg editTokenMsg) { EventQueue.invokeLater( () -> { var zoneGUID = GUID.valueOf(editTokenMsg.getZoneGuid()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); var token = Token.fromDto(editTokenMsg.getToken()); zone.editToken(token); MapTool.getFrame().refresh(); @@ -946,7 +974,7 @@ private void handle(DrawMsg drawMsg) { Pen pen = Pen.fromDto(drawMsg.getPen()); Drawable drawable = Drawable.fromDto(drawMsg.getDrawable()); - var zone = MapTool.getCampaign().getZone(zoneGuid); + var zone = client.getCampaign().getZone(zoneGuid); zone.addDrawable(new DrawnElement(drawable, pen)); MapTool.getFrame().refresh(); }); @@ -956,7 +984,7 @@ private void handle(ClearExposedAreaMsg clearExposedAreaMsg) { EventQueue.invokeLater( () -> { var zoneGUID = GUID.valueOf(clearExposedAreaMsg.getZoneGuid()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); zone.clearExposedArea(clearExposedAreaMsg.getGlobalOnly()); }); } @@ -966,7 +994,7 @@ private void handle(ClearAllDrawingsMsg clearAllDrawingsMsg) { () -> { var zoneGUID = GUID.valueOf(clearAllDrawingsMsg.getZoneGuid()); var layer = Zone.Layer.valueOf(clearAllDrawingsMsg.getLayer()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); zone.clearDrawables(layer); MapTool.getFrame().refresh(); }); @@ -978,7 +1006,7 @@ private void handle(ChangeZoneDisplayNameMsg changeZoneDisplayNameMsg) { var zoneGUID = GUID.valueOf(changeZoneDisplayNameMsg.getZoneGuid()); String displayName = changeZoneDisplayNameMsg.getName(); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); if (zone != null) { zone.setPlayerAlias(displayName); } @@ -997,7 +1025,7 @@ private void handle(AddTopologyMsg addTopologyMsg) { var area = Mapper.map(addTopologyMsg.getArea()); var topologyType = Zone.TopologyType.valueOf(addTopologyMsg.getType().name()); - var zone = MapTool.getCampaign().getZone(zoneGUID); + var zone = client.getCampaign().getZone(zoneGUID); zone.addTopology(area, topologyType); MapTool.getFrame().getZoneRenderer(zoneGUID).repaint(); @@ -1006,10 +1034,9 @@ private void handle(AddTopologyMsg addTopologyMsg) { private void handle(BootPlayerMsg bootPlayerMsg) { String playerName = bootPlayerMsg.getPlayerName(); - if (MapTool.getPlayer().getName().equals(playerName)) + if (client.getPlayer().getName().equals(playerName)) EventQueue.invokeLater( () -> { - ServerDisconnectHandler.disconnectExpected = true; AppActions.disconnectFromServer(); MapTool.showInformation("You have been booted from the server."); }); @@ -1021,7 +1048,7 @@ private void handle(UpdatePlayerStatusMsg updatePlayerStatusMsg) { var loaded = updatePlayerStatusMsg.getLoaded(); Player player = - MapTool.getPlayerList().stream() + client.getPlayerList().stream() .filter(x -> x.getName().equals(playerName)) .findFirst() .orElse(null); diff --git a/src/main/java/net/rptools/maptool/client/MapTool.java b/src/main/java/net/rptools/maptool/client/MapTool.java index e0f3a378d7..55fb505a48 100644 --- a/src/main/java/net/rptools/maptool/client/MapTool.java +++ b/src/main/java/net/rptools/maptool/client/MapTool.java @@ -14,8 +14,6 @@ */ package net.rptools.maptool.client; -import static net.rptools.maptool.model.player.PlayerDatabaseFactory.PlayerDatabaseType.PERSONAL_SERVER; - import com.jidesoft.plaf.LookAndFeelFactory; import com.jidesoft.plaf.UIDefaultsLookup; import com.jidesoft.plaf.basic.ThemePainter; @@ -43,12 +41,14 @@ import java.net.URL; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; -import java.text.MessageFormat; import java.util.*; -import java.util.concurrent.ExecutionException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.imageio.ImageIO; import javax.swing.*; import javax.swing.plaf.FontUIResource; +import net.rptools.clientserver.ConnectionFactory; +import net.rptools.clientserver.simple.connection.DirectConnection; import net.rptools.lib.BackupManager; import net.rptools.lib.DebugStream; import net.rptools.lib.FileUtil; @@ -56,10 +56,9 @@ import net.rptools.lib.image.ThumbnailManager; import net.rptools.lib.net.RPTURLStreamHandlerFactory; import net.rptools.lib.sound.SoundManager; +import net.rptools.maptool.client.MapToolConnection.HandshakeCompletionObserver; import net.rptools.maptool.client.events.ChatMessageAdded; -import net.rptools.maptool.client.events.PlayerConnected; -import net.rptools.maptool.client.events.PlayerDisconnected; -import net.rptools.maptool.client.events.ServerStopped; +import net.rptools.maptool.client.events.ServerDisconnected; import net.rptools.maptool.client.functions.UserDefinedMacroFunctions; import net.rptools.maptool.client.swing.MapToolEventQueue; import net.rptools.maptool.client.swing.NoteFrame; @@ -71,7 +70,6 @@ import net.rptools.maptool.client.ui.OSXAdapter; import net.rptools.maptool.client.ui.logger.LogConsoleFrame; import net.rptools.maptool.client.ui.sheet.stats.StatSheetListener; -import net.rptools.maptool.client.ui.startserverdialog.StartServerDialogPreferences; import net.rptools.maptool.client.ui.theme.Icons; import net.rptools.maptool.client.ui.theme.RessourceManager; import net.rptools.maptool.client.ui.theme.ThemeSupport; @@ -89,13 +87,14 @@ import net.rptools.maptool.model.TextMessage; import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.ZoneFactory; +import net.rptools.maptool.model.library.LibraryManager; import net.rptools.maptool.model.library.url.LibraryURLStreamHandler; import net.rptools.maptool.model.player.LocalPlayer; +import net.rptools.maptool.model.player.PersonalServerPlayerDatabase; import net.rptools.maptool.model.player.Player; -import net.rptools.maptool.model.player.PlayerDatabase; import net.rptools.maptool.model.player.PlayerDatabaseFactory; import net.rptools.maptool.model.player.PlayerZoneListener; -import net.rptools.maptool.model.player.Players; +import net.rptools.maptool.model.player.ServerSidePlayerDatabase; import net.rptools.maptool.model.zones.TokensAdded; import net.rptools.maptool.model.zones.TokensRemoved; import net.rptools.maptool.model.zones.ZoneAdded; @@ -108,11 +107,8 @@ import net.rptools.maptool.transfer.AssetTransferManager; import net.rptools.maptool.util.MessageUtil; import net.rptools.maptool.util.StringUtil; -import net.rptools.maptool.util.UPnPUtil; import net.rptools.maptool.util.UserJvmOptions; -import net.rptools.maptool.webapi.MTWebAppServer; import net.rptools.parser.ParserException; -import net.tsc.servicediscovery.ServiceAnnouncer; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; @@ -154,33 +150,23 @@ public class MapTool { private static String vendor = "RPTools!"; // Default, will get from JAR Manifest during normal // runtime - private static Campaign campaign; - - private static List playerList; - private static LocalPlayer player; private static PlayerZoneListener playerZoneListener; private static ZoneLoadedListener zoneLoadedListener; - private static MapToolConnection conn; - private static ClientMessageHandler handler; private static JMenuBar menuBar; private static MapToolFrame clientFrame; private static NoteFrame profilingNoteFrame; private static LogConsoleFrame logConsoleFrame; private static MapToolServer server; - private static ServerCommand serverCommand; - private static ServerPolicy serverPolicy; + private static MapToolClient client; private static BackupManager backupManager; private static AssetTransferManager assetTransferManager; - private static ServiceAnnouncer announcer; private static AutoSaveManager autoSaveManager; private static TaskBarFlasher taskbarFlasher; private static MapToolLineParser parser = new MapToolLineParser(); private static String lastWhisperer; - private static final MTWebAppServer webAppServer = new MTWebAppServer(); - // Jamz: To support new command line parameters for multi-monitor support & enhanced PrintStream private static boolean debug = false; private static int graphicsMonitor = -1; @@ -191,6 +177,20 @@ public class MapTool { private static int windowY = -1; private static String loadCampaignOnStartPath = ""; + static { + try { + var connections = DirectConnection.create("local"); + var playerDB = new PersonalServerPlayerDatabase(new LocalPlayer()); + var campaign = CampaignFactory.createBasicCampaign(); + var policy = new ServerPolicy(); + + server = new MapToolServer("", new Campaign(campaign), null, false, policy, playerDB); + client = new MapToolClient(server, campaign, playerDB.getPlayer(), connections.clientSide()); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new RuntimeException("Unable to create default personal server", e); + } + } + public static Dimension getThumbnailSize() { return THUMBNAIL_SIZE; } @@ -536,19 +536,6 @@ public static void playSound(String eventId) { } } - public static void updateServerPolicy() { - updateServerPolicy(serverPolicy); - } - - public static void updateServerPolicy(ServerPolicy policy) { - setServerPolicy(policy); - - // Give everyone the new policy - if (serverCommand != null) { - serverCommand.setServerPolicy(policy); - } - } - public static boolean isInFocus() { // TODO: This should probably also check owned windows return getFrame().isFocused(); @@ -653,6 +640,7 @@ private static void moveToMonitor(JFrame frame, int monitor, boolean maximize) { private static void initialize() { // First time AppSetup.install(); + LibraryManager.init(); // Clean up after ourselves FileUtil.delete(AppUtil.getAppHome("tmp"), 2); @@ -670,22 +658,26 @@ private static void initialize() { assetTransferManager = new AssetTransferManager(); assetTransferManager.addConsumerListener(new AssetTransferHandler()); - playerList = new ArrayList<>(); - - handler = new ClientMessageHandler(); - setClientFrame(new MapToolFrame(menuBar)); + taskbarFlasher = new TaskBarFlasher(clientFrame); - serverCommand = new ServerCommandClientImpl(); + // Make sure the user sees something right away so that they aren't staring at a black screen. + // Technically this call does too much, but since it is a blank campaign it's okay. + setCampaign(client.getCampaign()); try { - player = new LocalPlayer("", Player.Role.GM, ServerConfig.getPersonalServerGMPassword()); playerZoneListener = new PlayerZoneListener(); zoneLoadedListener = new ZoneLoadedListener(); + Campaign cmpgn = CampaignFactory.createBasicCampaign(); - // This was previously being done in the server thread and didn't always get done - // before the campaign was accessed by the postInitialize() method below. - setCampaign(cmpgn); + // Set the Topology drawing mode to the last mode used for convenience + // Should only be one zone, but let's cover our bases. + cmpgn.getZones().forEach(zone -> zone.setTopologyTypes(AppPreferences.getTopologyTypes())); + + // Stop the pre-init client/server. + disconnect(); + stopServer(); + startPersonalServer(cmpgn); } catch (Exception e) { MapTool.showError("While starting personal server", e); @@ -745,11 +737,11 @@ public static boolean isDevelopment() { } public static ServerPolicy getServerPolicy() { - return serverPolicy; + return client.getServerPolicy(); } - public static ServerCommand serverCommand() { - return serverCommand; + public static @Nonnull ServerCommand serverCommand() { + return client.getServerCommand(); } /** @@ -759,39 +751,6 @@ public static MapToolServer getServer() { return server; } - public static void addPlayer(Player player) { - if (!playerList.contains(player)) { - playerList.add(player); - new MapToolEventBus().getMainEventBus().post(new PlayerConnected(player)); - new Players().playerSignedIn(player); - - // LATER: Make this non-anonymous - playerList.sort((arg0, arg1) -> arg0.getName().compareToIgnoreCase(arg1.getName())); - - if (!player.equals(MapTool.getPlayer())) { - String msg = - MessageFormat.format(I18N.getText("msg.info.playerConnected"), player.getName()); - addLocalMessage(MessageUtil.getFormattedSystemMsg(msg)); - } - } - } - - public static void removePlayer(Player player) { - if (player == null) { - return; - } - playerList.remove(player); - new MapToolEventBus().getMainEventBus().post(new PlayerDisconnected(player)); - - new Players().playerSignedOut(player); - - if (MapTool.getPlayer() != null && !player.equals(MapTool.getPlayer())) { - String msg = - MessageFormat.format(I18N.getText("msg.info.playerDisconnected"), player.getName()); - addLocalMessage(MessageUtil.getFormattedSystemMsg(msg)); - } - } - /** * These are the messages that originate from the server * @@ -916,10 +875,7 @@ public static void addGlobalMessage(String message, List targets) { } public static Campaign getCampaign() { - if (campaign == null) { - campaign = CampaignFactory.createBasicCampaign(); - } - return campaign; + return client.getCampaign(); } public static MapToolLineParser getParser() { @@ -931,17 +887,15 @@ public static void setCampaign(Campaign campaign) { } public static void setCampaign(Campaign campaign, GUID defaultRendererId) { + campaign = Objects.requireNonNullElseGet(campaign, Campaign::new); + // Load up the new - MapTool.campaign = campaign; + client.setCampaign(campaign); ZoneRenderer currRenderer = null; clientFrame.clearZoneRendererList(); clientFrame.getInitiativePanel().setZone(null); clientFrame.clearTokenTree(); - if (campaign == null) { - clientFrame.setCurrentZoneRenderer(null); - return; - } // Install new campaign for (Zone zone : campaign.getZones()) { @@ -970,10 +924,6 @@ public static void setCampaign(Campaign campaign, GUID defaultRendererId) { UserDefinedMacroFunctions.getInstance().handleCampaignLoadMacroEvent(); } - public static void setServerPolicy(ServerPolicy policy) { - serverPolicy = policy; - } - public static AssetTransferManager getAssetTransferManager() { return assetTransferManager; } @@ -982,69 +932,80 @@ public static AssetTransferManager getAssetTransferManager() { * Start the server from a campaign file and various settings. * * @param id the id of the server for announcement. - * @param config the server configuration. + * @param config the server configuration. Set to null only for a personal server. * @param policy the server policy configuration to use. * @param campaign the campaign. * @param playerDatabase the player database to use for the connection. - * @param copyCampaign should the campaign be a copy of the one provided. - * @throws IOException if new MapToolServer fails. + * @throws IOException if we fail to start the new server. In this case, the new client and server + * will be available via {@link #getServer()} and {@link #getClient()}, but neither will be in + * a started state. */ public static void startServer( String id, - ServerConfig config, + @Nullable ServerConfig config, + boolean useUPnP, ServerPolicy policy, Campaign campaign, - PlayerDatabase playerDatabase, - boolean copyCampaign) + ServerSidePlayerDatabase playerDatabase, + LocalPlayer player) throws IOException { - if (server != null) { - Thread.dumpStack(); + if (server != null && server.getState() == MapToolServer.State.Started) { + log.error("A server is already running.", new Exception()); showError("msg.error.alreadyRunningServer"); return; } assetTransferManager.flush(); - // TODO: the client and server campaign MUST be different objects. - // Figure out a better init method - server = new MapToolServer(config, policy, playerDatabase); + var connections = DirectConnection.create("local"); + server = new MapToolServer(id, new Campaign(campaign), config, useUPnP, policy, playerDatabase); + client = new MapToolClient(server, campaign, player, connections.clientSide()); - serverPolicy = server.getPolicy(); - if (copyCampaign) { - server.setCampaign(new Campaign(campaign)); // copy of FoW depends on server policies - } else { - server.setCampaign(campaign); + if (!server.isPersonalServer()) { + getFrame().getConnectionPanel().startHosting(); } - if (announcer != null) { - announcer.stop(); - } - // Don't announce personal servers - if (!config.isPersonalServer()) { - announcer = - new ServiceAnnouncer(id, server.getConfig().getPort(), AppConstants.SERVICE_GROUP); - announcer.start(); - } + setUpClient(client); + client + .getConnection() + .onCompleted( + (success) -> { + if (success) { + // connected + EventQueue.invokeLater( + () -> { + MapTool.getFrame() + .getConnectionStatusPanel() + .setStatus(ConnectionStatusPanel.Status.server); + }); + } else { + // This should never happen. But if it does, just tear everything back down. + server.stop(); + } + }); - // Registered ? - if (config.isServerRegistered() && !config.isPersonalServer()) { - try { - MapToolRegistry.RegisterResponse result = - MapToolRegistry.getInstance() - .registerInstance(config.getServerName(), config.getPort(), config.getUseWebRTC()); - if (result == MapToolRegistry.RegisterResponse.NAME_EXISTS) { - MapTool.showError("msg.error.alreadyRegistered"); - } - // TODO: I don't like this - } catch (Exception e) { - MapTool.showError("msg.error.failedCannotRegisterServer", e); - } + server.start(); + try { + client.start(); + } catch (IOException e) { + // Oof. Server started but client can't. + server.stop(); + throw e; } - - if (MapTool.isHostingServer()) { - getFrame().getConnectionPanel().startHosting(); + if (!server.isPersonalServer()) { + MapTool.addLocalMessage( + MessageUtil.getFormattedSystemMsg(I18N.getText("msg.info.startServer"))); } - server.start(); + + // Adopt the local connection, no handshake required. + connections.serverSide().open(); + server.addLocalConnection(connections.serverSide(), player); + // Update the client, including running onCampaignLoad. + setCampaign( + client.getCampaign(), + Optional.ofNullable(clientFrame.getCurrentZoneRenderer()) + .map(zr -> zr.getZone().getId()) + .orElse(null)); } public static ThumbnailManager getThumbnailManager() { @@ -1055,23 +1016,27 @@ public static ThumbnailManager getThumbnailManager() { return thumbnailManager; } + /** + * Shutdown the current server. + * + *

The client must have already been disconnected if necessary. + */ public static void stopServer() { if (server == null) { return; } - disconnect(); server.stop(); - server = null; getFrame().getConnectionPanel().stopHosting(); } public static List getPlayerList() { - return playerList; + return client.getPlayerList(); } /** Returns the list of non-gm names. */ public static List getNonGMs() { + var playerList = client.getPlayerList(); List nonGMs = new ArrayList<>(playerList.size()); playerList.forEach( player -> { @@ -1084,6 +1049,7 @@ public static List getNonGMs() { /** Returns the list of gm names. */ public static List getGMs() { + var playerList = client.getPlayerList(); List gms = new ArrayList<>(playerList.size()); playerList.forEach( player -> { @@ -1094,22 +1060,6 @@ public static List getGMs() { return gms; } - /** - * checks if a specific player is connected to the game. - * - * @param player The name of the player to check. - * @return {@code true} if the player is connected otherwise {@code false}. - */ - public static boolean isPlayerConnected(String player) { - for (int i = 0; i < playerList.size(); i++) { - Player p = playerList.get(i); - if (p.getName().equalsIgnoreCase(player)) { - return true; - } - } - return false; - } - public static void removeZone(Zone zone) { MapTool.serverCommand().removeZone(zone.getId()); MapTool.getFrame().removeZoneRenderer(MapTool.getFrame().getZoneRenderer(zone.getId())); @@ -1155,69 +1105,59 @@ public static void addZone(Zone zone, boolean changeZone) { } } + public static MapToolClient getClient() { + return client; + } + public static LocalPlayer getPlayer() { - return player; + return client.getPlayer(); } public static void startPersonalServer(Campaign campaign) - throws IOException, - NoSuchAlgorithmException, - InvalidKeySpecException, - ExecutionException, - InterruptedException { - ServerConfig config = ServerConfig.createPersonalServerConfig(); - - PlayerDatabaseFactory.setCurrentPlayerDatabase(PERSONAL_SERVER); - PlayerDatabase playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); - MapTool.startServer(null, config, new ServerPolicy(), campaign, playerDatabase, false); - - String username = AppPreferences.getDefaultUserName(); - LocalPlayer localPlayer = (LocalPlayer) playerDatabase.getPlayer(username); - // Connect to server - MapTool.createConnection( - config, - localPlayer, - () -> { - // connecting - MapTool.getFrame() - .getConnectionStatusPanel() - .setStatus(ConnectionStatusPanel.Status.server); - }); + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + var player = new LocalPlayer(); + startServer( + "", + null, + false, + new ServerPolicy(), + campaign, + PlayerDatabaseFactory.getPersonalServerPlayerDatabase(player), + player); } - public static void createConnection(ServerConfig config, LocalPlayer player, Runnable onCompleted) - throws IOException, ExecutionException, InterruptedException { - MapTool.player = player; + private static void setUpClient(MapToolClient client) { MapTool.getFrame().getCommandPanel().clearAllIdentities(); - MapToolConnection clientConn = new MapToolConnection(config, player); - + MapToolConnection clientConn = client.getConnection(); clientConn.addActivityListener(clientFrame.getActivityMonitor()); - clientConn.addDisconnectHandler(new ServerDisconnectHandler()); - - clientConn.setOnCompleted( - () -> { - clientConn.addMessageHandler(handler); - // LATER: I really, really, really don't like this startup pattern - if (clientConn.isAlive()) { - conn = clientConn; - } - clientFrame.getLookupTablePanel().updateView(); - clientFrame.getInitiativePanel().updateView(); - onCompleted.run(); + clientConn.onCompleted( + (success) -> { + EventQueue.invokeLater( + () -> { + clientFrame.getLookupTablePanel().updateView(); + clientFrame.getInitiativePanel().updateView(); + }); }); - - clientConn.start(); } - public static void closeConnection() throws IOException { - if (conn != null) { - conn.close(); + public static void connectToRemoteServer( + ServerConfig config, LocalPlayer player, HandshakeCompletionObserver onCompleted) + throws IOException { + if (server != null && server.getState() == MapToolServer.State.Started) { + log.error("A local server is still running.", new Exception()); + showError("msg.error.stillRunningServer"); + return; } - } - public static MapToolConnection getConnection() { - return conn; + var connection = ConnectionFactory.getInstance().createConnection(player.getName(), config); + + server = null; + client = new MapToolClient(player, connection); + setUpClient(client); + client.getConnection().onCompleted(onCompleted); + + client.start(); } /** returns the current locale code. */ @@ -1227,54 +1167,23 @@ public static String getLanguage() { /** returns whether the player is using a personal server. */ public static boolean isPersonalServer() { - return server != null && server.getConfig().isPersonalServer(); + return server != null && server.isPersonalServer(); } /** returns whether the player is hosting a server - personal servers do not count. */ public static boolean isHostingServer() { - return server != null && !server.getConfig().isPersonalServer(); + return server != null && !server.isPersonalServer(); } public static void disconnect() { - // Close UPnP port mapping if used - StartServerDialogPreferences serverProps = new StartServerDialogPreferences(); - if (serverProps.getUseUPnP()) { - int port = serverProps.getPort(); - UPnPUtil.closePort(port); - } - boolean isPersonalServer = isPersonalServer(); - - if (announcer != null) { - announcer.stop(); - announcer = null; - } - - // Unregister ourselves - if (server != null && server.getConfig().isServerRegistered() && !isPersonalServer) { - try { - MapToolRegistry.getInstance().unregisterInstance(); - } catch (Throwable t) { - MapTool.showError("While unregistering server instance", t); - } - } - - try { - if (conn != null && conn.isAlive()) { - conn.close(); - } - } catch (IOException ioe) { - // This isn't critical, we're closing it anyway - log.debug("While closing connection", ioe); - } - - new MapToolEventBus().getMainEventBus().post(new ServerStopped()); - playerList.clear(); + client.close(); + new MapToolEventBus().getMainEventBus().post(new ServerDisconnected()); MapTool.getFrame() .getConnectionStatusPanel() .setStatus(ConnectionStatusPanel.Status.disconnected); - if (!isPersonalServer) { + if (!isPersonalServer()) { addLocalMessage(MessageUtil.getFormattedSystemMsg(I18N.getText("msg.info.disconnected"))); } } @@ -1365,18 +1274,10 @@ private static void postInitialize() { // fire up autosaves getAutoSaveManager().start(); - taskbarFlasher = new TaskBarFlasher(clientFrame); - // Jamz: After preferences are loaded, Asset Tree and ImagePanel are out of sync, // so after frame is all done loading we sync them back up. MapTool.getFrame().getAssetPanel().getAssetTree().initialize(); - // Set the Topology drawing mode to the last mode used for convenience - MapTool.getFrame() - .getCurrentZoneRenderer() - .getZone() - .setTopologyTypes(AppPreferences.getTopologyTypes()); - // Register the instance that will listen for token hover events and create a stat sheet. new MapToolEventBus().getMainEventBus().register(new StatSheetListener()); new MapToolEventBus().getMainEventBus().register(new TokenHoverListener()); @@ -1442,26 +1343,6 @@ public static boolean useToolTipsForUnformatedRolls() { } } - public static MTWebAppServer getWebAppServer() { - return webAppServer; - } - - public static void startWebAppServer(final int port) { - try { - Thread webAppThread = - new Thread( - () -> { - webAppServer.setPort(port); - webAppServer.startServer(); - }); - - webAppThread.start(); - } catch (Exception e) { // TODO: This needs to be logged - System.out.println("Unable to start web server"); - e.printStackTrace(); - } - } - public static String getClientId() { return clientId; } @@ -1483,10 +1364,8 @@ public void run() { e.printStackTrace(); } - ServerCommand command = serverCommand; - if (command != null) { - command.heartbeat(getPlayer().getName()); - } + ServerCommand command = client.getServerCommand(); + command.heartbeat(getPlayer().getName()); } } } @@ -1838,6 +1717,7 @@ public static void main(String[] args) { EventQueue.invokeLater( () -> { initialize(); + EventQueue.invokeLater( () -> { clientFrame.setVisible(true); diff --git a/src/main/java/net/rptools/maptool/client/MapToolClient.java b/src/main/java/net/rptools/maptool/client/MapToolClient.java new file mode 100644 index 0000000000..62c3e4a837 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/MapToolClient.java @@ -0,0 +1,313 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code 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. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client; + +import java.awt.EventQueue; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; +import net.rptools.clientserver.simple.connection.Connection; +import net.rptools.maptool.client.events.PlayerConnected; +import net.rptools.maptool.client.events.PlayerDisconnected; +import net.rptools.maptool.events.MapToolEventBus; +import net.rptools.maptool.language.I18N; +import net.rptools.maptool.model.Campaign; +import net.rptools.maptool.model.CampaignFactory; +import net.rptools.maptool.model.campaign.CampaignManager; +import net.rptools.maptool.model.player.LocalPlayer; +import net.rptools.maptool.model.player.Player; +import net.rptools.maptool.model.player.PlayerDatabase; +import net.rptools.maptool.model.player.PlayerDatabaseFactory; +import net.rptools.maptool.server.ClientHandshake; +import net.rptools.maptool.server.MapToolServer; +import net.rptools.maptool.server.ServerCommand; +import net.rptools.maptool.server.ServerPolicy; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * The client side of a client-server channel. + * + *

This has nothing to do with the GUI, but represents those parts of the client that are needed + * to interact with a server. Most of this used to exist as global state in {@link + * net.rptools.maptool.client.MapTool} and elsewhere. + */ +public class MapToolClient { + private static final Logger log = LogManager.getLogger(MapToolClient.class); + + public enum State { + New, + Started, + Connected, + Closed + } + + private final MapToolServer localServer; + private final LocalPlayer player; + private final PlayerDatabase playerDatabase; + + /** Case-insensitive ordered set of player names. */ + private final List playerList; + + private final MapToolConnection conn; + private Campaign campaign; + private ServerPolicy serverPolicy; + private final ServerCommand serverCommand; + private State currentState = State.New; + + private MapToolClient( + @Nullable MapToolServer localServer, + Campaign campaign, + LocalPlayer player, + Connection connection, + ServerPolicy policy, + PlayerDatabase playerDatabase) { + this.localServer = localServer; + this.campaign = campaign; + this.player = player; + this.playerDatabase = playerDatabase; + this.playerList = new ArrayList<>(); + this.serverPolicy = new ServerPolicy(policy); + + this.conn = + new MapToolConnection( + connection, player, localServer == null ? new ClientHandshake(this, connection) : null); + + this.serverCommand = new ServerCommandClientImpl(this); + + this.conn.addDisconnectHandler(this::onDisconnect); + this.conn.onCompleted( + (success) -> { + if (!success) { + // Failed handshake. Disconnect from the server, but treat it as unexpected. + this.conn.close(); + return; + } + + if (transitionToState(State.Started, State.Connected)) { + this.conn.addMessageHandler(new ClientMessageHandler(this)); + } + }); + } + + /** Creates a client for a local server, whether personal or hosted. */ + public MapToolClient( + MapToolServer localServer, Campaign campaign, LocalPlayer player, Connection connection) { + this( + localServer, + campaign, + player, + connection, + localServer.getPolicy(), + localServer.getPlayerDatabase()); + } + + /** + * Creates a client for use with a remote hosted server. + * + * @param player The player connecting to the server. + */ + public MapToolClient(LocalPlayer player, Connection connection) { + this( + null, + new Campaign(), + player, + connection, + new ServerPolicy(), + PlayerDatabaseFactory.getLocalPlayerDatabase(player)); + } + + /** + * Transition from any state except {@code newState} to {@code newState}. + * + * @param newState The new state to set. + */ + private boolean transitionToState(State newState) { + if (currentState == newState) { + log.warn( + "Failed to transition to state {} because that is already the current state", newState); + return false; + } else { + currentState = newState; + return true; + } + } + + /** + * Transition from {@code expectedState} to {@code newState}. + * + * @param expectedState The state to transition from + * @param newState The new state to set. + */ + private boolean transitionToState(State expectedState, State newState) { + if (currentState != expectedState) { + log.warn( + "Failed to transition from state {} to state {} because the current state is actually {}", + expectedState, + newState, + currentState); + return false; + } else { + currentState = newState; + return true; + } + } + + public State getState() { + return currentState; + } + + public void start() throws IOException { + if (transitionToState(State.New, State.Started)) { + try { + conn.start(); + } catch (IOException e) { + // Make sure we're in a reasonable state before propagating. + log.error("Failed to start client", e); + transitionToState(State.Closed); + throw e; + } + } + } + + public void close() { + if (transitionToState(State.Closed)) { + if (conn.isAlive()) { + conn.close(); + } + + playerList.clear(); + } + } + + public ServerCommand getServerCommand() { + return serverCommand; + } + + public LocalPlayer getPlayer() { + return player; + } + + public List getPlayerList() { + return Collections.unmodifiableList(playerList); + } + + public void addPlayer(Player player) { + if (!playerList.contains(player)) { + playerList.add(player); + new MapToolEventBus().getMainEventBus().post(new PlayerConnected(player)); + playerDatabase.playerSignedIn(player); + + playerList.sort((arg0, arg1) -> arg0.getName().compareToIgnoreCase(arg1.getName())); + } + } + + public void removePlayer(Player player) { + playerList.remove(player); + new MapToolEventBus().getMainEventBus().post(new PlayerDisconnected(player)); + playerDatabase.playerSignedOut(player); + } + + public boolean isPlayerConnected(String playerName) { + return playerList.stream().anyMatch(p -> p.getName().equalsIgnoreCase(playerName)); + } + + public PlayerDatabase getPlayerDatabase() { + return playerDatabase; + } + + public MapToolConnection getConnection() { + return conn; + } + + /** + * @return A copy of the client's server policy. + */ + public ServerPolicy getServerPolicy() { + return new ServerPolicy(serverPolicy); + } + + /** + * Sets the client's server policy. + * + *

If this also needs to be updated remotely, call {@link + * net.rptools.maptool.server.ServerCommand#setServerPolicy(net.rptools.maptool.server.ServerPolicy)} + * as well. + * + * @param serverPolicy The new policy to set. + */ + public void setServerPolicy(ServerPolicy serverPolicy) { + this.serverPolicy = new ServerPolicy(serverPolicy); + } + + public Campaign getCampaign() { + return this.campaign; + } + + public void setCampaign(Campaign campaign) { + this.campaign = campaign; + } + + private void onDisconnect(Connection connection) { + /* + * Three main cases: + * 1. Expected disconnect. This will be part of a broader shutdown sequence and we don't need to + * do anything to clean up client or server state. + * 2. Unexpected disconnect for remote server. Common case due to remote server shutdown or + * other lost connection. We need to clean up the connection, show an error to the user, and + * start a new personal server with a blank campaign. + * 3. Unexpected disconnect for local server. A rare case where we lost connection without + * shutting down the server. We need to clean up the connection, stop the server, show an + * error to the user, and start a new personal server with the current campaign. + */ + var disconnectExpected = currentState == State.Closed; + + if (!disconnectExpected) { + // Keep any local server campaign around in the new personal server. + final var newPersonalServerCampaign = + localServer == null ? CampaignFactory.createBasicCampaign() : localServer.getCampaign(); + + // Make sure the connection state is cleaned up since we can't count on it having been done. + MapTool.disconnect(); + MapTool.stopServer(); + + EventQueue.invokeLater( + () -> { + var errorText = I18N.getText("msg.error.server.disconnected"); + var connectionError = connection.getError(); + var errorMessage = + errorText + (connectionError != null ? (": " + connectionError) : ""); + MapTool.showError(errorMessage); + + // hide map so player doesn't get a brief GM view + MapTool.getFrame().setCurrentZoneRenderer(null); + MapTool.getFrame().getToolbarPanel().getMapselect().setVisible(true); + MapTool.getFrame().getAssetPanel().enableAssets(); + new CampaignManager().clearCampaignData(); + MapTool.getFrame().getToolbarPanel().setTokenSelectionGroupEnabled(true); + + try { + MapTool.startPersonalServer(newPersonalServerCampaign); + } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) { + MapTool.showError(I18N.getText("msg.error.server.cantrestart"), e); + } + }); + } + } +} diff --git a/src/main/java/net/rptools/maptool/client/MapToolConnection.java b/src/main/java/net/rptools/maptool/client/MapToolConnection.java index 320f5a0369..f73ec26dd0 100644 --- a/src/main/java/net/rptools/maptool/client/MapToolConnection.java +++ b/src/main/java/net/rptools/maptool/client/MapToolConnection.java @@ -15,14 +15,13 @@ package net.rptools.maptool.client; import java.io.IOException; -import java.util.concurrent.ExecutionException; -import net.rptools.clientserver.ConnectionFactory; +import java.util.ArrayList; +import java.util.List; +import net.rptools.clientserver.simple.DisconnectHandler; import net.rptools.clientserver.simple.connection.Connection; import net.rptools.maptool.client.ui.ActivityMonitorPanel; import net.rptools.maptool.model.player.LocalPlayer; -import net.rptools.maptool.server.ClientHandshake; import net.rptools.maptool.server.Handshake; -import net.rptools.maptool.server.ServerConfig; import net.rptools.maptool.server.proto.Message; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -31,51 +30,59 @@ * @author trevor */ public class MapToolConnection { + public interface HandshakeCompletionObserver { + void onComplete(boolean success); + } /** Instance used for log messages. */ private static final Logger log = LogManager.getLogger(MapToolConnection.class); private final LocalPlayer player; - private Connection connection; - private Handshake handshake; - private Runnable onCompleted; - - public MapToolConnection(ServerConfig config, LocalPlayer player) throws IOException { + private final Connection connection; + private final Handshake handshake; + private final List onCompleted; - this.connection = ConnectionFactory.getInstance().createConnection(player.getName(), config); + public MapToolConnection(Connection connection, LocalPlayer player, Handshake handshake) { + this.connection = connection; this.player = player; - this.handshake = new ClientHandshake(connection, player); - onCompleted = () -> {}; + this.handshake = handshake; + onCompleted = new ArrayList<>(); } - public void setOnCompleted(Runnable onCompleted) { - if (onCompleted == null) this.onCompleted = () -> {}; - else this.onCompleted = onCompleted; + public void onCompleted(HandshakeCompletionObserver onCompleted) { + this.onCompleted.add(onCompleted); } - public void start() throws IOException, ExecutionException, InterruptedException { - connection.addMessageHandler(handshake); - handshake.addObserver( - (ignore) -> { - connection.removeMessageHandler(handshake); - if (handshake.isSuccessful()) { - onCompleted.run(); - } else { - // For client side only show the error message as its more likely to make sense - // for players, the exception is logged just in case more info is required - var exception = handshake.getException(); + public void start() throws IOException { + if (handshake == null) { + // No handshake required. Transition immediately to connected. + connection.open(); + for (final var callback : onCompleted) { + callback.onComplete(true); + } + } else { + handshake.whenComplete( + (result, exception) -> { if (exception != null) { + // For client side only show the error message as its more likely to make sense + // for players, the exception is logged just in case more info is required log.warn(exception); + MapTool.showError(exception.getMessage()); + connection.close(); + for (final var callback : onCompleted) { + callback.onComplete(false); + } + } else { + for (final var callback : onCompleted) { + callback.onComplete(true); + } } - MapTool.showError(handshake.getErrorMessage()); - connection.close(); - onCompleted.run(); - AppActions.disconnectFromServer(); - } - }); - // this triggers the handshake from the server side - connection.open(); - handshake.startHandshake(); + }); + + // this triggers the handshake from the server side + connection.open(); + handshake.startHandshake(); + } } public void addMessageHandler(ClientMessageHandler handler) { @@ -86,7 +93,7 @@ public void addActivityListener(ActivityMonitorPanel activityMonitor) { connection.addActivityListener(activityMonitor); } - public void addDisconnectHandler(ServerDisconnectHandler serverDisconnectHandler) { + public void addDisconnectHandler(DisconnectHandler serverDisconnectHandler) { connection.addDisconnectHandler(serverDisconnectHandler); } @@ -94,12 +101,12 @@ public boolean isAlive() { return connection.isAlive(); } - public void close() throws IOException { + public void close() { connection.close(); } public void sendMessage(Message msg) { - log.debug(player.getName() + " sent " + msg.getMessageTypeCase()); + log.debug("{} sent {}", player.getName(), msg.getMessageTypeCase()); connection.sendMessage(msg.toByteArray()); } } diff --git a/src/main/java/net/rptools/maptool/client/MapToolVariableResolver.java b/src/main/java/net/rptools/maptool/client/MapToolVariableResolver.java index fc5c7d508a..3c93a0aedd 100644 --- a/src/main/java/net/rptools/maptool/client/MapToolVariableResolver.java +++ b/src/main/java/net/rptools/maptool/client/MapToolVariableResolver.java @@ -343,10 +343,8 @@ public Set getVariables() { protected void updateTokenProperty(Token token, String varname, String value) { // this logic allows unit tests to execute MT script that changes token properties // there should be no other context where we have no server of any kind - if (MapTool.serverCommand() != null) - MapTool.serverCommand() - .updateTokenProperty(tokenInContext, Token.Update.setProperty, varname, value); - else token.setProperty(varname, value); + MapTool.serverCommand() + .updateTokenProperty(tokenInContext, Token.Update.setProperty, varname, value); } @Override diff --git a/src/main/java/net/rptools/maptool/client/ServerCommandClientImpl.java b/src/main/java/net/rptools/maptool/client/ServerCommandClientImpl.java index 33722cd2e4..e1dbb46f86 100644 --- a/src/main/java/net/rptools/maptool/client/ServerCommandClientImpl.java +++ b/src/main/java/net/rptools/maptool/client/ServerCommandClientImpl.java @@ -21,7 +21,6 @@ import java.util.Arrays; import java.util.List; import java.util.Set; -import java.util.concurrent.LinkedBlockingQueue; import java.util.stream.Collectors; import net.rptools.lib.MD5Key; import net.rptools.maptool.client.functions.ExecFunction; @@ -51,14 +50,14 @@ * the {@link ServerMessageHandler ServerMessageHandler} */ public class ServerCommandClientImpl implements ServerCommand { + private static final Logger log = LogManager.getLogger(ServerCommandClientImpl.class); + private final MapToolClient client; private final TimedEventQueue movementUpdateQueue = new TimedEventQueue(100); - private final LinkedBlockingQueue assetRetrieveQueue = new LinkedBlockingQueue(); - private static final Logger log = LogManager.getLogger(ServerCommandClientImpl.class); - public ServerCommandClientImpl() { + public ServerCommandClientImpl(MapToolClient client) { + this.client = client; movementUpdateQueue.start(); - // new AssetRetrievalThread().start(); } public void heartbeat(String data) { @@ -88,7 +87,7 @@ public void setCampaign(Campaign campaign) { } public void setCampaignName(String name) { - MapTool.getCampaign().setName(name); + client.getCampaign().setName(name); MapTool.getFrame().setTitle(); var msg = SetCampaignNameMsg.newBuilder().setName(name); makeServerCall(Message.newBuilder().setSetCampaignNameMsg(msg).build()); @@ -166,7 +165,7 @@ public void restoreZoneView(GUID zoneGUID) { } public void editToken(GUID zoneGUID, Token token) { - MapTool.getCampaign().getZone(zoneGUID).editToken(token); + client.getCampaign().getZone(zoneGUID).editToken(token); var msg = EditTokenMsg.newBuilder().setZoneGuid(zoneGUID.toString()).setToken(token.toDto()); makeServerCall(Message.newBuilder().setEditTokenMsg(msg).build()); } @@ -175,7 +174,7 @@ public void putToken(GUID zoneGUID, Token token) { // Hack to generate zone event. All functions that update tokens call this method // after changing the token. But they don't tell the zone about it so classes // waiting for the zone change event don't get it. - MapTool.getCampaign().getZone(zoneGUID).putToken(token); + client.getCampaign().getZone(zoneGUID).putToken(token); var msg = PutTokenMsg.newBuilder().setZoneGuid(zoneGUID.toString()).setToken(token.toDto()); makeServerCall(Message.newBuilder().setPutTokenMsg(msg).build()); } @@ -183,7 +182,7 @@ public void putToken(GUID zoneGUID, Token token) { @Override public void removeToken(GUID zoneGUID, GUID tokenGUID) { // delete local token immediately - MapTool.getCampaign().getZone(zoneGUID).removeToken(tokenGUID); + client.getCampaign().getZone(zoneGUID).removeToken(tokenGUID); var msg = RemoveTokenMsg.newBuilder() .setZoneGuid(zoneGUID.toString()) @@ -194,7 +193,7 @@ public void removeToken(GUID zoneGUID, GUID tokenGUID) { @Override public void removeTokens(GUID zoneGUID, List tokenGUIDs) { // delete local tokens immediately - MapTool.getCampaign().getZone(zoneGUID).removeTokens(tokenGUIDs); + client.getCampaign().getZone(zoneGUID).removeTokens(tokenGUIDs); var msg = RemoveTokensMsg.newBuilder().setZoneGuid(zoneGUID.toString()); msg.addAllTokenGuid(tokenGUIDs.stream().map(t -> t.toString()).collect(Collectors.toList())); makeServerCall(Message.newBuilder().setRemoveTokensMsg(msg).build()); @@ -419,7 +418,7 @@ public void exposePCArea(GUID zoneGUID) { public void exposeFoW(GUID zoneGUID, Area area, Set selectedToks) { // Expose locally right away. - MapTool.getCampaign().getZone(zoneGUID).exposeArea(area, selectedToks); + client.getCampaign().getZone(zoneGUID).exposeArea(area, selectedToks); var msg = ExposeFowMsg.newBuilder().setZoneGuid(zoneGUID.toString()).setArea(Mapper.map(area)); msg.addAllTokenGuid(selectedToks.stream().map(g -> g.toString()).collect(Collectors.toList())); makeServerCall(Message.newBuilder().setExposeFowMsg(msg).build()); @@ -518,9 +517,15 @@ public void clearExposedArea(GUID zoneGUID, boolean globalOnly) { makeServerCall(Message.newBuilder().setClearExposedAreaMsg(msg).build()); } - private static void makeServerCall(Message msg) { - if (MapTool.getConnection() != null) { - MapTool.getConnection().sendMessage(msg); + private void makeServerCall(Message msg) { + log.debug( + "{} making server call {}; state is {}", + client.getPlayer().getName(), + msg.getMessageTypeCase(), + client.getState()); + + if (client.getState() == MapToolClient.State.Connected) { + client.getConnection().sendMessage(msg); } } @@ -797,7 +802,7 @@ public void updatePlayerStatus(Player player) { * some time interval. If a new event arrives before the time interval elapses, it is replaced. In * this way, only the most current version of the event is released. */ - private static class TimedEventQueue extends Thread { + private class TimedEventQueue extends Thread { Message msg; long delay; diff --git a/src/main/java/net/rptools/maptool/client/ServerDisconnectHandler.java b/src/main/java/net/rptools/maptool/client/ServerDisconnectHandler.java deleted file mode 100644 index a0345c0a35..0000000000 --- a/src/main/java/net/rptools/maptool/client/ServerDisconnectHandler.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.client; - -import java.io.IOException; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.util.concurrent.ExecutionException; -import net.rptools.clientserver.simple.DisconnectHandler; -import net.rptools.clientserver.simple.connection.Connection; -import net.rptools.maptool.language.I18N; -import net.rptools.maptool.model.CampaignFactory; -import net.rptools.maptool.model.campaign.CampaignManager; - -/** This class handles when the server inexplicably disconnects */ -public class ServerDisconnectHandler implements DisconnectHandler { - // TODO: This is a temporary hack until I can come up with a cleaner mechanism - public static boolean disconnectExpected; - - public void handleDisconnect(Connection connection) { - // Update internal state - MapTool.disconnect(); - - // TODO: attempt to reconnect if this was unexpected - if (!disconnectExpected) { - var errorText = I18N.getText("msg.error.server.disconnected"); - var connectionError = connection.getError(); - var errorMessage = errorText + (connectionError != null ? (": " + connectionError) : ""); - MapTool.showError(errorMessage); - - // hide map so player doesn't get a brief GM view - MapTool.getFrame().setCurrentZoneRenderer(null); - MapTool.getFrame().getToolbarPanel().getMapselect().setVisible(true); - MapTool.getFrame().getAssetPanel().enableAssets(); - new CampaignManager().clearCampaignData(); - MapTool.getFrame().getToolbarPanel().setTokenSelectionGroupEnabled(true); - try { - MapTool.startPersonalServer(CampaignFactory.createBasicCampaign()); - } catch (IOException - | NoSuchAlgorithmException - | InvalidKeySpecException - | ExecutionException - | InterruptedException e) { - MapTool.showError(I18N.getText("msg.error.server.cantrestart"), e); - } - } else if (!MapTool.isPersonalServer() && !MapTool.isHostingServer()) { - // expected disconnect from someone else's server - // hide map so player doesn't get a brief GM view - MapTool.getFrame().setCurrentZoneRenderer(null); - } - disconnectExpected = false; - } -} diff --git a/src/main/java/net/rptools/maptool/client/events/ServerStopped.java b/src/main/java/net/rptools/maptool/client/events/ServerDisconnected.java similarity index 94% rename from src/main/java/net/rptools/maptool/client/events/ServerStopped.java rename to src/main/java/net/rptools/maptool/client/events/ServerDisconnected.java index 1b21cdb367..65eb33514a 100644 --- a/src/main/java/net/rptools/maptool/client/events/ServerStopped.java +++ b/src/main/java/net/rptools/maptool/client/events/ServerDisconnected.java @@ -14,4 +14,4 @@ */ package net.rptools.maptool.client.events; -public record ServerStopped() {} +public record ServerDisconnected() {} diff --git a/src/main/java/net/rptools/maptool/client/functions/DrawingFunctions.java b/src/main/java/net/rptools/maptool/client/functions/DrawingFunctions.java index ce14761020..f60cfafb37 100644 --- a/src/main/java/net/rptools/maptool/client/functions/DrawingFunctions.java +++ b/src/main/java/net/rptools/maptool/client/functions/DrawingFunctions.java @@ -251,7 +251,7 @@ protected JsonObject getDrawingJSONInfo(String functionName, Zone map, GUID guid dinfo.addProperty("name", d.getName()); dinfo.addProperty("layer", el.getDrawable().getLayer().name()); dinfo.addProperty("type", getDrawbleType(d)); - dinfo.add("bounds", boundsToJSON(d)); + dinfo.add("bounds", boundsToJSON(map, d)); dinfo.addProperty("penColor", paintToString(el.getPen().getPaint())); dinfo.addProperty("fillColor", paintToString(el.getPen().getBackgroundPaint())); dinfo.addProperty("opacity", el.getPen().getOpacity()); @@ -262,12 +262,12 @@ protected JsonObject getDrawingJSONInfo(String functionName, Zone map, GUID guid return dinfo; } - private JsonObject boundsToJSON(AbstractDrawing d) { + private JsonObject boundsToJSON(Zone map, AbstractDrawing d) { JsonObject binfo = new JsonObject(); - binfo.addProperty("x", d.getBounds().x); - binfo.addProperty("y", d.getBounds().y); - binfo.addProperty("width", d.getBounds().width); - binfo.addProperty("height", d.getBounds().height); + binfo.addProperty("x", d.getBounds(map).x); + binfo.addProperty("y", d.getBounds(map).y); + binfo.addProperty("width", d.getBounds(map).width); + binfo.addProperty("height", d.getBounds(map).height); return binfo; } diff --git a/src/main/java/net/rptools/maptool/client/functions/DrawingMiscFunctions.java b/src/main/java/net/rptools/maptool/client/functions/DrawingMiscFunctions.java index 2312355706..03dfee4c3e 100644 --- a/src/main/java/net/rptools/maptool/client/functions/DrawingMiscFunctions.java +++ b/src/main/java/net/rptools/maptool/client/functions/DrawingMiscFunctions.java @@ -108,7 +108,7 @@ public Object childEvaluate( private JsonArray getCrossedPoints(final Zone map, final DrawnElement de, final String pathStr) { List> pathPoints = convertJSONStringToList(pathStr); JsonArray returnPoints = new JsonArray(); - Area a = de.getDrawable().getArea(); + Area a = de.getDrawable().getArea(map); int cnt = 0; Point previousPoint = new Point(); for (Map entry : pathPoints) { diff --git a/src/main/java/net/rptools/maptool/client/functions/MacroLinkFunction.java b/src/main/java/net/rptools/maptool/client/functions/MacroLinkFunction.java index 005021c779..a332758e50 100644 --- a/src/main/java/net/rptools/maptool/client/functions/MacroLinkFunction.java +++ b/src/main/java/net/rptools/maptool/client/functions/MacroLinkFunction.java @@ -599,7 +599,7 @@ private static void doWhisper(String message, Token token, String playerName) { playerName = (!playerNameMatch.equals("")) ? playerNameMatch : playerName; // Validate - if (!MapTool.isPlayerConnected(playerName)) { + if (!MapTool.getClient().isPlayerConnected(playerName)) { MapTool.addLocalMessage(I18N.getText("msg.error.playerNotConnected", playerName)); return; } diff --git a/src/main/java/net/rptools/maptool/client/functions/PlayerFunctions.java b/src/main/java/net/rptools/maptool/client/functions/PlayerFunctions.java index 04be7090e0..6b8bdc9d16 100644 --- a/src/main/java/net/rptools/maptool/client/functions/PlayerFunctions.java +++ b/src/main/java/net/rptools/maptool/client/functions/PlayerFunctions.java @@ -19,6 +19,7 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.ExecutionException; +import net.rptools.maptool.client.MapTool; import net.rptools.maptool.language.I18N; import net.rptools.maptool.model.player.PlayerInfo; import net.rptools.maptool.model.player.Players; @@ -48,7 +49,7 @@ public Object childEvaluate( throws ParserException { try { - Players players = new Players(); + Players players = new Players(MapTool.getClient().getPlayerDatabase()); return switch (functionName) { case "player.getName" -> players.getPlayer().get().name(); case "player.getInfo" -> { diff --git a/src/main/java/net/rptools/maptool/client/functions/ServerFunctions.java b/src/main/java/net/rptools/maptool/client/functions/ServerFunctions.java index 5c11e92600..7639f45b28 100644 --- a/src/main/java/net/rptools/maptool/client/functions/ServerFunctions.java +++ b/src/main/java/net/rptools/maptool/client/functions/ServerFunctions.java @@ -17,6 +17,7 @@ import java.math.BigDecimal; import java.util.List; import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.MapToolClient; import net.rptools.maptool.language.I18N; import net.rptools.maptool.server.ServerPolicy; import net.rptools.parser.Parser; @@ -56,9 +57,11 @@ public Object childEvaluate( if (parameters.size() == 1) { BigDecimal ml = (BigDecimal) parameters.get(0); if (ml.intValue() == 0 || ml.intValue() == 1) { - ServerPolicy policy = MapTool.getServerPolicy(); + MapToolClient client = MapTool.getClient(); + ServerPolicy policy = client.getServerPolicy(); policy.setIsMovementLocked(ml.intValue() != 0); - MapTool.updateServerPolicy(policy); + client.setServerPolicy(policy); + client.getServerCommand().setServerPolicy(policy); } else { throw new ParserException( I18N.getText("macro.function.general.argumentTypeInvalid", "setmovelock")); diff --git a/src/main/java/net/rptools/maptool/client/macro/MacroManager.java b/src/main/java/net/rptools/maptool/client/macro/MacroManager.java index 314be2fc0d..3163049832 100644 --- a/src/main/java/net/rptools/maptool/client/macro/MacroManager.java +++ b/src/main/java/net/rptools/maptool/client/macro/MacroManager.java @@ -113,7 +113,6 @@ public record MacroDetails( registerMacro(new ChangeColorMacro()); registerMacro(new WhisperReplyMacro()); registerMacro(new EmotePluralMacro()); - registerMacro(new ExperimentsMacro()); registerMacro(new TextureNoise()); registerMacro(new VersionMacro()); registerMacro(new AboutMacro()); diff --git a/src/main/java/net/rptools/maptool/client/macro/impl/ExperimentsMacro.java b/src/main/java/net/rptools/maptool/client/macro/impl/ExperimentsMacro.java deleted file mode 100644 index 976831d76d..0000000000 --- a/src/main/java/net/rptools/maptool/client/macro/impl/ExperimentsMacro.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.client.macro.impl; - -import javax.swing.JOptionPane; -import net.rptools.maptool.client.MapTool; -import net.rptools.maptool.client.MapToolMacroContext; -import net.rptools.maptool.client.macro.Macro; -import net.rptools.maptool.client.macro.MacroContext; -import net.rptools.maptool.client.macro.MacroDefinition; -import net.rptools.maptool.language.I18N; - -@MacroDefinition( - name = "experiments", - aliases = {"exp", "exper"}, - description = "experiments.description") -public class ExperimentsMacro implements Macro { - @Override - public void execute(MacroContext context, String macro, MapToolMacroContext executionContext) { - // There is only one at the moment, if more are added this will need to be more flexible. - System.err.println("here!"); - macro = macro.trim(); - if (macro.length() == 0) { - displayUsage(); - } else { - String[] args = macro.split("\\s+"); - if ("webapp".equalsIgnoreCase(args[0])) { - if (args.length < 2) { - displayUsage(); - } else { - if (!confirmWebServerStart()) { - return; - } - - if (args.length > 2) { - for (int i = 2; i < args.length; i++) { - String[] dirArgs = args[i].split("="); - if (dirArgs.length != 2) { - displayUsage(); - return; - } - MapTool.getWebAppServer().addResourceDir(dirArgs[0], dirArgs[1]); - } - } - try { - int port = Integer.parseInt(args[1]); - if (MapTool.getWebAppServer().hasStarted()) { - MapTool.addLocalMessage(I18N.getText("webapp.serverAlreadyRunning")); - } else { - MapTool.startWebAppServer(port); - } - } catch (NumberFormatException e) { - displayUsage(); - return; - } - } - } - } - } - - private boolean confirmWebServerStart() { - String msg = I18N.getText("msg.confirm.webServerStart"); - Object[] options = { - I18N.getText("msg.title.messageDialog.continue"), - I18N.getText("msg.title.messageDialog.cancel") - }; - String title = I18N.getText("msg.confirm.webServerStartTitle"); - int val = - JOptionPane.showOptionDialog( - MapTool.getFrame(), - msg, - title, - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE, - null, - options, - options[1]); - return val == 0; - } - - private void displayUsage() { - StringBuilder sb = new StringBuilder(); - sb.append(""); - sb.append(""); - MapTool.addLocalMessage(I18N.getText(sb.toString())); - } -} diff --git a/src/main/java/net/rptools/maptool/client/macro/impl/WhisperMacro.java b/src/main/java/net/rptools/maptool/client/macro/impl/WhisperMacro.java index 273f9c6175..ecbed273f3 100644 --- a/src/main/java/net/rptools/maptool/client/macro/impl/WhisperMacro.java +++ b/src/main/java/net/rptools/maptool/client/macro/impl/WhisperMacro.java @@ -55,7 +55,7 @@ public void execute(MacroContext context, String macro, MapToolMacroContext exec playerName = (!playerNameMatch.equals("")) ? playerNameMatch : playerName; // Validate - if (!MapTool.isPlayerConnected(playerName)) { + if (!MapTool.getClient().isPlayerConnected(playerName)) { MapTool.addMessage( TextMessage.me( context.getTransformationHistory(), diff --git a/src/main/java/net/rptools/maptool/client/macro/impl/WhisperReplyMacro.java b/src/main/java/net/rptools/maptool/client/macro/impl/WhisperReplyMacro.java index f0f22f5ff7..5d3b3d41ef 100644 --- a/src/main/java/net/rptools/maptool/client/macro/impl/WhisperReplyMacro.java +++ b/src/main/java/net/rptools/maptool/client/macro/impl/WhisperReplyMacro.java @@ -36,7 +36,7 @@ public void execute(MacroContext context, String message, MapToolMacroContext ex return; } // Validate - if (!MapTool.isPlayerConnected(playerName)) { + if (!MapTool.getClient().isPlayerConnected(playerName)) { MapTool.addMessage( TextMessage.me( context.getTransformationHistory(), diff --git a/src/main/java/net/rptools/maptool/client/swing/PlayersLoadingStatusBar.java b/src/main/java/net/rptools/maptool/client/swing/PlayersLoadingStatusBar.java index 70ddb6e92e..7416db36b4 100644 --- a/src/main/java/net/rptools/maptool/client/swing/PlayersLoadingStatusBar.java +++ b/src/main/java/net/rptools/maptool/client/swing/PlayersLoadingStatusBar.java @@ -22,7 +22,7 @@ import net.rptools.maptool.client.events.PlayerConnected; import net.rptools.maptool.client.events.PlayerDisconnected; import net.rptools.maptool.client.events.PlayerStatusChanged; -import net.rptools.maptool.client.events.ServerStopped; +import net.rptools.maptool.client.events.ServerDisconnected; import net.rptools.maptool.client.ui.theme.Icons; import net.rptools.maptool.client.ui.theme.RessourceManager; import net.rptools.maptool.events.MapToolEventBus; @@ -118,7 +118,7 @@ private void onPlayerDisconnected(PlayerDisconnected event) { } @Subscribe - private void onServerStopped(ServerStopped event) { + private void onServerDisconnected(ServerDisconnected event) { refreshCount(); } } diff --git a/src/main/java/net/rptools/maptool/client/swing/ZoomStatusBar.java b/src/main/java/net/rptools/maptool/client/swing/ZoomStatusBar.java index 804a6aaa81..5e9f04ba7a 100644 --- a/src/main/java/net/rptools/maptool/client/swing/ZoomStatusBar.java +++ b/src/main/java/net/rptools/maptool/client/swing/ZoomStatusBar.java @@ -14,7 +14,6 @@ */ package net.rptools.maptool.client.swing; -import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.text.ParseException; @@ -31,10 +30,9 @@ * and changing the zoom level to that amount. */ public class ZoomStatusBar extends JTextField implements ActionListener { - private static final Dimension minSize = new Dimension(50, 10); - public ZoomStatusBar() { - super("", RIGHT); + super("", 9); + setHorizontalAlignment(RIGHT); setToolTipText(I18N.getString("ZoomStatusBar.tooltip")); addActionListener(this); } @@ -61,26 +59,6 @@ public void actionPerformed(ActionEvent e) { } } - /* - * (non-Javadoc) - * - * @see javax.swing.JComponent#getMinimumSize() - */ - @Override - public Dimension getMinimumSize() { - return minSize; - } - - /* - * (non-Javadoc) - * - * @see javax.swing.JComponent#getPreferredSize() - */ - @Override - public Dimension getPreferredSize() { - return getMinimumSize(); - } - public void clear() { setText(""); } @@ -90,7 +68,18 @@ public void update() { if (MapTool.getFrame().getCurrentZoneRenderer() != null) { double scale = MapTool.getFrame().getCurrentZoneRenderer().getZoneScale().getScale(); scale *= 100; - zoom = String.format("%d%%", (int) scale); + + if (scale < 10) { + zoom = String.format("%.4f%%", scale); + } else if (scale < 100) { + zoom = String.format("%.3f%%", scale); + } else if (scale < 1000) { + zoom = String.format("%.2f%%", scale); + } else if (scale < 10000) { + zoom = String.format("%.1f%%", scale); + } else { + zoom = String.format("%.0f%%", scale); + } } setText(zoom); } diff --git a/src/main/java/net/rptools/maptool/client/tool/AI_Tool.java b/src/main/java/net/rptools/maptool/client/tool/AI_Tool.java index 15aaaa33a4..73b71af248 100644 --- a/src/main/java/net/rptools/maptool/client/tool/AI_Tool.java +++ b/src/main/java/net/rptools/maptool/client/tool/AI_Tool.java @@ -30,8 +30,12 @@ public AI_Tool() { @Override public void actionPerformed(ActionEvent e) { AppPreferences.setUseAstarPathfinding(isSelected()); - MapTool.getServerPolicy().setUsingAstarPathfinding(isSelected()); - MapTool.updateServerPolicy(); + + var client = MapTool.getClient(); + var policy = client.getServerPolicy(); + policy.setUsingAstarPathfinding(isSelected()); + client.setServerPolicy(policy); + client.getServerCommand().setServerPolicy(policy); // Trigger AI_UseVblTool's isAvailable MapTool.getFrame().getToolbox().updateTools(); diff --git a/src/main/java/net/rptools/maptool/client/tool/AI_UseVblTool.java b/src/main/java/net/rptools/maptool/client/tool/AI_UseVblTool.java index 7714e04fce..57f4ad6285 100644 --- a/src/main/java/net/rptools/maptool/client/tool/AI_UseVblTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/AI_UseVblTool.java @@ -30,8 +30,12 @@ public AI_UseVblTool() { @Override public void actionPerformed(ActionEvent e) { AppPreferences.setVblBlocksMove(isSelected()); - MapTool.getServerPolicy().setVblBlocksMove(isSelected()); - MapTool.updateServerPolicy(); + + var client = MapTool.getClient(); + var policy = client.getServerPolicy(); + policy.setVblBlocksMove(isSelected()); + client.setServerPolicy(policy); + client.getServerCommand().setServerPolicy(policy); } @Override diff --git a/src/main/java/net/rptools/maptool/client/tool/PointerTool.java b/src/main/java/net/rptools/maptool/client/tool/PointerTool.java index 091741f7ec..f952e118c2 100644 --- a/src/main/java/net/rptools/maptool/client/tool/PointerTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/PointerTool.java @@ -662,13 +662,12 @@ public void mouseReleased(MouseEvent e) { // MouseMotion @Override public void mouseMoved(MouseEvent e) { + super.mouseMoved(e); + if (renderer == null) { return; } - super.mouseMoved(e); - // mouseX = e.getX(); // done by super.mouseMoved() - // mouseY = e.getY(); if (isShowingPointer) { ZonePoint zp = new ScreenPoint(mouseX, mouseY).convertToZone(renderer); Pointer pointer = diff --git a/src/main/java/net/rptools/maptool/client/tool/StampTool.java b/src/main/java/net/rptools/maptool/client/tool/StampTool.java index 74d1c6cb77..c85b374f20 100644 --- a/src/main/java/net/rptools/maptool/client/tool/StampTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/StampTool.java @@ -496,10 +496,11 @@ public void mouseReleased(MouseEvent e) { */ @Override public void mouseMoved(MouseEvent e) { + super.mouseMoved(e); + if (renderer == null) { return; } - super.mouseMoved(e); if (isShowingTokenStackPopup) { if (tokenStackPanel.contains(e.getX(), e.getY())) { diff --git a/src/main/java/net/rptools/maptool/client/tool/boardtool/BoardTool.java b/src/main/java/net/rptools/maptool/client/tool/boardtool/BoardTool.java index 3952420334..a25560d39a 100644 --- a/src/main/java/net/rptools/maptool/client/tool/boardtool/BoardTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/boardtool/BoardTool.java @@ -280,12 +280,6 @@ public void mouseDragged(java.awt.event.MouseEvent e) { } } - @Override - public void mouseMoved(java.awt.event.MouseEvent e) { - mouseX = e.getX(); - mouseY = e.getY(); - } - /** A simple enum for correlating keys with directions */ private enum Direction { Left, diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/AbstractDrawingTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/AbstractDrawingTool.java index 28529667bd..c98aab2875 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/AbstractDrawingTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/AbstractDrawingTool.java @@ -34,7 +34,6 @@ import net.rptools.maptool.client.tool.DefaultTool; import net.rptools.maptool.client.ui.zone.ZoneOverlay; import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer; -import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.Token; import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.Zone.Layer; @@ -117,7 +116,7 @@ protected void paintTransformed(Graphics2D g, ZoneRenderer renderer, Drawable dr AffineTransform transform = getPaintTransform(renderer); AffineTransform oldTransform = g.getTransform(); g.transform(transform); - drawing.draw(g, pen); + drawing.draw(renderer.getZone(), g, pen); g.setTransform(oldTransform); } @@ -220,8 +219,7 @@ protected ZonePoint getPoint(MouseEvent e) { } protected Area getTokenTopology(Zone.TopologyType topologyType) { - List topologyTokens = - MapTool.getFrame().getCurrentZoneRenderer().getZone().getTokensWithTopology(topologyType); + List topologyTokens = getZone().getTokensWithTopology(topologyType); Area tokenTopology = new Area(); for (Token topologyToken : topologyTokens) { @@ -326,15 +324,16 @@ protected void paintTopologyOverlay(Graphics2D g, Drawable drawable, int penMode * Render a drawable on a zone. This method consolidates all of the calls to the server in one * place so that it is easier to keep them in sync. * - * @param zoneId Id of the zone where the drawable is being drawn. * @param pen The pen used to draw. * @param drawable What is being drawn. */ - protected void completeDrawable(GUID zoneId, Pen pen, Drawable drawable) { + protected void completeDrawable(Pen pen, Drawable drawable) { + var zone = getZone(); + if (!hasPaint(pen)) { return; } - if (drawable.getBounds() == null) { + if (drawable.getBounds(zone) == null) { return; } if (MapTool.getPlayer().isGM()) { @@ -348,11 +347,10 @@ protected void completeDrawable(GUID zoneId, Pen pen, Drawable drawable) { MapToolUtil.uploadTexture(pen.getBackgroundPaint()); // Tell the local/server to render the drawable. - MapTool.serverCommand().draw(zoneId, pen, drawable); + MapTool.serverCommand().draw(zone.getId(), pen, drawable); // Allow it to be undone - Zone z = MapTool.getFrame().getCurrentZoneRenderer().getZone(); - z.addDrawable(pen, drawable); + zone.addDrawable(pen, drawable); } private boolean hasPaint(Pen pen) { diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/AbstractLineTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/AbstractLineTool.java index 9cf10e40bd..30b15fe03b 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/AbstractLineTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/AbstractLineTool.java @@ -95,7 +95,7 @@ protected void stopLine(MouseEvent e) { if (isBackgroundFill(e) && line.getPoints().size() > 2) { drawable = new ShapeDrawable(getPolygon(trimLine)); } - completeDrawable(renderer.getZone().getId(), getPen(), drawable); + completeDrawable(getPen(), drawable); line = null; currentX = -1; diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/CrossTopologyTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/CrossTopologyTool.java index b9f508c62e..226c75fe81 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/CrossTopologyTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/CrossTopologyTool.java @@ -119,6 +119,8 @@ public void mouseDragged(MouseEvent e) { @Override public void mouseMoved(MouseEvent e) { + super.mouseMoved(e); + setIsEraser(isEraser(e)); ZonePoint p = getPoint(e); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/DeleteDrawingTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/DeleteDrawingTool.java index 016aa83202..3df29aa5db 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/DeleteDrawingTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/DeleteDrawingTool.java @@ -93,7 +93,7 @@ public void mouseClicked(MouseEvent e) { var drawable = element.getDrawable(); var id = drawable.getId(); ZonePoint pos = new ScreenPoint(e.getX(), e.getY()).convertToZone(renderer); - if (drawable.getBounds().contains(pos.x, pos.y)) { + if (drawable.getBounds(zone).contains(pos.x, pos.y)) { if (!selectedDrawings.contains(id)) selectedDrawings.add(id); else selectedDrawings.remove(id); break; @@ -116,7 +116,7 @@ public void paintOverlay(ZoneRenderer renderer, Graphics2D g) { } private void drawBox(Graphics2D g, DrawnElement element) { - var box = element.getDrawable().getBounds(); + var box = element.getDrawable().getBounds(getZone()); var pen = element.getPen(); var scale = renderer.getScale(); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/DiamondExposeTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/DiamondExposeTool.java index 925f4150c5..ee70cf3314 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/DiamondExposeTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/DiamondExposeTool.java @@ -63,15 +63,15 @@ protected Pen getPen() { } @Override - protected void completeDrawable(GUID zoneId, Pen pen, Drawable drawable) { + protected void completeDrawable(Pen pen, Drawable drawable) { if (!MapTool.getPlayer().isGM()) { MapTool.showError("msg.error.fogexpose"); MapTool.getFrame().refresh(); return; } - Zone zone = MapTool.getCampaign().getZone(zoneId); - Area area = new Area(drawable.getArea()); - Set selectedToks = MapTool.getFrame().getCurrentZoneRenderer().getSelectedTokenSet(); + Zone zone = getZone(); + Area area = new Area(drawable.getArea(zone)); + Set selectedToks = renderer.getSelectedTokenSet(); if (pen.isEraser()) { zone.hideArea(area, selectedToks); MapTool.serverCommand().hideFoW(zone.getId(), area, selectedToks); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/DiamondTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/DiamondTool.java index 5ef2fa455e..970ae65f30 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/DiamondTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/DiamondTool.java @@ -76,7 +76,7 @@ public void mousePressed(MouseEvent e) { return; } // ToolHelper.drawDiamondMeasurement(renderer, null, diamond); - completeDrawable(renderer.getZone().getId(), getPen(), new ShapeDrawable(diamond, false)); + completeDrawable(getPen(), new ShapeDrawable(diamond, false)); diamond = null; } setIsEraser(isEraser(e)); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/DiamondTopologyTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/DiamondTopologyTool.java index 2d6ed9d5a2..f8c77dc832 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/DiamondTopologyTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/DiamondTopologyTool.java @@ -78,7 +78,7 @@ public void mousePressed(MouseEvent e) { renderer.repaint(); return; } - Area area = new ShapeDrawable(diamond, false).getArea(); + Area area = new ShapeDrawable(diamond, false).getArea(getZone()); if (isEraser(e)) { getZone().removeTopology(area); MapTool.serverCommand() @@ -107,6 +107,8 @@ public void mouseDragged(MouseEvent e) { @Override public void mouseMoved(MouseEvent e) { + super.mouseMoved(e); + setIsEraser(isEraser(e)); ZonePoint zp = getPoint(e); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/DrawnTextTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/DrawnTextTool.java deleted file mode 100644 index 7e2b2f0b99..0000000000 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/DrawnTextTool.java +++ /dev/null @@ -1,269 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.client.tool.drawing; - -import java.awt.Color; -import java.awt.Font; -import java.awt.Graphics2D; -import java.awt.Point; -import java.awt.Rectangle; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; -import java.awt.event.MouseMotionListener; -import java.util.Map; -import javax.swing.AbstractAction; -import javax.swing.Action; -import javax.swing.KeyStroke; -import javax.swing.text.Style; -import net.rptools.maptool.client.swing.TwoToneTextPane; -import net.rptools.maptool.client.tool.Tool; -import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer; -import net.rptools.maptool.model.drawing.DrawnLabel; -import net.rptools.maptool.model.drawing.Pen; - -/** - * A text tool that uses a text component to allow text to be entered on the display and then - * renders it as an image. - * - * @author jgorrell - * @version $Revision: 5945 $ $Date: 2006-03-11 02:57:18 -0600 (Sat, 11 Mar 2006) $ $Author: - * azhrei_fje $ - */ -public class DrawnTextTool extends AbstractDrawingTool implements MouseMotionListener { - - /*--------------------------------------------------------------------------------------------- - * Instance Variables - *-------------------------------------------------------------------------------------------*/ - - /** Flag used to indicate that the anchor has been set. */ - private boolean anchorSet; - - /** The anchor point originally selected */ - private Point anchor = new Point(); - - /** The bounds of the display rectangle */ - private Rectangle bounds = new Rectangle(); - - /** The text pane used to paint the text. */ - private TwoToneTextPane textPane; - - /*--------------------------------------------------------------------------------------------- - * Constructors - *-------------------------------------------------------------------------------------------*/ - - /** A transparent color used in the background */ - private static final Color TRANSPARENT = new Color(0, 0, 0, 0); - - /*--------------------------------------------------------------------------------------------- - * Constructors - *-------------------------------------------------------------------------------------------*/ - - /** Initialize the tool icon */ - public DrawnTextTool() {} - - /*--------------------------------------------------------------------------------------------- - * Tool & AbstractDrawingTool Abstract Methods - *-------------------------------------------------------------------------------------------*/ - - /** - * @see net.rptools.maptool.client.tool.drawing.AbstractDrawingTool#paintOverlay(ZoneRenderer, - * java.awt.Graphics2D) - */ - @Override - public void paintOverlay(ZoneRenderer aRenderer, Graphics2D aG) { - if (!anchorSet) return; - aG.setColor(Color.BLACK); - aG.drawRect(bounds.x, bounds.y, bounds.width, bounds.height); - } - - /** - * @see Tool#getTooltip() - */ - @Override - public String getTooltip() { - return "tool.text.tooltip"; - } - - /** - * @see Tool#getInstructions() - */ - @Override - public String getInstructions() { - return "tool.text.instructions"; - } - - /** - * @see Tool#resetTool() - */ - @Override - protected void resetTool() { - anchorSet = false; - if (textPane != null) renderer.remove(textPane); - textPane = null; - renderer.repaint(); - } - - /*--------------------------------------------------------------------------------------------- - * MouseListener Interface Methods - *-------------------------------------------------------------------------------------------*/ - - /** - * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent) - */ - public void mouseClicked(MouseEvent event) { - // Do nothing - } - - /** - * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent) - */ - public void mousePressed(MouseEvent event) { - if (!anchorSet) { - anchor.x = event.getX(); - anchor.y = event.getY(); - anchorSet = true; - } else { - setBounds(event); - - // Create a text component and place it on the renderer's component - textPane = createTextPane(bounds, getPen(), "sanserif-BOLD-20"); - renderer.add(textPane); - textPane.requestFocusInWindow(); - - // Make the enter key addthe text - KeyStroke k = KeyStroke.getKeyStroke("ENTER"); - textPane.getKeymap().removeKeyStrokeBinding(k); - textPane - .getKeymap() - .addActionForKeyStroke( - k, - new AbstractAction() { - public void actionPerformed(ActionEvent aE) { - completeDrawable(); - } - }); - } - } - - /** - * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent) - */ - public void mouseReleased(MouseEvent aE) { - // TODO Auto-generated method stub - - } - - /** - * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent) - */ - public void mouseEntered(MouseEvent aE) { - // TODO Auto-generated method stub - - } - - /** - * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent) - */ - public void mouseExited(MouseEvent aE) { - // TODO Auto-generated method stub - - } - - /*--------------------------------------------------------------------------------------------- - * MouseMotionListener Interface Methods - *-------------------------------------------------------------------------------------------*/ - - /** - * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent) - */ - public void mouseMoved(MouseEvent event) { - if (!anchorSet) return; - if (textPane != null) return; - setBounds(event); - renderer.repaint(); - } - - /** - * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent) - */ - public void mouseDragged(MouseEvent aE) { - // TODO Auto-generated method stub - - } - - @Override - protected void installKeystrokes(Map actionMap) { - super.installKeystrokes(actionMap); - actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, true), null); - actionMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, false), null); - } - - /*--------------------------------------------------------------------------------------------- - * Instance Methods - *-------------------------------------------------------------------------------------------*/ - - /** - * Set the bounds for the text area. - * - * @param event The mouse event used in the calculation. - */ - private void setBounds(MouseEvent event) { - bounds.x = Math.min(anchor.x, event.getX()); - bounds.y = Math.min(anchor.y, event.getY()); - bounds.width = Math.abs(anchor.x - event.getX()); - bounds.height = Math.abs(anchor.y - event.getY()); - } - - /** Finish drawing the text. */ - private void completeDrawable() { - - // Create a drawable from the data and clean up the component. - DrawnLabel label = - new DrawnLabel( - textPane.getText(), bounds, TwoToneTextPane.getFontString(textPane.getLogicalStyle())); - textPane.setVisible(false); - textPane.getParent().remove(textPane); - textPane = null; - - // Tell everybody else - completeDrawable(renderer.getZone().getId(), getPen(), label); - resetTool(); - } - - /** - * Create a text pane with the passed bounds, pen, and font data - * - * @param bounds Bounds of the new text pane - * @param pen Pen used for foreground and background text colors. - * @param font Font used to pain the text - * @return A text pane used to draw text - */ - public static TwoToneTextPane createTextPane(Rectangle bounds, Pen pen, String font) { - // Create a text component and place it on the renderer's component - TwoToneTextPane textPane = new TwoToneTextPane(); - textPane.setBounds(bounds); - textPane.setOpaque(false); - textPane.setBackground(TRANSPARENT); - - // Create a style for the component - Style style = textPane.addStyle("default", null); - TwoToneTextPane.setFont(style, Font.decode(font)); - // style.addAttribute(StyleConstants.Foreground, new Color(pen.getColor())); - // style.addAttribute(StyleConstants.Background, new Color(pen.getBackgroundColor())); - textPane.setLogicalStyle(style); - return textPane; - } -} diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/FreehandExposeTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/FreehandExposeTool.java index 578b2d3b36..059edb246d 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/FreehandExposeTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/FreehandExposeTool.java @@ -76,18 +76,18 @@ protected void stopLine(MouseEvent e) { if (line == null) return; // Escape has been pressed addPoint(e); - completeDrawable(renderer.getZone().getId(), getPen(), line); + completeDrawable(getPen(), line); resetTool(); } @Override - protected void completeDrawable(GUID zoneId, Pen pen, Drawable drawable) { + protected void completeDrawable(Pen pen, Drawable drawable) { if (!MapTool.getPlayer().isGM()) { MapTool.showError("msg.error.fogexpose"); MapTool.getFrame().refresh(); return; } - Zone zone = MapTool.getCampaign().getZone(zoneId); + Zone zone = getZone(); Area area = null; if (drawable instanceof LineSegment) { @@ -96,7 +96,7 @@ protected void completeDrawable(GUID zoneId, Pen pen, Drawable drawable) { if (drawable instanceof ShapeDrawable) { area = new Area(((ShapeDrawable) drawable).getShape()); } - Set selectedToks = MapTool.getFrame().getCurrentZoneRenderer().getSelectedTokenSet(); + Set selectedToks = renderer.getSelectedTokenSet(); if (pen.isEraser()) { zone.hideArea(area, selectedToks); MapTool.serverCommand().hideFoW(zone.getId(), area, selectedToks); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/HollowDiamondTopologyTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/HollowDiamondTopologyTool.java index ccfe55cdc4..2b56df06c8 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/HollowDiamondTopologyTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/HollowDiamondTopologyTool.java @@ -106,6 +106,8 @@ public void mouseDragged(MouseEvent e) { @Override public void mouseMoved(MouseEvent e) { + super.mouseMoved(e); + setIsEraser(isEraser(e)); ZonePoint zp = getPoint(e); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/HollowOvalTopologyTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/HollowOvalTopologyTool.java index 84504a1702..722e1cd1a3 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/HollowOvalTopologyTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/HollowOvalTopologyTool.java @@ -124,6 +124,7 @@ public void mouseDragged(MouseEvent e) { } public void mouseMoved(MouseEvent e) { + super.mouseMoved(e); setIsEraser(isEraser(e)); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/HollowRectangleTopologyTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/HollowRectangleTopologyTool.java index 6248b45452..eff224ca08 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/HollowRectangleTopologyTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/HollowRectangleTopologyTool.java @@ -116,6 +116,8 @@ public void mouseDragged(MouseEvent e) { @Override public void mouseMoved(MouseEvent e) { + super.mouseMoved(e); + setIsEraser(isEraser(e)); ZonePoint p = getPoint(e); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/LineCellTemplateTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/LineCellTemplateTool.java index 3223c5f634..37604101c9 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/LineCellTemplateTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/LineCellTemplateTool.java @@ -19,13 +19,11 @@ import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import javax.swing.SwingUtilities; -import net.rptools.maptool.client.ScreenPoint; import net.rptools.maptool.client.swing.SwingUtil; import net.rptools.maptool.client.tool.Tool; import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer; import net.rptools.maptool.model.ZonePoint; import net.rptools.maptool.model.drawing.AbstractTemplate; -import net.rptools.maptool.model.drawing.AbstractTemplate.Quadrant; import net.rptools.maptool.model.drawing.LineCellTemplate; import net.rptools.maptool.model.drawing.Pen; @@ -109,7 +107,7 @@ public void paintOverlay(ZoneRenderer renderer, Graphics2D g) { g.setTransform(newTransform); ZonePoint vertex = template.getVertex(); ZonePoint pathVertex = ((LineCellTemplate) template).getPathVertex(); - template.draw(g, pen); + template.draw(renderer.getZone(), g, pen); Paint paint = pen.getPaint() != null ? pen.getPaint().getPaint() : null; paintCursor(g, paint, pen.getThickness(), vertex); if (pathVertex != null) { @@ -173,7 +171,6 @@ public void mousePressed(MouseEvent aE) { protected void handleMouseMovement(MouseEvent e) { // Setting anchor point? LineCellTemplate lt = (LineCellTemplate) template; - ZonePoint pathVertex = lt.getPathVertex(); ZonePoint vertex = lt.getVertex(); if (!anchorSet) { @@ -189,58 +186,21 @@ protected void handleMouseMovement(MouseEvent e) { template.setRadius(getRadiusAtMouse(e)); controlOffset = null; - // The path vertex remains null until it is set the first time. - if (pathVertex == null) { - pathVertex = new ZonePoint(vertex.x, vertex.y); - lt.setPathVertex(pathVertex); - } // endif - if (setCellAtMouse(e, pathVertex)) lt.clearPath(); - - // Determine which of the extra squares are used on diagonals - double dx = pathVertex.x - vertex.x; - double dy = pathVertex.y - vertex.y; - if (dx != 0 && dy != 0) { // Ignore straight lines - boolean mouseSlopeGreater = false; - double m = Math.abs(dy / dx); - double edx = e.getX() - vertex.x; - double edy = e.getY() - vertex.y; - if (edx != 0 && edy != 0) { // Handle straight lines differently - double em = Math.abs(edy / edx); - mouseSlopeGreater = em > m; - } else if (edx == 0) { - mouseSlopeGreater = true; - } // endif - if (mouseSlopeGreater != lt.isMouseSlopeGreater()) { - lt.setMouseSlopeGreater(mouseSlopeGreater); - renderer.repaint(); - } // endif - } // endif + ZonePoint pathVertex = getCellAtMouse(e); + lt.setPathVertex(pathVertex); + renderer.repaint(); // Let control move the path anchor } else if (SwingUtil.isControlDown(e)) { + ZonePoint pathVertex = lt.getPathVertex(); handleControlOffset(e, pathVertex); + lt.setPathVertex(pathVertex); // Set the final radius } else { template.setRadius(getRadiusAtMouse(e)); renderer.repaint(); controlOffset = null; - return; - } // endif - - // Quadrant change? - if (pathVertex != null) { - ZonePoint mouse = new ScreenPoint(e.getX(), e.getY()).convertToZone(renderer); - int dx = mouse.x - vertex.x; - int dy = mouse.y - vertex.y; - AbstractTemplate.Quadrant quadrant = - (dx < 0) - ? (dy < 0 ? Quadrant.NORTH_WEST : Quadrant.SOUTH_WEST) - : (dy < 0 ? Quadrant.NORTH_EAST : Quadrant.SOUTH_EAST); - if (quadrant != lt.getQuadrant()) { - lt.setQuadrant(quadrant); - renderer.repaint(); - } // endif } // endif } } diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/LineTemplateTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/LineTemplateTool.java index 73ac10dc2a..1bda1775ff 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/LineTemplateTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/LineTemplateTool.java @@ -116,7 +116,7 @@ public void paintOverlay(ZoneRenderer renderer, Graphics2D g) { g.setTransform(newTransform); ZonePoint vertex = template.getVertex(); ZonePoint pathVertex = ((LineTemplate) template).getPathVertex(); - template.draw(g, pen); + template.draw(renderer.getZone(), g, pen); Paint paint = pen.getPaint() != null ? pen.getPaint().getPaint() : null; paintCursor(g, paint, pen.getThickness(), vertex); if (pathVertex != null) { diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/LineTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/LineTool.java index bc88330a9f..6de6cd9cc0 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/LineTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/LineTool.java @@ -65,12 +65,13 @@ public void mouseDragged(MouseEvent e) { @Override public void mouseMoved(MouseEvent e) { + super.mouseMoved(e); + if (getLine() != null) { if (tempPoint != null) { removePoint(tempPoint); } tempPoint = addPoint(e); } - super.mouseMoved(e); } } diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/OvalExposeTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/OvalExposeTool.java index 9880cfdfbf..6f8d6f72f0 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/OvalExposeTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/OvalExposeTool.java @@ -70,17 +70,17 @@ public String getInstructions() { } @Override - protected void completeDrawable(GUID zoneId, Pen pen, Drawable drawable) { + protected void completeDrawable(Pen pen, Drawable drawable) { if (!MapTool.getPlayer().isGM()) { MapTool.showError("msg.error.fogexpose"); MapTool.getFrame().refresh(); return; } - Zone zone = MapTool.getCampaign().getZone(zoneId); + Zone zone = getZone(); - Rectangle bounds = drawable.getBounds(); + Rectangle bounds = drawable.getBounds(zone); Area area = new Area(new Ellipse2D.Double(bounds.x, bounds.y, bounds.width, bounds.height)); - Set selectedToks = MapTool.getFrame().getCurrentZoneRenderer().getSelectedTokenSet(); + Set selectedToks = renderer.getSelectedTokenSet(); if (pen.isEraser()) { zone.hideArea(area, selectedToks); MapTool.serverCommand().hideFoW(zone.getId(), area, selectedToks); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/OvalTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/OvalTool.java index b29622b2f0..e99f99a48e 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/OvalTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/OvalTool.java @@ -100,7 +100,6 @@ public void mousePressed(MouseEvent e) { } completeDrawable( - renderer.getZone().getId(), getPen(), new ShapeDrawable(new Ellipse2D.Float(oval.x, oval.y, oval.width, oval.height), true)); oval = null; @@ -127,6 +126,8 @@ public void mouseDragged(MouseEvent e) { */ @Override public void mouseMoved(MouseEvent e) { + super.mouseMoved(e); + if (oval != null) { ZonePoint sp = getPoint(e); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/OvalTopologyTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/OvalTopologyTool.java index 69d410c574..32b994ac70 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/OvalTopologyTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/OvalTopologyTool.java @@ -109,6 +109,8 @@ public void mouseDragged(MouseEvent e) { @Override public void mouseMoved(MouseEvent e) { + super.mouseMoved(e); + setIsEraser(isEraser(e)); if (oval != null) { ZonePoint sp = getPoint(e); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/PolygonExposeTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/PolygonExposeTool.java index f4e3711ca1..1c7e9d059f 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/PolygonExposeTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/PolygonExposeTool.java @@ -77,22 +77,22 @@ protected void stopLine(MouseEvent e) { if (line == null) return; // Escape has been pressed addPoint(e); - completeDrawable(renderer.getZone().getId(), getPen(), line); + completeDrawable(getPen(), line); resetTool(); } @Override - protected void completeDrawable(GUID zoneId, Pen pen, Drawable drawable) { + protected void completeDrawable(Pen pen, Drawable drawable) { if (!MapTool.getPlayer().isGM()) { MapTool.showError("msg.error.fogexpose"); MapTool.getFrame().refresh(); return; } - Zone zone = MapTool.getCampaign().getZone(zoneId); + Zone zone = getZone(); Polygon polygon = getPolygon((LineSegment) drawable); Area area = new Area(polygon); - Set selectedToks = MapTool.getFrame().getCurrentZoneRenderer().getSelectedTokenSet(); + Set selectedToks = renderer.getSelectedTokenSet(); if (pen.isEraser()) { zone.hideArea(area, selectedToks); MapTool.serverCommand().hideFoW(zone.getId(), area, selectedToks); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/PolygonTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/PolygonTool.java index ca6ba764a7..d7f3156669 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/PolygonTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/PolygonTool.java @@ -17,7 +17,6 @@ import java.awt.Point; import java.awt.Polygon; import java.awt.event.MouseMotionListener; -import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.drawing.Drawable; import net.rptools.maptool.model.drawing.LineSegment; import net.rptools.maptool.model.drawing.Pen; @@ -40,9 +39,9 @@ public String getInstructions() { } @Override - protected void completeDrawable(GUID zoneGUID, Pen pen, Drawable drawable) { + protected void completeDrawable(Pen pen, Drawable drawable) { LineSegment line = (LineSegment) drawable; - super.completeDrawable(zoneGUID, pen, new ShapeDrawable(getPolygon(line))); + super.completeDrawable(pen, new ShapeDrawable(getPolygon(line))); } @Override diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/PolygonTopologyTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/PolygonTopologyTool.java index 515e966fe3..463d78e6c0 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/PolygonTopologyTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/PolygonTopologyTool.java @@ -25,7 +25,6 @@ import net.rptools.maptool.client.AppStyle; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer; -import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.drawing.Drawable; import net.rptools.maptool.model.drawing.DrawableColorPaint; import net.rptools.maptool.model.drawing.LineSegment; @@ -74,7 +73,7 @@ protected boolean isBackgroundFill(MouseEvent e) { } @Override - protected void completeDrawable(GUID zoneGUID, Pen pen, Drawable drawable) { + protected void completeDrawable(Pen pen, Drawable drawable) { Area area = new Area(); if (drawable instanceof LineSegment) { diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/RadiusCellTemplateTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/RadiusCellTemplateTool.java index 9cd5a51468..1a0036ef85 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/RadiusCellTemplateTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/RadiusCellTemplateTool.java @@ -217,7 +217,6 @@ protected void resetTool(ZonePoint vertex) { } // endif template = createBaseTemplate(); template.setVertex(vertex); - template.setZoneId(renderer.getZone().getId()); controlOffset = null; renderer.repaint(); } @@ -313,7 +312,7 @@ public void paintOverlay(ZoneRenderer renderer, Graphics2D g) { AffineTransform newTransform = g.getTransform(); newTransform.concatenate(getPaintTransform(renderer)); g.setTransform(newTransform); - template.draw(g, pen); + template.draw(renderer.getZone(), g, pen); Paint paint = pen.getPaint() != null ? pen.getPaint().getPaint() : null; paintCursor(g, paint, pen.getThickness(), template.getVertex()); g.setTransform(old); @@ -354,7 +353,6 @@ protected Pen getPen() { @Override protected void detachFrom(ZoneRenderer renderer) { super.detachFrom(renderer); - template.setZoneId(null); renderer.repaint(); } @@ -363,7 +361,6 @@ protected void detachFrom(ZoneRenderer renderer) { */ @Override protected void attachTo(ZoneRenderer renderer) { - template.setZoneId(renderer.getZone().getId()); renderer.repaint(); super.attachTo(renderer); } @@ -412,7 +409,7 @@ public void mousePressed(MouseEvent e) { template.setRadius(getRadiusAtMouse(e)); ZonePoint vertex = template.getVertex(); ZonePoint newPoint = new ZonePoint(vertex.x, vertex.y); - completeDrawable(renderer.getZone().getId(), getPen(), template); + completeDrawable(getPen(), template); setIsEraser(false); resetTool(newPoint); } diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/RadiusTemplateTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/RadiusTemplateTool.java index b9c724a3d3..a5c84ff856 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/RadiusTemplateTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/RadiusTemplateTool.java @@ -220,7 +220,6 @@ protected void resetTool(ZonePoint vertex) { } // endif template = createBaseTemplate(); template.setVertex(vertex); - template.setZoneId(renderer.getZone().getId()); controlOffset = null; renderer.repaint(); } @@ -316,7 +315,7 @@ public void paintOverlay(ZoneRenderer renderer, Graphics2D g) { AffineTransform newTransform = g.getTransform(); newTransform.concatenate(getPaintTransform(renderer)); g.setTransform(newTransform); - template.draw(g, pen); + template.draw(renderer.getZone(), g, pen); Paint paint = pen.getPaint() != null ? pen.getPaint().getPaint() : null; paintCursor(g, paint, pen.getThickness(), template.getVertex()); g.setTransform(old); @@ -357,7 +356,6 @@ protected Pen getPen() { @Override protected void detachFrom(ZoneRenderer renderer) { super.detachFrom(renderer); - template.setZoneId(null); renderer.repaint(); } @@ -366,7 +364,6 @@ protected void detachFrom(ZoneRenderer renderer) { */ @Override protected void attachTo(ZoneRenderer renderer) { - template.setZoneId(renderer.getZone().getId()); renderer.repaint(); super.attachTo(renderer); } @@ -415,7 +412,7 @@ public void mousePressed(MouseEvent e) { template.setRadius(getRadiusAtMouse(e)); ZonePoint vertex = template.getVertex(); ZonePoint newPoint = new ZonePoint(vertex.x, vertex.y); - completeDrawable(renderer.getZone().getId(), getPen(), template); + completeDrawable(getPen(), template); setIsEraser(false); resetTool(newPoint); } diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/RectangleExposeTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/RectangleExposeTool.java index 4b8dc89145..7a1336b0bb 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/RectangleExposeTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/RectangleExposeTool.java @@ -64,17 +64,17 @@ protected Pen getPen() { } @Override - protected void completeDrawable(GUID zoneId, Pen pen, Drawable drawable) { + protected void completeDrawable(Pen pen, Drawable drawable) { if (!MapTool.getPlayer().isGM()) { MapTool.showError("msg.error.fogexpose"); MapTool.getFrame().refresh(); return; } - Zone zone = MapTool.getCampaign().getZone(zoneId); + Zone zone = getZone(); - Rectangle bounds = drawable.getBounds(); + Rectangle bounds = drawable.getBounds(zone); Area area = new Area(bounds); - Set selectedToks = MapTool.getFrame().getCurrentZoneRenderer().getSelectedTokenSet(); + Set selectedToks = renderer.getSelectedTokenSet(); if (pen.isEraser()) { zone.hideArea(area, selectedToks); MapTool.serverCommand().hideFoW(zone.getId(), area, selectedToks); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/RectangleTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/RectangleTool.java index 8b8d9777dd..aa68e050a2 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/RectangleTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/RectangleTool.java @@ -94,7 +94,7 @@ public void mousePressed(MouseEvent e) { rectangle.height *= 2; } // System.out.println("Adding Rectangle to zone: " + rectangle); - completeDrawable(renderer.getZone().getId(), getPen(), new ShapeDrawable(rectangle, false)); + completeDrawable(getPen(), new ShapeDrawable(rectangle, false)); rectangle = null; } setIsEraser(isEraser(e)); diff --git a/src/main/java/net/rptools/maptool/client/tool/drawing/RectangleTopologyTool.java b/src/main/java/net/rptools/maptool/client/tool/drawing/RectangleTopologyTool.java index ce75e04439..8bf5a95d39 100644 --- a/src/main/java/net/rptools/maptool/client/tool/drawing/RectangleTopologyTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/drawing/RectangleTopologyTool.java @@ -108,6 +108,8 @@ public void mouseDragged(MouseEvent e) { @Override public void mouseMoved(MouseEvent e) { + super.mouseMoved(e); + setIsEraser(isEraser(e)); ZonePoint p = getPoint(e); diff --git a/src/main/java/net/rptools/maptool/client/tool/gridtool/GridTool.java b/src/main/java/net/rptools/maptool/client/tool/gridtool/GridTool.java index aac0cfc007..fa4a0bfcd2 100644 --- a/src/main/java/net/rptools/maptool/client/tool/gridtool/GridTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/gridtool/GridTool.java @@ -37,10 +37,12 @@ import net.rptools.maptool.client.swing.SwingUtil; import net.rptools.maptool.client.tool.DefaultTool; import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer; +import net.rptools.maptool.events.MapToolEventBus; import net.rptools.maptool.model.CellPoint; import net.rptools.maptool.model.Grid; import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.ZonePoint; +import net.rptools.maptool.model.zones.GridChanged; /** */ public class GridTool extends DefaultTool { @@ -75,7 +77,8 @@ public GridTool() { controlPanel = new AbeillePanel(new AdjustGridControlPanelView().getRootComponent()); gridSizeSpinner = (JSpinner) controlPanel.getComponent("gridSize"); - gridSizeSpinner.setModel(new SpinnerNumberModel()); + gridSizeSpinner.setModel( + new SpinnerNumberModel(100, Grid.MIN_GRID_SIZE, Grid.MAX_GRID_SIZE, 1)); gridSizeSpinner.addChangeListener(new UpdateGridListener()); gridOffsetXTextField = (JTextField) controlPanel.getComponent("offsetX"); @@ -175,6 +178,8 @@ private void copyControlPanelToGrid() { grid.setOffset(getInt(gridOffsetXTextField, 0), getInt(gridOffsetYTextField, 0)); zone.setGridColor(colorWell.getColor().getRGB()); grid.setSize(Math.max((Integer) gridSizeSpinner.getValue(), Grid.MIN_GRID_SIZE)); + + new MapToolEventBus().getMainEventBus().post(new GridChanged(zone)); } @Override @@ -297,12 +302,6 @@ public void mouseDragged(java.awt.event.MouseEvent e) { } } - @Override - public void mouseMoved(java.awt.event.MouseEvent e) { - mouseX = e.getX(); - mouseY = e.getY(); - } - //// // MOUSE WHEEL LISTENER /* diff --git a/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java b/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java index 60adb19071..b23b3e53d0 100644 --- a/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java +++ b/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java @@ -228,6 +228,8 @@ public class MapToolFrame extends DefaultDockableHolder implements WindowListene private final DragImageGlassPane dragImageGlassPane = new DragImageGlassPane(); + private boolean dockingConfigured = false; + private final class KeyListenerDeleteDraw implements KeyListener { private final JTree tree; @@ -455,8 +457,7 @@ public MapToolFrame(JMenuBar menuBar) { restorePreferences(); updateKeyStrokes(); - // This will cause the frame to be set to visible (BAD jide, BAD! No cookie for you!) - configureDocking(); + initializeFrames(); new WindowPreferences(AppConstants.APP_NAME, "mainFrame", this); chatTyperTimers = new ChatNotificationTimers(); @@ -545,8 +546,6 @@ public String getPropertyName() { } private void configureDocking() { - initializeFrames(); - getDockingManager().setProfileKey(DOCKING_PROFILE_NAME); getDockingManager().setOutlineMode(com.jidesoft.docking.DockingManager.PARTIAL_OUTLINE_MODE); getDockingManager().setUsePref(false); @@ -619,6 +618,16 @@ private void configureDocking() { /* /Issue #2485 */ } + @Override + public void setVisible(boolean b) { + if (!dockingConfigured) { + dockingConfigured = true; + configureDocking(); + } + + super.setVisible(b); + } + public DockableFrame getFrame(MTFrame frame) { return frameMap.get(frame); } @@ -1181,11 +1190,13 @@ public void mousePressed(MouseEvent e) { tree.addSelectionInterval(rowIndex, rowIndex); if (row instanceof DrawnElement && e.getClickCount() == 2) { DrawnElement de = (DrawnElement) row; + var renderer = getCurrentZoneRenderer(); + var zone = renderer.getZone(); getCurrentZoneRenderer() .centerOn( new ZonePoint( - (int) de.getDrawable().getBounds().getCenterX(), - (int) de.getDrawable().getBounds().getCenterY())); + (int) de.getDrawable().getBounds(zone).getCenterX(), + (int) de.getDrawable().getBounds(zone).getCenterY())); } /* * int[] treeRows = tree.getSelectionRows(); java.util.Arrays.sort(treeRows); drawablesPanel.clearSelectedIds(); for (int i = 0; i < treeRows.length; i++) { TreePath p = @@ -1949,8 +1960,8 @@ public void closingMaintenance() { } public void close() { - ServerDisconnectHandler.disconnectExpected = true; MapTool.disconnect(); + MapTool.stopServer(); getDockingManager() .saveLayoutDataToFile(AppUtil.getAppHome("config").getAbsolutePath() + "/layout.dat"); diff --git a/src/main/java/net/rptools/maptool/client/ui/Scale.java b/src/main/java/net/rptools/maptool/client/ui/Scale.java index cb19392ccd..10cdefac9e 100644 --- a/src/main/java/net/rptools/maptool/client/ui/Scale.java +++ b/src/main/java/net/rptools/maptool/client/ui/Scale.java @@ -17,69 +17,50 @@ import java.awt.Point; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; +import java.io.Serial; import java.io.Serializable; public class Scale implements Serializable { + public static final String PROPERTY_SCALE = "scale"; + public static final String PROPERTY_OFFSET = "offset"; + private static final int MIN_ZOOM_LEVEL = -175; + private static final int MAX_ZOOM_LEVEL = 175; private final double oneToOneScale = 1; // Let this be configurable at some point private double scale = oneToOneScale; private final double scaleIncrement = .075; private int zoomLevel = 0; - - public static final String PROPERTY_SCALE = "scale"; - public static final String PROPERTY_OFFSET = "offset"; - - private transient PropertyChangeSupport propertyChangeSupport; - private int offsetX; private int offsetY; - - private final int width; - private final int height; - - private boolean initialized; - - // LEGACY for 1.3b31 and earlier - private transient int scaleIndex; // 'transient' prevents serialization; prep for 1.4 + private transient PropertyChangeSupport propertyChangeSupport; public Scale() { - this(0, 0); - } - - public Scale(int width, int height) { - this.width = width; - this.height = height; + this.offsetX = 0; + this.offsetY = 0; } public Scale(Scale copy) { - this.width = copy.width; - this.height = copy.height; this.offsetX = copy.offsetX; this.offsetY = copy.offsetY; - this.zoomLevel = copy.zoomLevel; - this.initialized = copy.initialized; - this.scale = copy.scale; - // this.oneToOneScale = copy.oneToOneScale; - // this.scaleIncrement = copy.scaleIncrement; + setScale(copy.scale); } - public void addPropertyChangeListener(PropertyChangeListener listener) { - getPropertyChangeSupport().addPropertyChangeListener(listener); + @Serial + private Object readResolve() { + // Make sure the zoom level is correct. + setScale(this.scale); + return this; } - public void addPropertyChangeListener(String property, PropertyChangeListener listener) { - getPropertyChangeSupport().addPropertyChangeListener(property, listener); + public void addPropertyChangeListener(PropertyChangeListener listener) { + getPropertyChangeSupport().addPropertyChangeListener(listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { getPropertyChangeSupport().removePropertyChangeListener(listener); } - public void removePropertyChangeListener(String property, PropertyChangeListener listener) { - getPropertyChangeSupport().removePropertyChangeListener(property, listener); - } - public int getOffsetX() { return offsetX; } @@ -110,14 +91,28 @@ public void setScale(double scale) { } // Determine zoomLevel appropriate for given scale - zoomLevel = (int) Math.round(Math.log(scale / oneToOneScale) / Math.log(1 + scaleIncrement)); - - setScaleNoZoomLevel(scale); + var zoomLevel = + (int) Math.round(Math.log(scale / oneToOneScale) / Math.log(1 + scaleIncrement)); + // Check that we haven't gone out of bounds with our zooming. + if (zoomLevel < MIN_ZOOM_LEVEL) { + setZoomLevel(MIN_ZOOM_LEVEL); + } else if (zoomLevel > MAX_ZOOM_LEVEL) { + setZoomLevel(MAX_ZOOM_LEVEL); + } else { + // Acceptable scale. Use it. + var oldScale = this.scale; + this.scale = scale; + this.zoomLevel = zoomLevel; + getPropertyChangeSupport().firePropertyChange(PROPERTY_SCALE, oldScale, this.scale); + } } - private void setScaleNoZoomLevel(double scale) { + private void setScaleFromZoomLevel() { double oldScale = this.scale; - this.scale = scale; + + // Check for zero just to avoid any possible imprecision. + this.scale = + zoomLevel == 0 ? oneToOneScale : oneToOneScale * Math.pow(1 + scaleIncrement, zoomLevel); getPropertyChangeSupport().firePropertyChange(PROPERTY_SCALE, oldScale, scale); } @@ -126,29 +121,28 @@ public double getOneToOneScale() { return oneToOneScale; } - public double reset() { - double oldScale = this.scale; - scale = oneToOneScale; - zoomLevel = 0; + public void reset() { + setZoomLevel(0); + } - getPropertyChangeSupport().firePropertyChange(PROPERTY_SCALE, oldScale, scale); - return oldScale; + private void setZoomLevel(int zoomLevel) { + this.zoomLevel = Math.clamp(zoomLevel, MIN_ZOOM_LEVEL, MAX_ZOOM_LEVEL); + System.out.println(this.zoomLevel); + setScaleFromZoomLevel(); } - public double scaleUp() { - zoomLevel++; - setScaleNoZoomLevel(oneToOneScale * Math.pow(1 + scaleIncrement, zoomLevel)); - return scale; + private void scaleUp() { + setZoomLevel(zoomLevel + 1); } - public double scaleDown() { - zoomLevel--; - setScaleNoZoomLevel(oneToOneScale * Math.pow(1 + scaleIncrement, zoomLevel)); - return scale; + private void scaleDown() { + setZoomLevel(zoomLevel - 1); } public void zoomReset(int x, int y) { - zoomTo(x, y, reset()); + var oldScale = scale; + reset(); + zoomTo(x, y, oldScale); } public void zoomIn(int x, int y) { @@ -169,10 +163,6 @@ public void zoomScale(int x, int y, double scale) { zoomTo(x, y, oldScale); } - public boolean isInitialized() { - return initialized; - } - private PropertyChangeSupport getPropertyChangeSupport() { if (propertyChangeSupport == null) { propertyChangeSupport = new PropertyChangeSupport(this); @@ -180,37 +170,6 @@ private PropertyChangeSupport getPropertyChangeSupport() { return propertyChangeSupport; } - /** - * Fit the image into the given space by finding the zoom level that allows the image to fit. Then - * center the image. - * - * @param width width of the image - * @param height height of the image - * @return true if this call did something, false if the init has already been called - */ - public boolean initialize(int width, int height) { - - if (initialized) { - return false; - } - - centerIn(width, height); - - initialized = true; - return true; - } - - public void centerIn(int width, int height) { - - int currWidth = (int) (this.width * getScale()); - int currHeight = (int) (this.height * getScale()); - - int x = (width - currWidth) / 2; - int y = (height - currHeight) / 2; - - setOffset(x, y); - } - private void zoomTo(int x, int y, double oldScale) { // Keep the current pixel centered diff --git a/src/main/java/net/rptools/maptool/client/ui/adjustgrid/AdjustGridPanel.java b/src/main/java/net/rptools/maptool/client/ui/adjustgrid/AdjustGridPanel.java deleted file mode 100644 index 722de0ce69..0000000000 --- a/src/main/java/net/rptools/maptool/client/ui/adjustgrid/AdjustGridPanel.java +++ /dev/null @@ -1,467 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.client.ui.adjustgrid; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Point; -import java.awt.event.ActionEvent; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.MouseMotionListener; -import java.awt.event.MouseWheelEvent; -import java.awt.event.MouseWheelListener; -import java.awt.image.BufferedImage; -import java.beans.PropertyChangeListener; -import java.beans.PropertyChangeSupport; -import javax.swing.AbstractAction; -import javax.swing.JComponent; -import javax.swing.SwingUtilities; -import net.rptools.maptool.client.AppActions; -import net.rptools.maptool.client.swing.SwingUtil; -import net.rptools.maptool.client.ui.Scale; - -public class AdjustGridPanel extends JComponent - implements MouseListener, MouseMotionListener, MouseWheelListener { - - private static final int MINIMUM_GRID_SIZE = 5; - - private enum Direction { - Increase, - Decrease - } - - public static final String PROPERTY_GRID_OFFSET_X = "gridOffsetX"; - public static final String PROPERTY_GRID_OFFSET_Y = "gridOffsetY"; - public static final String PROPERTY_GRID_SIZE = "gridSize"; - public static final String PROPERTY_ZOOM = "zoom"; - - private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this); - - private int gridOffsetX = 0; - private int gridOffsetY = 0; - private int gridSize = 40; - private boolean showGrid = true; - - private int mouseX; - private int mouseY; - - private int dragStartX; - private int dragStartY; - private int dragOffsetX; - private int dragOffsetY; - - private Color gridColor = Color.darkGray; - - private BufferedImage image; - - private Scale scale; - - public AdjustGridPanel() { - setOpaque(false); - addMouseListener(this); - addMouseMotionListener(this); - addMouseWheelListener(this); - - getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) - .put(AppActions.ZOOM_OUT.getKeyStroke(), "zoomOut"); - getActionMap() - .put( - "zoomOut", - new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - zoomOut(); - } - }); - - getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(AppActions.ZOOM_IN.getKeyStroke(), "zoomIn"); - getActionMap() - .put( - "zoomIn", - new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - zoomIn(); - } - }); - - getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) - .put(AppActions.ZOOM_RESET.getKeyStroke(), "zoomReset"); - getActionMap() - .put( - "zoomReset", - new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - zoomReset(); - } - }); - } - - public void setZoneImage(BufferedImage image) { - this.image = image; - - scale = new Scale(image.getWidth(), image.getHeight()); - } - - public void setZoomIndex(int index) { - scale.setScale(index); - repaint(); - } - - @Override - protected void paintComponent(Graphics g) { - - if (image == null) { - return; - } - - Dimension size = getSize(); - - if (scale.initialize(size.width, size.height)) { - propertyChangeSupport.firePropertyChange(PROPERTY_ZOOM, 0, (int) scale.getScale()); - } - - // CALCULATIONS - Dimension imageSize = getScaledImageSize(); - Point imagePosition = getScaledImagePosition(); - - double imgRatio = scale.getScale(); - - // SETUP - Graphics2D g2d = (Graphics2D) g; - - // BG FILL - g2d.setColor(getBackground()); - g2d.fillRect(0, 0, size.width, size.height); - - // IMAGE - g2d.drawImage(image, imagePosition.x, imagePosition.y, imageSize.width, imageSize.height, null); - g2d.setColor(Color.black); - g2d.drawRect(imagePosition.x, imagePosition.y, imageSize.width, imageSize.height); - - // GRID - g2d.setColor(gridColor); - double gridSize = this.gridSize * imgRatio; - - // across - int x = imagePosition.x + (int) (gridOffsetX * imgRatio); - for (double i = gridSize; i <= imageSize.width; i += gridSize) { - g2d.drawLine( - x + (int) i, imagePosition.y, x + (int) i, imagePosition.y + imageSize.height - 1); - } - - // down - int y = imagePosition.y + (int) (gridOffsetY * imgRatio); - for (double i = gridSize; i <= imageSize.height; i += gridSize) { - g2d.drawLine( - imagePosition.x, y + (int) i, imagePosition.x + imageSize.width - 1, y + (int) i); - } - } - - public void setGridColor(Color color) { - gridColor = color; - } - - @Override - public boolean isFocusable() { - return true; - } - - public void setGridOffset(int offsetX, int offsetY) { - gridOffsetX = offsetX; - gridOffsetY = offsetY; - - repaint(); - } - - public int getGridSize() { - return gridSize; - } - - public int getGridOffsetX() { - return gridOffsetX; - } - - public int getGridOffsetY() { - return gridOffsetY; - } - - public void setGridSize(int size) { - gridSize = Math.max(MINIMUM_GRID_SIZE, size); - repaint(); - } - - private Dimension getScaledImageSize() { - - Dimension imageSize = new Dimension(image.getWidth(), image.getHeight()); - imageSize.width *= scale.getScale(); - imageSize.height *= scale.getScale(); - - return imageSize; - } - - private Point getScaledImagePosition() { - - int imgX = scale.getOffsetX(); - int imgY = scale.getOffsetY(); - - return new Point(imgX, imgY); - } - - public void zoomIn() { - scale.zoomIn(mouseX, mouseY); - repaint(); - } - - public void zoomOut() { - scale.zoomOut(mouseX, mouseY); - repaint(); - } - - public void zoomReset() { - scale.reset(); - repaint(); - } - - public void moveGridBy(int dx, int dy) { - - int oldOffsetX = gridOffsetX; - int oldOffsetY = gridOffsetY; - - gridOffsetX += dx; - gridOffsetY += dy; - - gridOffsetX %= gridSize; - gridOffsetY %= gridSize; - - if (gridOffsetY > 0) { - gridOffsetY = gridOffsetY - gridSize; - } - - if (gridOffsetX > 0) { - gridOffsetX = gridOffsetX - gridSize; - } - - repaint(); - - propertyChangeSupport.firePropertyChange(PROPERTY_GRID_OFFSET_X, oldOffsetX, gridOffsetX); - propertyChangeSupport.firePropertyChange(PROPERTY_GRID_OFFSET_Y, oldOffsetY, gridOffsetY); - } - - public void adjustGridSize(int delta) { - - int oldSize = gridSize; - gridSize = Math.max(MINIMUM_GRID_SIZE, gridSize + delta); - - repaint(); - propertyChangeSupport.firePropertyChange(PROPERTY_GRID_SIZE, oldSize, gridSize); - } - - private void adjustGridSize(Direction direction) { - - Point imagePosition = getScaledImagePosition(); - - double gridSize = this.gridSize * scale.getScale(); - - int cellX = (int) ((mouseX - imagePosition.x - gridOffsetX) / gridSize); - int cellY = (int) ((mouseY - imagePosition.y - gridOffsetY) / gridSize); - - switch (direction) { - case Increase: - adjustGridSize(1); - - if (this.gridSize != gridSize) { - moveGridBy(-cellX, -cellY); - } - break; - case Decrease: - adjustGridSize(-1); - - if (this.gridSize != gridSize) { - moveGridBy(cellX, cellY); - } - break; - } - } - - //// - // MOUSE LISTENER - @Override - public void mouseClicked(MouseEvent e) {} - - @Override - public void mouseEntered(MouseEvent e) {} - - @Override - public void mouseExited(MouseEvent e) {} - - @Override - public void mousePressed(MouseEvent e) { - - mouseX = e.getX(); - mouseY = e.getY(); - - dragStartX = e.getX(); - dragStartY = e.getY(); - - Point imagePosition = getScaledImagePosition(); - - int x = (int) ((e.getX() - imagePosition.x) / scale.getScale() - gridOffsetX); - int y = (int) ((e.getY() - imagePosition.y) / scale.getScale() - gridOffsetY); - - dragOffsetX = x % gridSize; - dragOffsetY = y % gridSize; - } - - @Override - public void mouseReleased(MouseEvent e) {} - - //// - // MOUSE MOTION LISTENER - @Override - public void mouseDragged(MouseEvent e) { - - if (SwingUtilities.isLeftMouseButton(e)) { - - Point imagePosition = getScaledImagePosition(); - - int x = (int) ((e.getX() - imagePosition.x) / scale.getScale() - dragOffsetX); - int y = (int) ((e.getY() - imagePosition.y) / scale.getScale() - dragOffsetY); - - int oldOffsetX = gridOffsetX; - int oldOffsetY = gridOffsetY; - - gridOffsetX = x % gridSize; - gridOffsetY = y % gridSize; - - if (gridOffsetY > 0) { - gridOffsetY = gridOffsetY - gridSize; - } - - if (gridOffsetX > 0) { - gridOffsetX = gridOffsetX - gridSize; - } - - repaint(); - propertyChangeSupport.firePropertyChange(PROPERTY_GRID_OFFSET_X, oldOffsetX, gridOffsetX); - propertyChangeSupport.firePropertyChange(PROPERTY_GRID_OFFSET_Y, oldOffsetY, gridOffsetY); - } else { - int offsetX = scale.getOffsetX() + e.getX() - dragStartX; - int offsetY = scale.getOffsetY() + e.getY() - dragStartY; - - scale.setOffset(offsetX, offsetY); - - dragStartX = e.getX(); - dragStartY = e.getY(); - - repaint(); - } - } - - @Override - public void mouseMoved(MouseEvent e) { - - Dimension imgSize = getScaledImageSize(); - Point imgPos = getScaledImagePosition(); - - boolean insideMap = - e.getX() > imgPos.x - && e.getX() < imgPos.x + imgSize.width - && e.getY() > imgPos.y - && e.getY() < imgPos.y + imgSize.height; - if ((insideMap && showGrid) || (!insideMap && !showGrid)) { - showGrid = !insideMap; - repaint(); - } - - mouseX = e.getX(); - mouseY = e.getY(); - } - - //// - // MOUSE WHEEL LISTENER - @Override - public void mouseWheelMoved(MouseWheelEvent e) { - // Fix for hi-res mice - if (e.getWheelRotation() == 0) { - return; - } - if (SwingUtil.isControlDown(e)) { - - double oldScale = scale.getScale(); - if (e.getWheelRotation() > 0) { - scale.zoomOut(e.getX(), e.getY()); - } else { - scale.zoomIn(e.getX(), e.getY()); - } - propertyChangeSupport.firePropertyChange(PROPERTY_ZOOM, oldScale, scale.getScale()); - } else { - - if (e.getWheelRotation() > 0) { - - adjustGridSize(Direction.Increase); - } else { - - adjustGridSize(Direction.Decrease); - } - } - repaint(); - } - - //// - // PROPERTY CHANGE SUPPORT - @Override - public void addPropertyChangeListener(PropertyChangeListener listener) { - propertyChangeSupport.addPropertyChangeListener(listener); - } - - @Override - public void removePropertyChangeListener(PropertyChangeListener listener) { - propertyChangeSupport.removePropertyChangeListener(listener); - } - - @Override - public void addPropertyChangeListener(String name, PropertyChangeListener listener) { - propertyChangeSupport.addPropertyChangeListener(name, listener); - } - - @Override - public void removePropertyChangeListener(String name, PropertyChangeListener listener) { - propertyChangeSupport.removePropertyChangeListener(name, listener); - } - /* - * private final Map KEYSTROKES = new HashMap() { { put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.SHIFT_DOWN_MASK), new GridSizeAction(Size.Decrease)); - * put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.SHIFT_DOWN_MASK), new GridSizeAction(Size.Decrease)); put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, KeyEvent.SHIFT_DOWN_MASK), new - * GridSizeAction(Size.Increase)); put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.SHIFT_DOWN_MASK), new GridSizeAction(Size.Increase)); put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), new - * GridOffsetAction(GridOffsetAction.Direction.Up)); put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), new GridOffsetAction(GridOffsetAction.Direction.Left)); - * put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), new GridOffsetAction(GridOffsetAction.Direction.Down)); put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), new - * GridOffsetAction(GridOffsetAction.Direction.Right)); } }; protected Map getKeyActionMap() { return KEYSTROKES; } - * - * private final class GridSizeAction extends AbstractAction { private final Size size; public GridSizeAction(Size size) { this.size = size; } - * - * public void actionPerformed(ActionEvent e) { ZoneRenderer renderer = (ZoneRenderer) e.getSource(); adjustGridSize(renderer, size); } } - * - * private static final class GridOffsetAction extends AbstractAction { private static enum Direction { Left, Right, Up, Down }; private final Direction direction; - * - * public GridOffsetAction(Direction direction) { this.direction = direction; } - * - * public void actionPerformed(ActionEvent e) { ZoneRenderer renderer = (ZoneRenderer) e.getSource(); switch (direction) { case Left: renderer.moveGridBy(-1, 0); break; case Right: - * renderer.moveGridBy(1, 0); break; case Up: renderer.moveGridBy(0, -1); break; case Down: renderer.moveGridBy(0, 1); break; } } } - */ -} diff --git a/src/main/java/net/rptools/maptool/client/ui/adjustgrid/AdvancedAdjustGridPanel.java b/src/main/java/net/rptools/maptool/client/ui/adjustgrid/AdvancedAdjustGridPanel.java deleted file mode 100644 index d8a89beef0..0000000000 --- a/src/main/java/net/rptools/maptool/client/ui/adjustgrid/AdvancedAdjustGridPanel.java +++ /dev/null @@ -1,468 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.client.ui.adjustgrid; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Point; -import java.awt.Rectangle; -import java.awt.RenderingHints; -import java.awt.event.ActionEvent; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.MouseMotionListener; -import java.awt.event.MouseWheelEvent; -import java.awt.event.MouseWheelListener; -import java.awt.image.BufferedImage; -import javax.swing.AbstractAction; -import javax.swing.JComponent; -import javax.swing.SwingUtilities; -import net.rptools.maptool.client.AppActions; -import net.rptools.maptool.client.ui.Scale; - -public class AdvancedAdjustGridPanel extends JComponent - implements MouseListener, MouseMotionListener, MouseWheelListener { - - private static final int MINIMUM_GRID_SIZE = 5; - - private int gridCountX = 10; - private int gridCountY = 10; - private boolean showGrid = true; - - private int mouseX; - private int mouseY; - - private BufferedImage image; - - private Scale scale; - - private enum Handle { - TOP, - BOTTOM, - LEFT, - RIGHT - } - - private int topGrid; - private int bottomGrid; - private int leftGrid; - private int rightGrid; - - private boolean showRows = true; - private boolean showCols = true; - - private Handle draggingHandle; - - public AdvancedAdjustGridPanel() { - setOpaque(false); - addMouseListener(this); - addMouseMotionListener(this); - addMouseWheelListener(this); - - getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) - .put(AppActions.ZOOM_OUT.getKeyStroke(), "zoomOut"); - getActionMap() - .put( - "zoomOut", - new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - zoomOut(); - } - }); - - getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(AppActions.ZOOM_IN.getKeyStroke(), "zoomIn"); - getActionMap() - .put( - "zoomIn", - new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - zoomIn(); - } - }); - - getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) - .put(AppActions.ZOOM_RESET.getKeyStroke(), "zoomReset"); - getActionMap() - .put( - "zoomReset", - new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - zoomReset(); - } - }); - } - - public Rectangle getGridBounds() { - return new Rectangle(topGrid, leftGrid, rightGrid - leftGrid, bottomGrid - topGrid); - } - - public void setZoneImage(BufferedImage image) { - this.image = image; - - topGrid = 0; - bottomGrid = image.getHeight(); - leftGrid = 0; - rightGrid = image.getWidth(); - - scale = new Scale(image.getWidth(), image.getHeight()); - } - - public void setShowRows(boolean show) { - showRows = show; - } - - public void setShowCols(boolean show) { - showCols = show; - } - - @Override - protected void paintComponent(Graphics g) { - - if (image == null) { - return; - } - - Dimension size = getSize(); - - scale.initialize(size.width, size.height); - - // CALCULATIONS - Dimension imageSize = getScaledImageSize(); - Point imagePosition = getScaledImagePosition(); - - double imgRatio = scale.getScale(); - - // handles - int top = (int) (topGrid * imgRatio); - int bottom = (int) (bottomGrid * imgRatio); - int left = (int) (leftGrid * imgRatio); - int right = (int) (rightGrid * imgRatio); - - // SETUP - Graphics2D g2d = (Graphics2D) g; - - // BG FILL - g2d.setColor(getBackground()); - g2d.fillRect(0, 0, size.width, size.height); - - // IMAGE - g2d.drawImage(image, imagePosition.x, imagePosition.y, imageSize.width, imageSize.height, null); - - // GRID - g2d.setColor(Color.blue); - double dx = ((rightGrid - leftGrid) / (float) gridCountX) * imgRatio; - double dy = ((bottomGrid - topGrid) / (float) gridCountY) * imgRatio; - - // across - if (showCols) { - int x = imagePosition.x + left; - for (int i = 0; i < gridCountX; i++) { - g2d.drawLine( - x + (int) (i * dx), - imagePosition.y + top, - x + (int) (i * dx), - imagePosition.y + bottom); - } - } - - // down - if (showRows) { - int y = imagePosition.y + top; - for (int i = 0; i < gridCountY; i++) { - g2d.drawLine( - imagePosition.x + left, - y + (int) (i * dy), - imagePosition.x + right, - y + (int) (i * dy)); - } - } - - // HANDLES - int handleSize = 10; - - Object oldValue = g2d.getRenderingHint(RenderingHints.KEY_ANTIALIASING); - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - g2d.setColor(Color.red); - - // Top - int halfHandleX = imagePosition.x + imageSize.width / 2; - g2d.fillRect(halfHandleX - handleSize, top + imagePosition.y - 3, handleSize * 2, 3); - g2d.drawLine( - imagePosition.x - handleSize, - top + imagePosition.y, - imagePosition.x + imageSize.width + handleSize, - top + imagePosition.y); - - // Bottom - g2d.fillRect(halfHandleX - handleSize, bottom + imagePosition.y + 1, handleSize * 2, 3); - g2d.drawLine( - imagePosition.x - handleSize, - bottom + imagePosition.y, - imagePosition.x + imageSize.width + handleSize, - bottom + imagePosition.y); - - // Left - int halfHandleY = imagePosition.y + imageSize.height / 2; - g2d.fillRect(left + imagePosition.x - 3, halfHandleY - handleSize, 3, handleSize * 2); - g2d.drawLine( - left + imagePosition.x, - imagePosition.y - handleSize, - left + imagePosition.x, - imagePosition.y + imageSize.height + handleSize); - - // Right - g2d.fillRect(right + 1 + imagePosition.x, halfHandleY - handleSize, 3, handleSize * 2); - g2d.drawLine( - right + imagePosition.x, - imagePosition.y - handleSize, - right + imagePosition.x, - imagePosition.y + imageSize.height + handleSize); - - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldValue); - } - - @Override - public boolean isFocusable() { - return true; - } - - public void setGridCountX(int count) { - gridCountX = count; - repaint(); - } - - public void setGridCountY(int count) { - gridCountY = count; - repaint(); - } - - private float getScaledImageRatio() { - return getScaledImageSize().width / (float) image.getWidth(); - } - - private Dimension getScaledImageSize() { - - Dimension imageSize = new Dimension(image.getWidth(), image.getHeight()); - imageSize.width *= scale.getScale(); - imageSize.height *= scale.getScale(); - - return imageSize; - } - - private Point getScaledImagePosition() { - Dimension size = getSize(); - Dimension imageSize = getScaledImageSize(); - - int imgX = scale.getOffsetX(); - int imgY = scale.getOffsetY(); - - return new Point(imgX, imgY); - } - - private void updateHandles(MouseEvent e) { - // Convert - float imgRatio = getScaledImageRatio(); - Point imgPosition = getScaledImagePosition(); - - Point location = e.getPoint(); - location.translate(-imgPosition.x, -imgPosition.y); - - location = new Point((int) (location.x / imgRatio), (int) (location.y / imgRatio)); - - switch (draggingHandle) { - case TOP: - { - if (location.y < 0) { - location.y = 0; - } - if (location.y > bottomGrid - MINIMUM_GRID_SIZE) { - location.y = bottomGrid - MINIMUM_GRID_SIZE; - } - - topGrid = location.y; - break; - } - case BOTTOM: - { - if (location.y < topGrid + MINIMUM_GRID_SIZE) { - location.y = topGrid + MINIMUM_GRID_SIZE; - } - if (location.y > image.getHeight()) { - location.y = image.getHeight(); - } - - bottomGrid = location.y; - break; - } - case LEFT: - { - if (location.x < 0) { - location.x = 0; - } - if (location.x > rightGrid - MINIMUM_GRID_SIZE) { - location.x = rightGrid - MINIMUM_GRID_SIZE; - } - - leftGrid = location.x; - break; - } - case RIGHT: - { - if (location.x < leftGrid + MINIMUM_GRID_SIZE) { - location.x = leftGrid + MINIMUM_GRID_SIZE; - } - if (location.x > image.getWidth()) { - location.x = image.getWidth(); - } - - rightGrid = location.x; - break; - } - } - repaint(); - } - - public void zoomIn() { - scale.zoomIn(mouseX, mouseY); - repaint(); - } - - public void zoomOut() { - scale.zoomOut(mouseX, mouseY); - repaint(); - } - - public void zoomReset() { - scale.reset(); - repaint(); - } - - //// - // MOUSE LISTENER - @Override - public void mouseClicked(MouseEvent e) {} - - @Override - public void mouseEntered(MouseEvent e) {} - - @Override - public void mouseExited(MouseEvent e) {} - - @Override - public void mousePressed(MouseEvent e) { - - if (SwingUtilities.isLeftMouseButton(e)) { - float imgRatio = getScaledImageRatio(); - Point imgPos = getScaledImagePosition(); - - int top = (int) (topGrid * imgRatio) + imgPos.y; - int bottom = (int) (bottomGrid * imgRatio) + imgPos.y; - int left = (int) (leftGrid * imgRatio) + imgPos.x; - int right = (int) (rightGrid * imgRatio) + imgPos.x; - - int distTop = Math.abs(e.getY() - top); - int distBottom = Math.abs(e.getY() - bottom); - int distLeft = Math.abs(e.getX() - left); - int distRight = Math.abs(e.getX() - right); - - int dist = distTop; - draggingHandle = Handle.TOP; - - if (distBottom < dist) { - dist = distBottom; - draggingHandle = Handle.BOTTOM; - } - if (distLeft < dist) { - dist = distLeft; - draggingHandle = Handle.LEFT; - } - if (distRight < dist) { - dist = distRight; - draggingHandle = Handle.RIGHT; - } - - updateHandles(e); - } else { - mouseX = e.getX(); - mouseY = e.getY(); - } - } - - @Override - public void mouseReleased(MouseEvent e) { - draggingHandle = null; - - repaint(); - } - - //// - // MOUSE MOTION LISTENER - @Override - public void mouseDragged(MouseEvent e) { - - if (SwingUtilities.isLeftMouseButton(e)) { - updateHandles(e); - } else { - int offsetX = scale.getOffsetX() + e.getX() - mouseX; - int offsetY = scale.getOffsetY() + e.getY() - mouseY; - - scale.setOffset(offsetX, offsetY); - - mouseX = e.getX(); - mouseY = e.getY(); - } - - repaint(); - } - - @Override - public void mouseMoved(MouseEvent e) { - - Dimension imgSize = getScaledImageSize(); - Point imgPos = getScaledImagePosition(); - - boolean insideMap = - e.getX() > imgPos.x - && e.getX() < imgPos.x + imgSize.width - && e.getY() > imgPos.y - && e.getY() < imgPos.y + imgSize.height; - if ((insideMap && showGrid) || (!insideMap && !showGrid)) { - showGrid = !insideMap; - repaint(); - } - - mouseX = e.getX(); - mouseY = e.getY(); - } - - //// - // MOUSE WHEEL LISTENER - @Override - public void mouseWheelMoved(MouseWheelEvent e) { - if (e.getWheelRotation() < 0) { - scale.zoomIn(e.getX(), e.getY()); - } else if (e.getWheelRotation() > 0) { - scale.zoomOut(e.getX(), e.getY()); - } - - repaint(); - } -} diff --git a/src/main/java/net/rptools/maptool/client/ui/connectioninfodialog/ConnectionInfoDialog.java b/src/main/java/net/rptools/maptool/client/ui/connectioninfodialog/ConnectionInfoDialog.java index d966c4a8a4..8ab2d5f611 100644 --- a/src/main/java/net/rptools/maptool/client/ui/connectioninfodialog/ConnectionInfoDialog.java +++ b/src/main/java/net/rptools/maptool/client/ui/connectioninfodialog/ConnectionInfoDialog.java @@ -103,8 +103,8 @@ public ConnectionInfoDialog(MapToolServer server) { JTextField portLabel = panel.getTextField("port"); externalAddressLabel = panel.getTextField("externalAddress"); - String name = server.getConfig().getServerName(); - if (name == null) { + String name = server.getName(); + if (name == null || name.isEmpty()) { name = "---"; } @@ -122,14 +122,14 @@ public ConnectionInfoDialog(MapToolServer server) { log.warn("Can't resolve our own IPv6 address!?", e); } - String port = - MapTool.isPersonalServer() ? "---" : Integer.toString(server.getConfig().getPort()); + int port = server.getPort(); + String portString = port < 0 ? "---" : Integer.toString(port); nameLabel.setText(name); localv4AddressLabel.setText(localv4Address); localv6AddressLabel.setText(localv6Address); externalAddressLabel.setText(I18N.getText("ConnectionInfoDialog.discovering")); - portLabel.setText(port); + portLabel.setText(portString); JButton okButton = (JButton) panel.getButton("okButton"); bindOKButtonActions(okButton); diff --git a/src/main/java/net/rptools/maptool/client/ui/connections/ClientConnectionPanel.java b/src/main/java/net/rptools/maptool/client/ui/connections/ClientConnectionPanel.java index ef6771b347..f2a5d95fa1 100644 --- a/src/main/java/net/rptools/maptool/client/ui/connections/ClientConnectionPanel.java +++ b/src/main/java/net/rptools/maptool/client/ui/connections/ClientConnectionPanel.java @@ -33,7 +33,7 @@ import net.rptools.maptool.client.events.PlayerConnected; import net.rptools.maptool.client.events.PlayerDisconnected; import net.rptools.maptool.client.events.PlayerStatusChanged; -import net.rptools.maptool.client.events.ServerStopped; +import net.rptools.maptool.client.events.ServerDisconnected; import net.rptools.maptool.client.swing.PopupListener; import net.rptools.maptool.events.MapToolEventBus; import net.rptools.maptool.language.I18N; @@ -128,7 +128,7 @@ private void onPlayerDisconnected(PlayerDisconnected event) { } @Subscribe - private void onServerStopped(ServerStopped event) { + private void onServerDisconnected(ServerDisconnected event) { listModel.clear(); } diff --git a/src/main/java/net/rptools/maptool/client/ui/drawpanel/DrawPanelPopupMenu.java b/src/main/java/net/rptools/maptool/client/ui/drawpanel/DrawPanelPopupMenu.java index 520271eb8c..671e9a5e3a 100644 --- a/src/main/java/net/rptools/maptool/client/ui/drawpanel/DrawPanelPopupMenu.java +++ b/src/main/java/net/rptools/maptool/client/ui/drawpanel/DrawPanelPopupMenu.java @@ -279,14 +279,14 @@ public void actionPerformed(ActionEvent e) { // only bother doing stuff if more than one selected List drawableList = renderer.getZone().getAllDrawnElements(); Iterator iter = drawableList.iterator(); - Area a = elementUnderMouse.getDrawable().getArea(); + Area a = elementUnderMouse.getDrawable().getArea(renderer.getZone()); while (iter.hasNext()) { DrawnElement de = iter.next(); if (selectedDrawSet.contains(de.getDrawable().getId())) { renderer.getZone().removeDrawable(de.getDrawable().getId()); MapTool.serverCommand().undoDraw(renderer.getZone().getId(), de.getDrawable().getId()); de.getDrawable().setLayer(elementUnderMouse.getDrawable().getLayer()); - if (!de.equals(elementUnderMouse)) a.add(de.getDrawable().getArea()); + if (!de.equals(elementUnderMouse)) a.add(de.getDrawable().getArea(renderer.getZone())); } } Shape s = a; diff --git a/src/main/java/net/rptools/maptool/client/ui/drawpanel/DrawPanelTreeModel.java b/src/main/java/net/rptools/maptool/client/ui/drawpanel/DrawPanelTreeModel.java index 296bf97335..b0c4c3747c 100644 --- a/src/main/java/net/rptools/maptool/client/ui/drawpanel/DrawPanelTreeModel.java +++ b/src/main/java/net/rptools/maptool/client/ui/drawpanel/DrawPanelTreeModel.java @@ -29,7 +29,6 @@ import javax.swing.tree.TreePath; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.events.MapToolEventBus; -import net.rptools.maptool.language.I18N; import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.drawing.AbstractTemplate; import net.rptools.maptool.model.drawing.DrawablesGroup; @@ -56,18 +55,12 @@ public static View getByLayer(Zone.Layer layer) { return byLayer.get(layer); } - private final String displayName; private final Zone.Layer layer; private View(Zone.Layer layer) { - this.displayName = I18N.getText("panel.DrawExplorer.View." + layer.name()); this.layer = layer; } - public String getDisplayName() { - return displayName; - } - public Zone.Layer getLayer() { return layer; } diff --git a/src/main/java/net/rptools/maptool/client/ui/drawpanel/DrawablesPanel.java b/src/main/java/net/rptools/maptool/client/ui/drawpanel/DrawablesPanel.java index 6c80cb4b2a..de27f8cf96 100644 --- a/src/main/java/net/rptools/maptool/client/ui/drawpanel/DrawablesPanel.java +++ b/src/main/java/net/rptools/maptool/client/ui/drawpanel/DrawablesPanel.java @@ -80,12 +80,12 @@ protected void paintComponent(Graphics g) { } if (drawableList.size() > 0) { Collections.reverse(drawableList); - Rectangle bounds = getBounds(drawableList); + Rectangle bounds = getBounds(zone, drawableList); double scale = (double) Math.min(MAX_PANEL_SIZE, getSize().width) / (double) bounds.width; if ((bounds.height * scale) > MAX_PANEL_SIZE) scale = (double) Math.min(MAX_PANEL_SIZE, getSize().height) / (double) bounds.height; - g.drawImage(drawDrawables(drawableList, bounds, scale, onlyCuts), 0, 0, null); + g.drawImage(drawDrawables(zone, drawableList, bounds, scale, onlyCuts), 0, 0, null); } } } @@ -93,7 +93,11 @@ protected void paintComponent(Graphics g) { } private BufferedImage drawDrawables( - List drawableList, Rectangle viewport, double scale, boolean showEraser) { + Zone zone, + List drawableList, + Rectangle viewport, + double scale, + boolean showEraser) { BufferedImage backBuffer = new BufferedImage( (int) (viewport.width * scale), @@ -126,24 +130,33 @@ private BufferedImage drawDrawables( if (drawable instanceof DrawablesGroup) { g.drawImage( drawDrawables( - ((DrawablesGroup) drawable).getDrawableList(), new Rectangle(viewport), 1, false), + zone, + ((DrawablesGroup) drawable).getDrawableList(), + new Rectangle(viewport), + 1, + false), viewport.x, viewport.y, null); - } else drawable.draw(g, pen); + } else { + drawable.draw(zone, g, pen); + } g.setComposite(oldComposite); } g.dispose(); return backBuffer; } - private Rectangle getBounds(List drawableList) { + private Rectangle getBounds(Zone zone, List drawableList) { Rectangle bounds = null; for (DrawnElement element : drawableList) { // Empty drawables are created by right clicking during the draw process // and need to be skipped. - if (element.getDrawable().getBounds() == null) continue; - Rectangle drawnBounds = new Rectangle(element.getDrawable().getBounds()); + Rectangle drawnBounds = element.getDrawable().getBounds(zone); + if (drawnBounds == null) { + continue; + } + drawnBounds = new Rectangle(drawnBounds); // Handle pen size Pen pen = element.getPen(); int penSize = pen.getForegroundMode() == Pen.MODE_TRANSPARENT ? 0 : (int) pen.getThickness(); diff --git a/src/main/java/net/rptools/maptool/client/ui/exportdialog/ExportDialog.java b/src/main/java/net/rptools/maptool/client/ui/exportdialog/ExportDialog.java index 4ec0f65da0..cbf494e71c 100644 --- a/src/main/java/net/rptools/maptool/client/ui/exportdialog/ExportDialog.java +++ b/src/main/java/net/rptools/maptool/client/ui/exportdialog/ExportDialog.java @@ -883,7 +883,7 @@ public Rectangle zoneExtents(PlayerView view) { } Drawable drawable = element.getDrawable(); - Rectangle drawnBounds = new Rectangle(drawable.getBounds()); + Rectangle drawnBounds = new Rectangle(drawable.getBounds(zone)); // Handle pen size // This slightly over-estimates the size of the pen, but we want to diff --git a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLFrame.java b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLFrame.java index ce95f272b4..a3b2ba9cbc 100644 --- a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLFrame.java +++ b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLFrame.java @@ -310,7 +310,7 @@ public void updateContents( /** Run all callback macros for "onChangeSelection". */ public static void doSelectedChanged() { for (HTMLFrame frame : frames.values()) { - if (frame.isVisible()) { + if (!frame.isHidden()) { HTMLPanelContainer.selectedChanged(frame.macroCallbacks); } } @@ -319,7 +319,7 @@ public static void doSelectedChanged() { /** Run all callback macros for "onChangeImpersonated". */ public static void doImpersonatedChanged() { for (HTMLFrame frame : frames.values()) { - if (frame.isVisible()) { + if (!frame.isHidden()) { HTMLPanelContainer.impersonatedChanged(frame.macroCallbacks); } } @@ -333,7 +333,7 @@ public static void doImpersonatedChanged() { public static void doTokenChanged(Token token) { if (token != null) { for (HTMLFrame frame : frames.values()) { - if (frame.isVisible()) { + if (!frame.isHidden()) { HTMLPanelContainer.tokenChanged(token, frame.macroCallbacks); } } diff --git a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLWebViewManager.java b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLWebViewManager.java index e6b16cd3dd..942e473cda 100644 --- a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLWebViewManager.java +++ b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLWebViewManager.java @@ -17,21 +17,31 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.sun.webkit.WebPage; import com.sun.webkit.dom.HTMLSelectElementImpl; import java.awt.*; import java.awt.event.ActionListener; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.math.BigDecimal; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.LinkedList; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; import javafx.beans.value.ObservableValue; import javafx.concurrent.Worker; +import javafx.event.EventType; import javafx.scene.Scene; import javafx.scene.control.ButtonType; import javafx.scene.control.TextInputDialog; +import javafx.scene.input.DataFormat; +import javafx.scene.input.DragEvent; +import javafx.scene.input.Dragboard; +import javafx.scene.input.MouseEvent; +import javafx.scene.input.TransferMode; import javafx.scene.web.*; import javafx.stage.Stage; import javafx.stage.StageStyle; @@ -231,6 +241,8 @@ public void setupWebView(WebView webView) { webEngine.setCreatePopupHandler(HTMLWebViewManager::showPopup); webEngine.setOnError(HTMLWebViewManager::showError); + addWorkaroundFor3679(this.webView); + // Workaround to load Java Bridge before everything else. webEngine.onStatusChangedProperty().set(this::setBridge); } @@ -767,4 +779,141 @@ private int getHScrollValue() { private void scrollTo(int x, int y) { webEngine.executeScript("window.scrollTo(" + x + ", " + y + ")"); } + + // region Drag-and-drop workaround for [#3679](https://github.com/RPTools/maptool/issues/3679) + // This is exactly what WebView itself does, except that we do not cache the mimes and values. + // Doing so leads to more questions than answers and lacking invalidation causes the bug. + + private static void addWorkaroundFor3679(WebView webView) { + final var webEngine = webView.getEngine(); + final WebPage page; + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + lookup = MethodHandles.privateLookupIn(WebEngine.class, lookup); + MethodHandle getPageHandle = lookup.findGetter(WebEngine.class, "page", WebPage.class); + + page = (WebPage) getPageHandle.invokeExact(webEngine); + } catch (Throwable throwable) { + log.error("Unable to access WebPage from WebEngine", throwable); + return; + } + + webView.setOnDragEntered(event -> dragHandler(page, event)); + webView.setOnDragExited(event -> dragHandler(page, event)); + webView.setOnDragOver(event -> dragHandler(page, event)); + webView.setOnDragDropped(event -> dragHandler(page, event)); + webView.setOnDragDetected(event -> onDragDetected(page, event)); + webView.setOnDragDone(event -> onDragDone(page, event)); + } + + private static int getWKDndEventType(EventType et) { + int commandId = 0; + if (et == DragEvent.DRAG_ENTERED) { + commandId = WebPage.DND_DST_ENTER; + } else if (et == DragEvent.DRAG_EXITED) { + commandId = WebPage.DND_DST_EXIT; + } else if (et == DragEvent.DRAG_OVER) { + commandId = WebPage.DND_DST_OVER; + } else if (et == DragEvent.DRAG_DROPPED) { + commandId = WebPage.DND_DST_DROP; + } + return commandId; + } + + private static final int WK_DND_ACTION_NONE = 0x0; + private static final int WK_DND_ACTION_COPY = 0x1; + private static final int WK_DND_ACTION_MOVE = 0x2; + private static final int WK_DND_ACTION_LINK = 0x40000000; + + private static int getWKDndAction(TransferMode... tms) { + int dndActionId = WK_DND_ACTION_NONE; + for (TransferMode tm : tms) { + if (tm == TransferMode.COPY) { + dndActionId |= WK_DND_ACTION_COPY; + } else if (tm == TransferMode.MOVE) { + dndActionId |= WK_DND_ACTION_MOVE; + } else if (tm == TransferMode.LINK) { + dndActionId |= WK_DND_ACTION_LINK; + } + } + return dndActionId; + } + + private static TransferMode[] getFXDndAction(int wkDndAction) { + LinkedList tms = new LinkedList<>(); + if ((wkDndAction & WK_DND_ACTION_COPY) != 0) { + tms.add(TransferMode.COPY); + } + if ((wkDndAction & WK_DND_ACTION_MOVE) != 0) { + tms.add(TransferMode.MOVE); + } + if ((wkDndAction & WK_DND_ACTION_LINK) != 0) { + tms.add(TransferMode.LINK); + } + return tms.toArray(new TransferMode[0]); + } + + // Drag target + + private static void dragHandler(WebPage page, DragEvent event) { + try { + Dragboard db = event.getDragboard(); + LinkedList mimes = new LinkedList<>(); + LinkedList values = new LinkedList<>(); + for (DataFormat df : db.getContentTypes()) { + Object content = db.getContent(df); + if (content != null) { + for (String mime : df.getIdentifiers()) { + mimes.add(mime); + values.add(content.toString()); + } + } + } + + if (!mimes.isEmpty()) { + int wkDndEventType = getWKDndEventType(event.getEventType()); + int wkDndAction = + page.dispatchDragOperation( + wkDndEventType, + mimes.toArray(new String[0]), + values.toArray(new String[0]), + (int) event.getX(), + (int) event.getY(), + (int) event.getScreenX(), + (int) event.getScreenY(), + getWKDndAction(db.getTransferModes().toArray(new TransferMode[0]))); + + if (!(wkDndEventType == WebPage.DND_DST_DROP && wkDndAction == WK_DND_ACTION_NONE)) { + event.acceptTransferModes(getFXDndAction(wkDndAction)); + } + event.consume(); + } + } catch (SecurityException ex) { + log.error("Security exception", ex); + } + } + + // Drag source + + private static void onDragDetected(WebPage page, MouseEvent event) { + if (page.isDragConfirmed()) { + page.confirmStartDrag(); + event.consume(); + } + } + + private static void onDragDone(WebPage page, DragEvent event) { + page.dispatchDragOperation( + WebPage.DND_SRC_DROP, + null, + null, + (int) event.getX(), + (int) event.getY(), + (int) event.getScreenX(), + (int) event.getScreenY(), + getWKDndAction(event.getAcceptedTransferMode())); + event.consume(); + } + + // endregion } diff --git a/src/main/java/net/rptools/maptool/client/ui/macrobuttons/buttongroups/ButtonGroupPopupMenu.java b/src/main/java/net/rptools/maptool/client/ui/macrobuttons/buttongroups/ButtonGroupPopupMenu.java index f113f32a02..c231d33bd4 100644 --- a/src/main/java/net/rptools/maptool/client/ui/macrobuttons/buttongroups/ButtonGroupPopupMenu.java +++ b/src/main/java/net/rptools/maptool/client/ui/macrobuttons/buttongroups/ButtonGroupPopupMenu.java @@ -40,7 +40,7 @@ import net.rptools.maptool.model.MacroButtonProperties; import net.rptools.maptool.model.Token; import net.rptools.maptool.util.PersistenceUtil; -import org.eclipse.jetty.util.StringUtil; +import org.apache.commons.lang.StringUtils; @SuppressWarnings("serial") public class ButtonGroupPopupMenu extends JPopupMenu { @@ -278,7 +278,7 @@ public void actionPerformed(ActionEvent event) { } if (alreadyExists) { String tokenName = token.getName(); - if (MapTool.getPlayer().isGM() && !StringUtil.isEmpty(token.getGMName())) { + if (MapTool.getPlayer().isGM() && !StringUtils.isEmpty(token.getGMName())) { tokenName = tokenName + "(" + token.getGMName() + ")"; } alreadyExists = @@ -301,7 +301,7 @@ public void actionPerformed(ActionEvent event) { } if (alreadyExists) { String tokenName = token.getName(); - if (MapTool.getPlayer().isGM() && !StringUtil.isEmpty(token.getGMName())) { + if (MapTool.getPlayer().isGM() && !StringUtils.isEmpty(token.getGMName())) { tokenName += "(" + token.getGMName() + ")"; } alreadyExists = @@ -358,7 +358,7 @@ public void addMacrosToToken(List newButtonProps, Token t List toAdd = new ArrayList<>(newButtonProps.size()); int nextIndex = token.getMacroNextIndex(); String tokenName = token.getName(); - if (MapTool.getPlayer().isGM() && !StringUtil.isEmpty(token.getGMName())) { + if (MapTool.getPlayer().isGM() && !StringUtils.isEmpty(token.getGMName())) { tokenName = tokenName + "(" + token.getGMName() + ")"; } for (MacroButtonProperties nextProps : newButtonProps) { diff --git a/src/main/java/net/rptools/maptool/client/ui/players/PlayerDatabaseDialogController.java b/src/main/java/net/rptools/maptool/client/ui/players/PlayerDatabaseDialogController.java index a4b54edcd0..c5bb3e0f67 100644 --- a/src/main/java/net/rptools/maptool/client/ui/players/PlayerDatabaseDialogController.java +++ b/src/main/java/net/rptools/maptool/client/ui/players/PlayerDatabaseDialogController.java @@ -43,7 +43,9 @@ import net.rptools.maptool.client.ui.javfx.SwingJavaFXDialogController; import net.rptools.maptool.language.I18N; import net.rptools.maptool.model.player.PasswordDatabaseException; +import net.rptools.maptool.model.player.PersistedPlayerDatabase; import net.rptools.maptool.model.player.Player.Role; +import net.rptools.maptool.model.player.PlayerDBPropertyChange; import net.rptools.maptool.model.player.PlayerDatabase.AuthMethod; import net.rptools.maptool.model.player.PlayerInfo; import net.rptools.maptool.model.player.Players; @@ -71,20 +73,20 @@ public class PlayerDatabaseDialogController extends AbstractSwingJavaFXDialogCon Platform.runLater( () -> { switch (e.getPropertyName()) { - case Players.PROPERTY_CHANGE_PLAYER_CHANGED -> { + case PlayerDBPropertyChange.PROPERTY_CHANGE_PLAYER_CHANGED -> { removePlayer(e.getNewValue().toString()); addPlayer(e.getNewValue().toString()); playersTable.sort(); } - case Players.PROPERTY_CHANGE_PLAYER_ADDED -> { + case PlayerDBPropertyChange.PROPERTY_CHANGE_PLAYER_ADDED -> { addPlayer(e.getNewValue().toString()); playersTable.sort(); } - case Players.PROPERTY_CHANGE_PLAYER_REMOVED -> { + case PlayerDBPropertyChange.PROPERTY_CHANGE_PLAYER_REMOVED -> { removePlayer(e.getOldValue().toString()); playersTable.sort(); } - case Players.PROPERTY_CHANGE_DATABASE_CHANGED -> { + case PlayerDBPropertyChange.PROPERTY_CHANGE_DATABASE_CHANGED -> { addPlayers(); } } @@ -92,6 +94,8 @@ public class PlayerDatabaseDialogController extends AbstractSwingJavaFXDialogCon }; ObservableList playerInfoList = FXCollections.observableArrayList(); + private PersistedPlayerDatabase playerDatabase; + private Players players; @FXML // This method is called by the FXMLLoader when initialization is complete void initialize() { @@ -105,8 +109,14 @@ void initialize() { @Override public void init() { - - Players.addPropertyChangeListener(changeListener); + var currentDb = MapTool.getClient().getPlayerDatabase(); + if (!(currentDb instanceof PersistedPlayerDatabase persistedPlayerDatabase)) { + throw new RuntimeException( + "Player database dialog is only valid for persisted player databases"); + } + playerDatabase = persistedPlayerDatabase; + playerDatabase.addPropertyChangeListener(changeListener); + players = new Players(currentDb); String gmI81n = I18N.getString("userTerm.GM"); String playerI81n = I18N.getString("userTerm.Player"); @@ -153,11 +163,10 @@ public void init() { createButtonCellFactory( I18N.getText("playerDB.dialog.delete"), p -> { - String playerName = p.name(); SwingUtilities.invokeLater( () -> { if (MapTool.confirm("playerDB.dialog.deleteConfirm", p.name())) { - Platform.runLater(() -> new Players().removePlayer(p.name())); + Platform.runLater(() -> playerDatabase.deletePlayer(p.name())); } }); }); @@ -188,7 +197,7 @@ public void init() { @Override public void close() { try { - new Players().commitChanges(); + playerDatabase.commitChanges(); } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeySpecException @@ -196,13 +205,13 @@ public void close() { | InvalidKeyException e) { MapTool.showError("playerDB.dialog.error.savingChanges", e); } - ; - Players.removePropertyChangeListener(changeListener); + + playerDatabase.removePropertyChangeListener(changeListener); } private void addPlayer(String name) { try { - PlayerInfo playerInfo = new Players().getPlayer(name).get(); + PlayerInfo playerInfo = players.getPlayer(name).get(); playerInfoList.add(playerInfo); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); @@ -214,7 +223,7 @@ private void removePlayer(String name) { } private void addPlayers() { - new Players() + players .getDatabasePlayers() .thenAccept( p -> diff --git a/src/main/java/net/rptools/maptool/client/ui/players/PlayerDatabaseEditController.java b/src/main/java/net/rptools/maptool/client/ui/players/PlayerDatabaseEditController.java index b7fa4af08b..ed44b230d9 100644 --- a/src/main/java/net/rptools/maptool/client/ui/players/PlayerDatabaseEditController.java +++ b/src/main/java/net/rptools/maptool/client/ui/players/PlayerDatabaseEditController.java @@ -37,6 +37,7 @@ import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javax.crypto.NoSuchPaddingException; +import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.ui.javfx.SwingJavaFXDialogController; import net.rptools.maptool.client.ui.javfx.SwingJavaFXDialogEventHandler; import net.rptools.maptool.language.I18N; @@ -318,7 +319,7 @@ private void validatePublicKey() { /** Updates the values in the database based on the form values. */ private void updateDatabase() { - Players players = new Players(); + Players players = new Players(MapTool.getClient().getPlayerDatabase()); Role role = roleCombo.getSelectionModel().getSelectedItem().equals(GM_ROLE_NAME) ? Role.GM @@ -366,7 +367,10 @@ private void validatePlayerName() { validationErrors.add(ValidationErrors.NAME_MISSING); } else if (playerNameText.isEditable()) { try { - PlayerInfo playerInfo = new Players().getPlayer(playerNameText.getText()).get(); + PlayerInfo playerInfo = + new Players(MapTool.getClient().getPlayerDatabase()) + .getPlayer(playerNameText.getText()) + .get(); if (playerInfo != null) { validationErrors.add(ValidationErrors.PLAYER_EXISTS); } diff --git a/src/main/java/net/rptools/maptool/client/ui/theme/ThemeSupport.java b/src/main/java/net/rptools/maptool/client/ui/theme/ThemeSupport.java index e01dd7ac7a..1cf6db45cc 100644 --- a/src/main/java/net/rptools/maptool/client/ui/theme/ThemeSupport.java +++ b/src/main/java/net/rptools/maptool/client/ui/theme/ThemeSupport.java @@ -461,6 +461,7 @@ public record ThemeDetails( new ThemeDetails("Aah(Large Print)", AahLAF_LP.class, "Aah-LP.png", false), new ThemeDetails("Aah(Small Print)", AahLAF_SP.class, "Aah-SP.png", false), new ThemeDetails("Aah(Very Large Print)", AahLAF_VLP.class, "Aah-VLP.png", false), + new ThemeDetails("Aark", AarkLaF.class, "Aark.png", true), }; /** The current theme being used. */ diff --git a/src/main/java/net/rptools/maptool/api/ApiException.java b/src/main/java/net/rptools/maptool/client/ui/themes/AarkLaF.java similarity index 62% rename from src/main/java/net/rptools/maptool/api/ApiException.java rename to src/main/java/net/rptools/maptool/client/ui/themes/AarkLaF.java index 61f69718a6..2eb8e4ec55 100644 --- a/src/main/java/net/rptools/maptool/api/ApiException.java +++ b/src/main/java/net/rptools/maptool/client/ui/themes/AarkLaF.java @@ -12,15 +12,23 @@ * and specifically the Affero license * text at . */ -package net.rptools.maptool.api; +package net.rptools.maptool.client.ui.themes; -public class ApiException extends Exception { +import com.formdev.flatlaf.FlatDarkLaf; - public ApiException(String message) { - super(message); +public class AarkLaF extends FlatDarkLaf { + public static final String NAME = "Aark"; + + public static boolean setup() { + return setup(new AarkLaF()); + } + + public static void installLafInfo() { + installLafInfo(NAME, AarkLaF.class); } - public ApiException(String message, Throwable cause) { - super(message, cause); + @Override + public String getName() { + return NAME; } } diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/DiskBasedPartitionedDrawableRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/DiskBasedPartitionedDrawableRenderer.java index 5276f96843..10793c14fb 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/DiskBasedPartitionedDrawableRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/DiskBasedPartitionedDrawableRenderer.java @@ -34,6 +34,7 @@ import javax.imageio.ImageIO; import net.rptools.lib.FileUtil; import net.rptools.maptool.client.AppUtil; +import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.drawing.Drawable; import net.rptools.maptool.model.drawing.DrawnElement; import net.rptools.maptool.model.drawing.Pen; @@ -45,6 +46,7 @@ public class DiskBasedPartitionedDrawableRenderer implements DrawableRenderer { private static final BufferedImage NO_IMAGE = new BufferedImage(1, 1, Transparency.OPAQUE); private static final int CHUNK_SIZE = 256; + private final Zone zone; private final Map chunkMap = new HashMap(); private double lastScale; @@ -67,7 +69,8 @@ public class DiskBasedPartitionedDrawableRenderer implements DrawableRenderer { } } - public DiskBasedPartitionedDrawableRenderer() { + public DiskBasedPartitionedDrawableRenderer(Zone zone) { + this.zone = zone; flush(); } @@ -193,7 +196,7 @@ private BufferedImage createChunk( for (DrawnElement element : drawableList) { Drawable drawable = element.getDrawable(); - Rectangle2D drawnBounds = drawable.getBounds(); + Rectangle2D drawnBounds = drawable.getBounds(zone); Rectangle2D chunkBounds = new Rectangle( (int) (gridx * (CHUNK_SIZE / scale)), @@ -230,7 +233,7 @@ private BufferedImage createChunk( // if (gridx == 0 && gridy == 1) { // System.out.println("draw"); // } - drawable.draw(g, pen); + drawable.draw(zone, g, pen); g.setComposite(oldComposite); } diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/PartitionedDrawableRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/PartitionedDrawableRenderer.java index 72b33859b3..ba9e7ae199 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/PartitionedDrawableRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/PartitionedDrawableRenderer.java @@ -29,6 +29,7 @@ import net.rptools.lib.CodeTimer; import net.rptools.lib.image.ImageUtil; import net.rptools.maptool.client.DeveloperOptions; +import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.drawing.Drawable; import net.rptools.maptool.model.drawing.DrawablesGroup; import net.rptools.maptool.model.drawing.DrawnElement; @@ -44,6 +45,7 @@ public class PartitionedDrawableRenderer implements DrawableRenderer { private static final int CHUNK_SIZE = 256; private static List unusedChunkList = new LinkedList(); + private final Zone zone; private final Set noImageSet = new HashSet(); private final List chunkList = new LinkedList(); private int maxChunks; @@ -56,6 +58,10 @@ public class PartitionedDrawableRenderer implements DrawableRenderer { private boolean dirty = false; + public PartitionedDrawableRenderer(Zone zone) { + this.zone = zone; + } + public void flush() { int unusedSize = unusedChunkList.size(); for (Tuple tuple : chunkList) { @@ -218,12 +224,13 @@ private BufferedImage createChunk( for (DrawnElement element : drawableList) { timer.start("createChunk:calculate"); Drawable drawable = element.getDrawable(); - if (drawable.getBounds() == null) { + Rectangle drawableBounds = drawable.getBounds(zone); + if (drawableBounds == null) { timer.stop("createChunk:calculate"); continue; } - Rectangle2D drawnBounds = new Rectangle(drawable.getBounds()); + Rectangle2D drawnBounds = new Rectangle(drawableBounds); Rectangle2D chunkBounds = new Rectangle( (int) (gridx * (CHUNK_SIZE / scale)), @@ -280,7 +287,7 @@ private BufferedImage createChunk( Graphics2D g2 = image.createGraphics(); g2.drawImage(groupImage, 0, 0, CHUNK_SIZE, CHUNK_SIZE, null); g2.dispose(); - } else drawable.draw(g, pen); + } else drawable.draw(zone, g, pen); g.setComposite(oldComposite); timer.stop("createChunk:Draw"); } diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java index 00cad9fda9..afc56902f6 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java @@ -336,14 +336,9 @@ private List calculateLitAreaForLightSource( final var p = FogUtil.calculateVisionCenter(lightSourceToken, zone); final var translateTransform = AffineTransform.getTranslateInstance(p.x, p.y); - final var magnifyTransform = AffineTransform.getScaleInstance(multiplier, multiplier); - final var lightSourceArea = lightSource.getArea(lightSourceToken, zone); // Calculate exposed area - // Jamz: OK, let not have lowlight vision type multiply darkness radius - if (multiplier != 1 && lightSource.getType() == LightSource.Type.NORMAL) { - lightSourceArea.transform(magnifyTransform); - } + final var lightSourceArea = lightSource.getArea(lightSourceToken, zone, multiplier); lightSourceArea.transform(translateTransform); Area lightSourceVisibleArea = lightSourceArea; @@ -365,35 +360,16 @@ private List calculateLitAreaForLightSource( final var litAreas = new ArrayList(); - // Tracks the cummulative inner ranges of light sources so that we can cut them out of the - // outer ranges and end up with disjoint sets, even when magnifying. - // Note that this "hole punching" has nothing to do with lumen strength, it's just a way of - // making smaller ranges act as lower bounds for larger ranges. - final var cummulativeNotTransformedArea = new Area(); - for (final var light : lightSource.getLightList()) { - final var notScaledLightArea = - light.getArea(lightSourceToken, zone, lightSource.isScaleWithToken()); - if (notScaledLightArea == null) { - continue; - } - final var lightArea = new Area(notScaledLightArea); - - // Lowlight vision does not magnify darkness. - if (multiplier != 1 - && lightSource.getType() == LightSource.Type.NORMAL - && light.getLumens() >= 0) { - lightArea.transform(magnifyTransform); - } + for (final var lightArea : lightSource.getLightAreas(lightSourceToken, zone, multiplier)) { + var area = lightArea.area(); + var light = lightArea.light(); - lightArea.subtract(cummulativeNotTransformedArea); - lightArea.transform(translateTransform); - lightArea.intersect(lightSourceVisibleArea); + area.transform(translateTransform); + area.intersect(lightSourceVisibleArea); litAreas.add( new ContributedLight( - new LitArea(light.getLumens(), lightArea), new LightInfo(lightSource, light))); - - cummulativeNotTransformedArea.add(notScaledLightArea); + new LitArea(light.getLumens(), area), new LightInfo(lightSource, light))); } // Magnification can cause different ranges for a single light source to overlap. This is not diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/VisionOverlayRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/VisionOverlayRenderer.java index c121d5695a..6b537da5ee 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/VisionOverlayRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/VisionOverlayRenderer.java @@ -68,7 +68,6 @@ public void render(Graphics2D g, PlayerView view, Token tokenUnderMouse) { private void renderWorld(Graphics2D worldG, PlayerView view, Token token) { // The vision of the token is not necessarily related to the current view. - // final var tokenView = view.derive(Collections.singleton(token)); final var tokenView = new PlayerView(view.getRole(), List.of(token)); Area currentTokenVisionArea = zoneView.getVisibleArea(token, tokenView); diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java index 23f7e91ed4..e4040b318c 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java @@ -77,11 +77,9 @@ import net.rptools.parser.ParserException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.NotNull; /** */ -public class ZoneRenderer extends JComponent - implements DropTargetListener, Comparable { +public class ZoneRenderer extends JComponent implements DropTargetListener { private static final long serialVersionUID = 3832897780066104884L; private static final Logger log = LogManager.getLogger(ZoneRenderer.class); @@ -107,8 +105,7 @@ public class ZoneRenderer extends JComponent private final SelectionModel selectionModel; private Scale zoneScale; - private final Map drawableRenderers = - CollectionUtil.newFilledEnumMap(Zone.Layer.class, layer -> new PartitionedDrawableRenderer()); + private final Map drawableRenderers; private final List overlayList = new ArrayList(); private final Map> tokenLocationMap = new HashMap>(); @@ -173,6 +170,10 @@ public ZoneRenderer(Zone zone) { zoneView = new ZoneView(zone); setZoneScale(new Scale()); + drawableRenderers = + CollectionUtil.newFilledEnumMap( + Zone.Layer.class, layer -> new PartitionedDrawableRenderer(zone)); + var renderHelper = new RenderHelper(this, tempBufferPool); this.compositor = new ZoneCompositor(); this.gridRenderer = new GridRenderer(); @@ -3557,16 +3558,10 @@ private void onGridChanged(GridChanged event) { if (event.zone() != this.zone) { return; } - repaintDebouncer.dispatch(); - } - // - // COMPARABLE - public int compareTo(@NotNull ZoneRenderer o) { - if (o != this) { - return (int) (zone.getCreationTime() - o.zone.getCreationTime()); - } - return 0; + // A change in grid can change the size of templates. + flushDrawableRenderer(); + repaintDebouncer.dispatch(); } // Begin token common macro identification diff --git a/src/main/java/net/rptools/maptool/model/AssetLoader.java b/src/main/java/net/rptools/maptool/model/AssetLoader.java index c04872b96b..d787c925db 100644 --- a/src/main/java/net/rptools/maptool/model/AssetLoader.java +++ b/src/main/java/net/rptools/maptool/model/AssetLoader.java @@ -318,18 +318,10 @@ public void run() { } } - // System.out.println("Got " + id + " from MT"); // Last resort, ask the MT server - final var serverCommand = MapTool.serverCommand(); - if (serverCommand != null) { - // We can drop off the end of this runnable because it'll background load the - // image from the server - serverCommand.getAsset(id); - } else { - // This could be too early in the loading process for a server command to be set. - AssetManager.putAsset(Asset.createBrokenImageAsset(id)); - completeRequest(id); - } + // We can drop off the end of this runnable because it'll background load the + // image from the server + MapTool.serverCommand().getAsset(id); } } } diff --git a/src/main/java/net/rptools/maptool/model/CellPoint.java b/src/main/java/net/rptools/maptool/model/CellPoint.java index a49c2710df..cfdcdf1f86 100644 --- a/src/main/java/net/rptools/maptool/model/CellPoint.java +++ b/src/main/java/net/rptools/maptool/model/CellPoint.java @@ -43,6 +43,10 @@ public CellPoint(int x, int y, double distanceTraveled, double distanceTraveledW this.distanceTraveledWithoutTerrain = distanceTraveledWithoutTerrain; } + public CellPoint(CellPoint other) { + this(other.x, other.y); + } + @Override public String toString() { return "CellPoint" + super.toString(); diff --git a/src/main/java/net/rptools/maptool/model/Grid.java b/src/main/java/net/rptools/maptool/model/Grid.java index 1f7b1090d0..087161543e 100644 --- a/src/main/java/net/rptools/maptool/model/Grid.java +++ b/src/main/java/net/rptools/maptool/model/Grid.java @@ -25,6 +25,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; import javax.swing.Action; import javax.swing.KeyStroke; import net.rptools.lib.FileUtil; @@ -352,7 +353,7 @@ public void setSize(int size) { * @param scaleWithToken used to increase the area based on token footprint * @return Area */ - public Area getShapedArea( + public @Nonnull Area getShapedArea( ShapeType shape, Token token, double range, diff --git a/src/main/java/net/rptools/maptool/model/Light.java b/src/main/java/net/rptools/maptool/model/Light.java index 878e85bbae..f195fc8063 100644 --- a/src/main/java/net/rptools/maptool/model/Light.java +++ b/src/main/java/net/rptools/maptool/model/Light.java @@ -100,18 +100,28 @@ public double getArcAngle() { return shape; } - public @Nonnull Area getArea(@Nonnull Token token, @Nonnull Zone zone, boolean scaleWithToken) { + public @Nonnull Area getArea( + @Nonnull Token token, @Nonnull Zone zone, double multiplier, boolean scaleWithToken) { + var radius = getRadius(); + // Darkness does not get magnified. + if (lumens >= 0) { + radius *= multiplier; + } return zone.getGrid() .getShapedArea( getShape(), token, - getRadius(), + radius, getWidth(), getArcAngle(), (int) getFacingOffset(), scaleWithToken); } + public @Nonnull Area getArea(@Nonnull Token token, @Nonnull Zone zone, boolean scaleWithToken) { + return getArea(token, zone, 1.0, scaleWithToken); + } + public boolean isGM() { return isGM; } diff --git a/src/main/java/net/rptools/maptool/model/LightSource.java b/src/main/java/net/rptools/maptool/model/LightSource.java index 0c7830644a..619494ecbe 100644 --- a/src/main/java/net/rptools/maptool/model/LightSource.java +++ b/src/main/java/net/rptools/maptool/model/LightSource.java @@ -216,6 +216,50 @@ public boolean isIgnoresVBL() { return ignoresVBL; } + public record LightArea(Light light, Area area) {} + + public @Nonnull List getLightAreas( + @Nonnull Token token, @Nonnull Zone zone, double multiplier) { + // Tracks the cumulative inner ranges of light sources so that we can cut them out of the + // outer ranges and end up with disjoint sets, even when magnifying. + // Note that this "hole punching" has nothing to do with lumen strength, it's just a way of + // making smaller ranges act as lower bounds for larger ranges. + + // Auras do not get magnified. + if (type != Type.NORMAL) { + multiplier = 1.0; + } + + final var result = new ArrayList(); + final var cummulativeNotTransformedArea = new Area(); + + for (final var light : lightList) { + final var notScaledLightArea = light.getArea(token, zone, scaleWithToken); + + final var lightArea = light.getArea(token, zone, multiplier, scaleWithToken); + lightArea.subtract(cummulativeNotTransformedArea); + result.add(new LightArea(light, lightArea)); + + cummulativeNotTransformedArea.add(notScaledLightArea); + } + return result; + } + + /* Area for all lights combined */ + public @Nonnull Area getArea(@Nonnull Token token, @Nonnull Zone zone, double multiplier) { + // Auras do not get magnified. + if (type != Type.NORMAL) { + multiplier = 1.0; + } + + Area area = new Area(); + for (Light light : lightList) { + area.add(light.getArea(token, zone, multiplier, isScaleWithToken())); + } + + return area; + } + /* * Area for a single light, subtracting any previous lights */ @@ -232,12 +276,7 @@ public boolean isIgnoresVBL() { /* Area for all lights combined */ public @Nonnull Area getArea(@Nonnull Token token, @Nonnull Zone zone) { - Area area = new Area(); - for (Light light : lightList) { - area.add(light.getArea(token, zone, isScaleWithToken())); - } - - return area; + return getArea(token, zone, 1.0); } @SuppressWarnings("unchecked") diff --git a/src/main/java/net/rptools/maptool/model/Zone.java b/src/main/java/net/rptools/maptool/model/Zone.java index c468e51f36..74e409bac3 100644 --- a/src/main/java/net/rptools/maptool/model/Zone.java +++ b/src/main/java/net/rptools/maptool/model/Zone.java @@ -37,7 +37,6 @@ import net.rptools.maptool.language.I18N; import net.rptools.maptool.model.InitiativeList.TokenInitiative; import net.rptools.maptool.model.Token.TerrainModifierOperation; -import net.rptools.maptool.model.drawing.AbstractTemplate; import net.rptools.maptool.model.drawing.Drawable; import net.rptools.maptool.model.drawing.DrawableColorPaint; import net.rptools.maptool.model.drawing.DrawablePaint; @@ -364,12 +363,6 @@ public String toString() { public static final DrawablePaint DEFAULT_FOG = new DrawableColorPaint(Color.black); - // The zones should be ordered. We could have the server assign each zone - // an incrementing number as new zones are created, but that would take a lot - // more elegance than we really need. Instead, let's just keep track of the - // time when it was created. This should give us sufficient granularity, because - // seriously -- what's the likelihood of two GMs separately creating a new zone at exactly - // the same millisecond since the epoch? private long creationTime = System.currentTimeMillis(); private GUID id = new GUID(); // Ideally would be 'final', but that complicates imported() @@ -479,10 +472,6 @@ public String toString() { drawablesByLayer.put(Layer.BACKGROUND, backgroundDrawables); } - /** - * Note: When adding new fields to this class, make sure to update all constructors, {@link - * #imported()}, {@link #readResolve()}, and potentially {@link #optimize()}. - */ public Zone() { // TODO: Was this needed? // setGrid(new SquareGrid()); @@ -612,16 +601,11 @@ public DrawablePaint getFogPaint() { } /** - * Note: When adding new fields to this class, make sure to update all constructors, {@link - * #imported()}, {@link #readResolve()}, and potentially {@link #optimize()}. + * Create a new zone with old zone's properties and with new token ids. * *

JFJ 2010-10-27 Don't forget that since there are new zones AND new tokens created here from * the old one being passed in, if you have any data that needs to transfer over, you will need to * manually copy it as is done below for various items. - */ - - /** - * Create a new zone with old zone's properties and with new token ids. * * @param zone The zone to copy. */ @@ -638,6 +622,7 @@ public Zone(Zone zone) { public Zone(Zone zone, boolean keepIds) { if (keepIds) { this.id = zone.getId(); + this.creationTime = zone.creationTime; } backgroundPaint = zone.backgroundPaint; @@ -663,9 +648,17 @@ public Zone(Zone zone, boolean keepIds) { playerAlias = zone.playerAlias; for (final var entry : drawablesByLayer.entrySet()) { - entry.getValue().addAll(zone.drawablesByLayer.get(entry.getKey())); + final var otherDrawables = zone.drawablesByLayer.get(entry.getKey()); + final var thisDrawables = entry.getValue(); + + for (final var element : otherDrawables) { + final var copy = new DrawnElement(element); + if (!keepIds) { + copy.getDrawable().setId(new GUID()); + } + thisDrawables.add(copy); + } } - validateTemplateZoneIds(); if (!zone.labels.isEmpty()) { for (GUID guid : zone.labels.keySet()) { @@ -748,14 +741,9 @@ public GUID getId() { /** * Should be invoked only when a Zone has been imported from an external source and needs to be - * cleaned up before being used. Currently this cleanup consists of allocating a new GUID, setting - * the creation time to `now', and resetting the initiative list (setting the related zone and - * clearing the model). + * cleaned up before being used. */ public void imported() { - id = new GUID(); - creationTime = System.currentTimeMillis(); - initiativeList.setZone(this); initiativeList.clearModel(); } @@ -1507,17 +1495,6 @@ public void addDrawable(Pen pen, Drawable drawable) { undo.addDrawable(pen, drawable); } - private void validateTemplateZoneIds() { - // Classes that extend Abstract template have a zone id so we need to make sure to update it - for (var list : drawablesByLayer.values()) { - for (var de : list) { - if (de.getDrawable() instanceof AbstractTemplate at) { - at.setZoneId(id); - } - } - } - } - public boolean canUndo() { return undo.canUndo(); } @@ -2087,8 +2064,8 @@ private void collapseDrawableLayer(List layer) { for (ListIterator drawnIter = list.listIterator(); drawnIter.hasNext(); ) { DrawnElement drawn = drawnIter.next(); // Are we covered ourselves ? - Area drawnArea = drawn.getDrawable().getArea(); - if (drawnArea == null) { + Area drawnArea = drawn.getDrawable().getArea(this); + if (drawnArea.isEmpty()) { continue; } // The following is over-zealous optimization. Lines (1-dimensional) should be kept. @@ -2220,7 +2197,6 @@ protected Object readResolve() { drawablesByLayer.put(Layer.GM, gmDrawables); drawablesByLayer.put(Layer.OBJECT, objectDrawables); drawablesByLayer.put(Layer.BACKGROUND, backgroundDrawables); - validateTemplateZoneIds(); return this; } @@ -2348,8 +2324,6 @@ public static Zone fromDto(ZoneDto dto) { zone.height = dto.getHeight(); zone.width = dto.getWidth(); - zone.validateTemplateZoneIds(); - return zone; } diff --git a/src/main/java/net/rptools/maptool/model/ZonePoint.java b/src/main/java/net/rptools/maptool/model/ZonePoint.java index 2e79710d82..3b89a26244 100644 --- a/src/main/java/net/rptools/maptool/model/ZonePoint.java +++ b/src/main/java/net/rptools/maptool/model/ZonePoint.java @@ -19,6 +19,10 @@ public ZonePoint(int x, int y) { super(x, y); } + public ZonePoint(ZonePoint other) { + this(other.x, other.y); + } + @Override public String toString() { return "ZonePoint" + super.toString(); diff --git a/src/main/java/net/rptools/maptool/model/drawing/AbstractDrawing.java b/src/main/java/net/rptools/maptool/model/drawing/AbstractDrawing.java index 39efe3bd35..6db6d4f4fe 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/AbstractDrawing.java +++ b/src/main/java/net/rptools/maptool/model/drawing/AbstractDrawing.java @@ -33,8 +33,12 @@ * implementing classes. */ public abstract class AbstractDrawing implements Drawable, ImageObserver { - /** The unique identifier for this drawable. It is immutable. */ - private final GUID id; + /** + * The unique identifier for this drawable. + * + *

It should not typically be changed except to give copies a new ID. + */ + private GUID id; private String layer; private String name; @@ -47,12 +51,15 @@ protected AbstractDrawing(GUID id) { this.id = id; } - /* - * (non-Javadoc) - * - * @see maptool.model.drawing.Drawable#draw(java.awt.Graphics2D, maptool.model.drawing.Pen) - */ - public void draw(Graphics2D g, Pen pen) { + protected AbstractDrawing(AbstractDrawing other) { + // The only thing we don't preserve is the ID. + this.id = other.id; + this.layer = other.layer; + this.name = other.name; + } + + @Override + public void draw(Zone zone, Graphics2D g, Pen pen) { if (pen == null) { pen = Pen.DEFAULT; } @@ -72,7 +79,7 @@ public void draw(Graphics2D g, Pen pen) { // **** Legacy support for 1.1 g.setColor(new Color(pen.getBackgroundColor())); } - drawBackground(g); + drawBackground(zone, g); } if (pen.getForegroundMode() == Pen.MODE_SOLID) { if (pen.getPaint() != null) { @@ -81,15 +88,15 @@ public void draw(Graphics2D g, Pen pen) { // **** Legacy support for 1.1 g.setColor(new Color(pen.getColor())); } - draw(g); + draw(zone, g); } g.setComposite(oldComposite); g.setStroke(oldStroke); } - protected abstract void draw(Graphics2D g); + protected abstract void draw(Zone zone, Graphics2D g); - protected abstract void drawBackground(Graphics2D g); + protected abstract void drawBackground(Zone zone, Graphics2D g); @VisibleForTesting protected Campaign getCampaign() { @@ -105,6 +112,11 @@ public GUID getId() { return id; } + @Override + public void setId(GUID guid) { + this.id = guid; + } + public void setLayer(Zone.Layer layer) { this.layer = layer != null ? layer.name() : null; } diff --git a/src/main/java/net/rptools/maptool/model/drawing/AbstractTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/AbstractTemplate.java index b15bf75782..fd1500a148 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/AbstractTemplate.java +++ b/src/main/java/net/rptools/maptool/model/drawing/AbstractTemplate.java @@ -19,7 +19,6 @@ import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.geom.Line2D; -import net.rptools.maptool.client.MapTool; import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.ZonePoint; @@ -42,15 +41,18 @@ public abstract class AbstractTemplate extends AbstractDrawing { /** The location of the vertex where painting starts. */ private ZonePoint vertex = new ZonePoint(0, 0); - /** The id of the zone where this drawable is painted. */ - private GUID zoneId; - protected AbstractTemplate() {} protected AbstractTemplate(GUID id) { super(id); } + protected AbstractTemplate(AbstractTemplate other) { + super(other); + this.radius = other.radius; + this.vertex = new ZonePoint(other.vertex); + } + /*--------------------------------------------------------------------------------------------- * Class Variables *-------------------------------------------------------------------------------------------*/ @@ -162,36 +164,18 @@ public void setVertex(ZonePoint vertex) { this.vertex = vertex; } - /** - * Get the zoneId for this RadiusTemplate. - * - * @return Returns the current value of zoneId. - */ - public GUID getZoneId() { - return zoneId; - } - - /** - * Set the value of zoneId for this RadiusTemplate. - * - * @param zoneId The zoneId to set. - */ - public void setZoneId(GUID zoneId) { - this.zoneId = zoneId; - } - /** * Paint the border or area of the template * + * @param zone The zone that is being painted * @param g Where to paint * @param border Paint the border? * @param area Paint the area? */ - protected void paint(Graphics2D g, boolean border, boolean area) { + protected void paint(Zone zone, Graphics2D g, boolean border, boolean area) { if (radius == 0) { return; } - Zone zone = MapTool.getCampaign().getZone(zoneId); if (zone == null) { return; } @@ -331,25 +315,20 @@ public int getDistance(int x, int y) { * Overridden AbstractDrawing Methods *-------------------------------------------------------------------------------------------*/ - /** - * @see net.rptools.maptool.model.drawing.AbstractDrawing#draw(java.awt.Graphics2D) - */ @Override - protected void draw(Graphics2D g) { - paint(g, true, false); + protected void draw(Zone zone, Graphics2D g) { + paint(zone, g, true, false); } - /** - * @see net.rptools.maptool.model.drawing.AbstractDrawing#drawBackground(java.awt.Graphics2D) - */ @Override - protected void drawBackground(Graphics2D g) { + protected void drawBackground(Zone zone, Graphics2D g) { // Adjust alpha automatically Composite old = g.getComposite(); - if (old != AlphaComposite.Clear) + if (old != AlphaComposite.Clear) { g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, DEFAULT_BG_ALPHA)); - paint(g, false, true); + } + paint(zone, g, false, true); g.setComposite(old); } diff --git a/src/main/java/net/rptools/maptool/model/drawing/BlastTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/BlastTemplate.java index 7398001059..7464cde575 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/BlastTemplate.java +++ b/src/main/java/net/rptools/maptool/model/drawing/BlastTemplate.java @@ -15,13 +15,11 @@ package net.rptools.maptool.model.drawing; import com.google.protobuf.StringValue; -import java.awt.AlphaComposite; -import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Rectangle; -import java.awt.Shape; +import java.awt.RenderingHints; import java.awt.geom.Area; -import net.rptools.maptool.client.MapTool; +import javax.annotation.Nonnull; import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.ZonePoint; @@ -40,9 +38,6 @@ public class BlastTemplate extends ConeTemplate { * Instance Variables *-------------------------------------------------------------------------------------------*/ - /** Renderer for the blast. The {@link Shape} is just a rectangle. */ - private ShapeDrawable renderer = new ShapeDrawable(new Rectangle()); - private int offsetX; private int offsetY; @@ -54,32 +49,31 @@ public BlastTemplate(GUID id, int offsetX, int offsetY) { this.offsetY = offsetY; } + public BlastTemplate(BlastTemplate other) { + super(other); + this.offsetX = other.offsetX; + this.offsetY = other.offsetY; + } + /*--------------------------------------------------------------------------------------------- * Instance Methods *-------------------------------------------------------------------------------------------*/ - /** - * This methods adjusts the rectangle in the renderer to match the new radius, vertex, or - * location. - */ - private void adjustRectangle() { - if (getZoneId() == null) return; - Zone zone; - if (MapTool.isHostingServer()) { - zone = MapTool.getServer().getCampaign().getZone(getZoneId()); - } else { - zone = MapTool.getCampaign().getZone(getZoneId()); + @Override + public Drawable copy() { + return new BlastTemplate(this); + } + + private Rectangle makeShape(Zone zone) { + if (zone == null) { + return new Rectangle(); } - if (zone == null) return; int gridSize = zone.getGrid().getSize(); int size = getRadius() * gridSize; - Rectangle r = (Rectangle) renderer.getShape(); - r.setBounds(getVertex().x, getVertex().y, size, size); - - r.x += offsetX * gridSize; - r.y += offsetY * gridSize; + return new Rectangle( + getVertex().x + offsetX * gridSize, getVertex().y + offsetY * gridSize, size, size); } /*--------------------------------------------------------------------------------------------- @@ -87,8 +81,8 @@ private void adjustRectangle() { *-------------------------------------------------------------------------------------------*/ @Override - public Rectangle getBounds() { - Rectangle r = new Rectangle(renderer.getShape().getBounds()); + public Rectangle getBounds(Zone zone) { + Rectangle r = makeShape(zone); // We don't know pen width, so add some padding to account for it r.x -= 5; r.y -= 5; @@ -135,17 +129,6 @@ public void setControlCellRelative(int relX, int relY) { } offsetX = centerOffset + Math.min(Math.max(lowerBound, relX), upperBound); } - adjustRectangle(); - } - - /** - * @see - * net.rptools.maptool.model.drawing.AbstractTemplate#setVertex(net.rptools.maptool.model.ZonePoint) - */ - @Override - public void setVertex(ZonePoint vertex) { - super.setVertex(vertex); - adjustRectangle(); } /** @@ -160,29 +143,27 @@ public int getDistance(int x, int y) { * Overridden AbstractDrawing Methods *-------------------------------------------------------------------------------------------*/ - /** - * @see net.rptools.maptool.model.drawing.AbstractDrawing#draw(java.awt.Graphics2D) - */ - @Override - protected void draw(Graphics2D g) { - renderer.draw(g); - } - - /** - * @see net.rptools.maptool.model.drawing.AbstractDrawing#drawBackground(java.awt.Graphics2D) - */ @Override - protected void drawBackground(Graphics2D g) { - Composite old = g.getComposite(); - if (old != AlphaComposite.Clear) - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, DEFAULT_BG_ALPHA)); - renderer.drawBackground(g); - g.setComposite(old); + protected void paint(Zone zone, Graphics2D g, boolean border, boolean area) { + Object oldAA = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING); + try { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + var shape = makeShape(zone); + if (area) { + g.fill(shape); + } + if (border) { + g.draw(shape); + } + } finally { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldAA); + } } @Override - public Area getArea() { - return renderer.getArea(); + public @Nonnull Area getArea(Zone zone) { + return new Area(makeShape(zone)); } public int getOffsetX() { @@ -198,7 +179,6 @@ public DrawableDto toDto() { var dto = BlastTemplateDto.newBuilder(); dto.setId(getId().toString()) .setLayer(getLayer().name()) - .setZoneId(getZoneId().toString()) .setRadius(getRadius()) .setVertex(getVertex().toDto()) .setDirection(getDirection().name()) @@ -209,4 +189,18 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setBlastTemplate(dto).build(); } + + public static BlastTemplate fromDto(BlastTemplateDto dto) { + var id = GUID.valueOf(dto.getId()); + var drawable = new BlastTemplate(id, dto.getOffsetX(), dto.getOffsetY()); + drawable.setRadius(dto.getRadius()); + var vertex = dto.getVertex(); + drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); + drawable.setDirection(AbstractTemplate.Direction.valueOf(dto.getDirection())); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + return drawable; + } } diff --git a/src/main/java/net/rptools/maptool/model/drawing/BurstTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/BurstTemplate.java index 8217a3a5e3..c0943d056d 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/BurstTemplate.java +++ b/src/main/java/net/rptools/maptool/model/drawing/BurstTemplate.java @@ -15,13 +15,11 @@ package net.rptools.maptool.model.drawing; import com.google.protobuf.StringValue; -import java.awt.AlphaComposite; -import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Rectangle; -import java.awt.Shape; +import java.awt.RenderingHints; import java.awt.geom.Area; -import net.rptools.maptool.client.MapTool; +import javax.annotation.Nonnull; import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.ZonePoint; @@ -38,70 +36,43 @@ public class BurstTemplate extends RadiusTemplate { * Instance Variables *-------------------------------------------------------------------------------------------*/ - /** Renderer for the blast. The {@link Shape} is just a rectangle. */ - private final ShapeDrawable renderer = new ShapeDrawable(new Rectangle()); - - /** Renderer for the blast. The {@link Shape} is just a rectangle. */ - private final ShapeDrawable vertexRenderer = new ShapeDrawable(new Rectangle()); - public BurstTemplate() {} public BurstTemplate(GUID id) { super(id); } + public BurstTemplate(BurstTemplate other) { + super(other); + } + /*--------------------------------------------------------------------------------------------- * Instance Methods *-------------------------------------------------------------------------------------------*/ - /** - * This methods adjusts the rectangle in the renderer to match the new radius, vertex, or - * direction. Due to the fact that it is impossible to draw to the cardinal directions evenly when - * the radius is an even number and still stay in the squares, that case isn't allowed. - */ - private void adjustShape() { - if (getZoneId() == null) return; - Zone zone; - if (MapTool.isHostingServer()) { - zone = MapTool.getServer().getCampaign().getZone(getZoneId()); - } else { - zone = MapTool.getCampaign().getZone(getZoneId()); - } - if (zone == null) return; + @Override + public Drawable copy() { + return new BurstTemplate(this); + } + private Rectangle makeVertexShape(Zone zone) { int gridSize = zone.getGrid().getSize(); - Rectangle r = (Rectangle) vertexRenderer.getShape(); - r.setBounds(getVertex().x, getVertex().y, gridSize, gridSize); - r = (Rectangle) renderer.getShape(); - r.setBounds(getVertex().x, getVertex().y, gridSize, gridSize); - r.x -= getRadius() * gridSize; - r.y -= getRadius() * gridSize; - r.width = r.height = (getRadius() * 2 + 1) * gridSize; + return new Rectangle(getVertex().x, getVertex().y, gridSize, gridSize); + } + + private Rectangle makeShape(Zone zone) { + int gridSize = zone.getGrid().getSize(); + return new Rectangle( + getVertex().x - getRadius() * gridSize, + getVertex().y - getRadius() * gridSize, + (getRadius() * 2 + 1) * gridSize, + (getRadius() * 2 + 1) * gridSize); } /*--------------------------------------------------------------------------------------------- * Overridden *Template Methods *-------------------------------------------------------------------------------------------*/ - /** - * @see net.rptools.maptool.model.drawing.AbstractTemplate#setRadius(int) - */ - @Override - public void setRadius(int squares) { - super.setRadius(squares); - adjustShape(); - } - - /** - * @see - * net.rptools.maptool.model.drawing.AbstractTemplate#setVertex(net.rptools.maptool.model.ZonePoint) - */ - @Override - public void setVertex(ZonePoint vertex) { - super.setVertex(vertex); - adjustShape(); - } - /** * @see net.rptools.maptool.model.drawing.AbstractTemplate#getDistance(int, int) */ @@ -111,8 +82,8 @@ public int getDistance(int x, int y) { } @Override - public Rectangle getBounds() { - Rectangle r = new Rectangle(renderer.getShape().getBounds()); + public Rectangle getBounds(Zone zone) { + Rectangle r = makeShape(zone); // We don't know pen width, so add some padding to account for it r.x -= 5; r.y -= 5; @@ -126,30 +97,28 @@ public Rectangle getBounds() { * Overridden AbstractDrawing Methods *-------------------------------------------------------------------------------------------*/ - /** - * @see net.rptools.maptool.model.drawing.AbstractDrawing#draw(java.awt.Graphics2D) - */ - @Override - protected void draw(Graphics2D g) { - renderer.draw(g); - vertexRenderer.draw(g); - } - - /** - * @see net.rptools.maptool.model.drawing.AbstractDrawing#drawBackground(java.awt.Graphics2D) - */ @Override - protected void drawBackground(Graphics2D g) { - Composite old = g.getComposite(); - if (old != AlphaComposite.Clear) - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, DEFAULT_BG_ALPHA)); - renderer.drawBackground(g); - g.setComposite(old); + protected void paint(Zone zone, Graphics2D g, boolean border, boolean area) { + Object oldAA = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING); + try { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + var shape = makeShape(zone); + if (border) { + g.draw(shape); + g.draw(makeVertexShape(zone)); + } + if (area) { + g.fill(shape); + } + } finally { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldAA); + } } @Override - public Area getArea() { - return renderer.getArea(); + public @Nonnull Area getArea(Zone zone) { + return new Area(makeShape(zone)); } @Override @@ -157,7 +126,6 @@ public DrawableDto toDto() { var dto = BurstTemplateDto.newBuilder(); dto.setId(getId().toString()) .setLayer(getLayer().name()) - .setZoneId(getZoneId().toString()) .setRadius(getRadius()) .setVertex(getVertex().toDto()); @@ -165,4 +133,17 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setBurstTemplate(dto).build(); } + + public static BurstTemplate fromDto(BurstTemplateDto dto) { + var id = GUID.valueOf(dto.getId()); + var drawable = new BurstTemplate(id); + drawable.setRadius(dto.getRadius()); + var vertex = dto.getVertex(); + drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + return drawable; + } } diff --git a/src/main/java/net/rptools/maptool/model/drawing/ConeTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/ConeTemplate.java index a475c7ea5d..85d1288948 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/ConeTemplate.java +++ b/src/main/java/net/rptools/maptool/model/drawing/ConeTemplate.java @@ -18,7 +18,7 @@ import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.geom.Area; -import net.rptools.maptool.client.MapTool; +import javax.annotation.Nonnull; import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.ZonePoint; @@ -35,16 +35,6 @@ */ public class ConeTemplate extends RadiusTemplate { - public ConeTemplate() {} - - public ConeTemplate(GUID id) { - super(id); - } - - /*--------------------------------------------------------------------------------------------- - * Instance Variables - *-------------------------------------------------------------------------------------------*/ - /** * The dirction to paint. The ne,se,nw,sw paint a quadrant and the n,w,e,w paint along the spine * of the selected vertex. Saved as a string as a hack to get around the hessian library's problem @@ -52,10 +42,26 @@ public ConeTemplate(GUID id) { */ private String direction = Direction.SOUTH_EAST.name(); + public ConeTemplate() {} + + public ConeTemplate(GUID id) { + super(id); + } + + public ConeTemplate(ConeTemplate other) { + super(other); + this.direction = other.direction; + } + /*--------------------------------------------------------------------------------------------- * Instance Methods *-------------------------------------------------------------------------------------------*/ + @Override + public Drawable copy() { + return new ConeTemplate(this); + } + /** * Get the direction for this ConeTemplate. * @@ -257,21 +263,9 @@ private boolean withinQuadrant(Quadrant q) { * Drawable Interface Methods *-------------------------------------------------------------------------------------------*/ - /** - * @see net.rptools.maptool.model.drawing.Drawable#getBounds() - */ - public Rectangle getBounds() { - if (MapTool.getCampaign().getZone(getZoneId()) == null) { - // How does this happen ?! Anyway, try to use the current zone (since that's what we're - // drawing anyway, seems reasonable - if (MapTool.getFrame().getCurrentZoneRenderer() == null) { - // Wha?! - return new Rectangle(); - } - setZoneId(MapTool.getFrame().getCurrentZoneRenderer().getZone().getId()); - } - - int gridSize = MapTool.getCampaign().getZone(getZoneId()).getGrid().getSize(); + @Override + public Rectangle getBounds(Zone zone) { + int gridSize = zone.getGrid().getSize(); int quadrantSize = getRadius() * gridSize + BOUNDS_PADDING; // Find the x,y loc @@ -307,11 +301,7 @@ public Rectangle getBounds() { } @Override - public Area getArea() { - if (getZoneId() == null) { - return new Area(); - } - Zone zone = getCampaign().getZone(getZoneId()); + public @Nonnull Area getArea(Zone zone) { if (zone == null) { return new Area(); } @@ -352,7 +342,6 @@ public DrawableDto toDto() { var dto = ConeTemplateDto.newBuilder(); dto.setId(getId().toString()) .setLayer(getLayer().name()) - .setZoneId(getZoneId().toString()) .setRadius(getRadius()) .setVertex(getVertex().toDto()) .setDirection(getDirection().name()); @@ -361,4 +350,18 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setConeTemplate(dto).build(); } + + public static ConeTemplate fromDto(ConeTemplateDto dto) { + var id = GUID.valueOf(dto.getId()); + var drawable = new ConeTemplate(id); + drawable.setRadius(dto.getRadius()); + var vertex = dto.getVertex(); + drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); + drawable.setDirection(AbstractTemplate.Direction.valueOf(dto.getDirection())); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + return drawable; + } } diff --git a/src/main/java/net/rptools/maptool/model/drawing/Cross.java b/src/main/java/net/rptools/maptool/model/drawing/Cross.java index 2559cd9962..58ed13e4b4 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/Cross.java +++ b/src/main/java/net/rptools/maptool/model/drawing/Cross.java @@ -19,7 +19,9 @@ import java.awt.Point; import java.awt.RenderingHints; import java.awt.geom.Area; +import javax.annotation.Nonnull; import net.rptools.maptool.model.GUID; +import net.rptools.maptool.model.Zone; import net.rptools.maptool.server.Mapper; import net.rptools.maptool.server.proto.drawing.CrossDrawableDto; import net.rptools.maptool.server.proto.drawing.DrawableDto; @@ -41,8 +43,21 @@ public Cross(GUID id, int startX, int startY, int endX, int endY) { endPoint = new Point(endX, endY); } - public Area getArea() { - return new Area(getBounds()); + public Cross(Cross other) { + super(other); + + this.startPoint = new Point(other.startPoint); + this.endPoint = new Point(other.endPoint); + } + + @Override + public Drawable copy() { + return new Cross(this); + } + + @Override + public @Nonnull Area getArea(Zone zone) { + return new Area(getBounds(zone)); } @Override @@ -59,13 +74,21 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setCrossDrawable(dto).build(); } - /* - * (non-Javadoc) - * - * @see net.rptools.maptool.model.drawing.Drawable#getBounds() - */ - public java.awt.Rectangle getBounds() { + public static Cross fromDto(CrossDrawableDto dto) { + var id = GUID.valueOf(dto.getId()); + var startPoint = dto.getStartPoint(); + var endPoint = dto.getEndPoint(); + var drawable = + new Cross(id, startPoint.getX(), startPoint.getY(), endPoint.getX(), endPoint.getY()); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + return drawable; + } + @Override + public java.awt.Rectangle getBounds(Zone zone) { if (bounds == null) { int x = Math.min(startPoint.x, endPoint.x); int y = Math.min(startPoint.y, endPoint.y); @@ -86,7 +109,8 @@ public Point getEndPoint() { return endPoint; } - protected void draw(Graphics2D g) { + @Override + protected void draw(Zone zone, Graphics2D g) { int minX = Math.min(startPoint.x, endPoint.x); int minY = Math.min(startPoint.y, endPoint.y); @@ -104,7 +128,8 @@ protected void draw(Graphics2D g) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldAA); } - protected void drawBackground(Graphics2D g) { + @Override + protected void drawBackground(Zone zone, Graphics2D g) { int minX = Math.min(startPoint.x, endPoint.x); int minY = Math.min(startPoint.y, endPoint.y); diff --git a/src/main/java/net/rptools/maptool/model/drawing/Drawable.java b/src/main/java/net/rptools/maptool/model/drawing/Drawable.java index 2b10791b15..034fa9fcc2 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/Drawable.java +++ b/src/main/java/net/rptools/maptool/model/drawing/Drawable.java @@ -16,12 +16,9 @@ import java.awt.Graphics2D; import java.awt.geom.Area; -import java.util.ArrayList; -import net.rptools.maptool.model.CellPoint; +import javax.annotation.Nonnull; import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.Zone; -import net.rptools.maptool.model.ZonePoint; -import net.rptools.maptool.server.Mapper; import net.rptools.maptool.server.proto.drawing.DrawableDto; import org.apache.logging.log4j.LogManager; @@ -29,15 +26,19 @@ * @author drice */ public interface Drawable { + Drawable copy(); - void draw(Graphics2D g, Pen pen); + void draw(Zone zone, Graphics2D g, Pen pen); - java.awt.Rectangle getBounds(); + java.awt.Rectangle getBounds(Zone zone); - Area getArea(); + @Nonnull + Area getArea(Zone zone); GUID getId(); + void setId(GUID guid); + Zone.Layer getLayer(); void setLayer(Zone.Layer layer); @@ -45,210 +46,27 @@ public interface Drawable { DrawableDto toDto(); static Drawable fromDto(DrawableDto drawableDto) { - switch (drawableDto.getDrawableTypeCase()) { - case SHAPE_DRAWABLE -> { - var dto = drawableDto.getShapeDrawable(); - var shape = Mapper.map(dto.getShape()); - var id = GUID.valueOf(dto.getId()); - var drawable = new ShapeDrawable(id, shape, dto.getUseAntiAliasing()); - if (dto.hasName()) drawable.setName(dto.getName().getValue()); - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - return drawable; - } - case RECTANGLE_DRAWABLE -> { - var dto = drawableDto.getRectangleDrawable(); - var id = GUID.valueOf(dto.getId()); - var startPoint = dto.getStartPoint(); - var endPoint = dto.getEndPoint(); - var drawable = - new Rectangle( - id, startPoint.getX(), startPoint.getY(), endPoint.getX(), endPoint.getY()); - if (dto.hasName()) drawable.setName(dto.getName().getValue()); - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - return drawable; - } - case OVAL_DRAWABLE -> { - var dto = drawableDto.getOvalDrawable(); - var id = GUID.valueOf(dto.getId()); - var startPoint = dto.getStartPoint(); - var endPoint = dto.getEndPoint(); - var drawable = - new Oval(id, startPoint.getX(), startPoint.getY(), endPoint.getX(), endPoint.getY()); - if (dto.hasName()) drawable.setName(dto.getName().getValue()); - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - return drawable; - } - case CROSS_DRAWABLE -> { - var dto = drawableDto.getCrossDrawable(); - var id = GUID.valueOf(dto.getId()); - var startPoint = dto.getStartPoint(); - var endPoint = dto.getEndPoint(); - var drawable = - new Cross(id, startPoint.getX(), startPoint.getY(), endPoint.getX(), endPoint.getY()); - if (dto.hasName()) drawable.setName(dto.getName().getValue()); - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - return drawable; - } - case DRAWN_LABEL -> { - var dto = drawableDto.getDrawnLabel(); - var id = GUID.valueOf(dto.getId()); - var bounds = dto.getBounds(); - var drawable = - new DrawnLabel(id, dto.getText(), Mapper.map(dto.getBounds()), dto.getFont()); - if (dto.hasName()) drawable.setName(dto.getName().getValue()); - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - return drawable; - } - case LINE_SEGMENT -> { - var dto = drawableDto.getLineSegment(); - var id = GUID.valueOf(dto.getId()); - var drawable = new LineSegment(id, dto.getWidth(), dto.getSquareCap()); - var points = drawable.getPoints(); - var pointDtos = dto.getPointsList(); - pointDtos.forEach(p -> points.add(Mapper.map(p))); - if (dto.hasName()) drawable.setName(dto.getName().getValue()); - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - return drawable; - } - case DRAWABLES_GROUP -> { - var dto = drawableDto.getDrawablesGroup(); - var id = GUID.valueOf(dto.getId()); - var elements = new ArrayList(); - var elementDtos = dto.getDrawnElementsList(); - elementDtos.forEach(e -> elements.add(DrawnElement.fromDto(e))); - var drawable = new DrawablesGroup(id, elements); - if (dto.hasName()) drawable.setName(dto.getName().getValue()); - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - return drawable; - } - case RADIUS_CELL_TEMPLATE -> { - var dto = drawableDto.getRadiusCellTemplate(); - var id = GUID.valueOf(dto.getId()); - var drawable = new RadiusCellTemplate(id); - drawable.setZoneId(GUID.valueOf(dto.getZoneId())); - drawable.setRadius(dto.getRadius()); - var vertex = dto.getVertex(); - drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); - if (dto.hasName()) drawable.setName(dto.getName().getValue()); - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - return drawable; - } - case LINE_CELL_TEMPLATE -> { - var dto = drawableDto.getLineCellTemplate(); - var id = GUID.valueOf(dto.getId()); - var drawable = new LineCellTemplate(id); - drawable.setZoneId(GUID.valueOf(dto.getZoneId())); - drawable.setRadius(dto.getRadius()); - var vertex = dto.getVertex(); - drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); - if (!dto.getQuadrant().isEmpty()) { - drawable.setQuadrant(AbstractTemplate.Quadrant.valueOf(dto.getQuadrant())); - } - drawable.setMouseSlopeGreater(dto.getMouseSlopeGreater()); - var pathVertex = dto.getPathVertex(); - drawable.setPathVertex(new ZonePoint(pathVertex.getX(), pathVertex.getY())); - if (dto.hasName()) { - drawable.setName(dto.getName().getValue()); - } - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - return drawable; - } - case RADIUS_TEMPLATE -> { - var dto = drawableDto.getRadiusTemplate(); - var id = GUID.valueOf(dto.getId()); - var drawable = new RadiusTemplate(id); - drawable.setZoneId(GUID.valueOf(dto.getZoneId())); - drawable.setRadius(dto.getRadius()); - var vertex = dto.getVertex(); - drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); - if (dto.hasName()) drawable.setName(dto.getName().getValue()); - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - return drawable; - } - case BURST_TEMPLATE -> { - var dto = drawableDto.getBurstTemplate(); - var id = GUID.valueOf(dto.getId()); - var drawable = new BurstTemplate(id); - drawable.setZoneId(GUID.valueOf(dto.getZoneId())); - drawable.setRadius(dto.getRadius()); - var vertex = dto.getVertex(); - drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); - if (dto.hasName()) drawable.setName(dto.getName().getValue()); - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - return drawable; - } - case CONE_TEMPLATE -> { - var dto = drawableDto.getConeTemplate(); - var id = GUID.valueOf(dto.getId()); - var drawable = new ConeTemplate(id); - drawable.setZoneId(GUID.valueOf(dto.getZoneId())); - drawable.setRadius(dto.getRadius()); - var vertex = dto.getVertex(); - drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); - drawable.setDirection(AbstractTemplate.Direction.valueOf(dto.getDirection())); - if (dto.hasName()) drawable.setName(dto.getName().getValue()); - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - return drawable; - } - case BLAST_TEMPLATE -> { - var dto = drawableDto.getBlastTemplate(); - var id = GUID.valueOf(dto.getId()); - var drawable = new BlastTemplate(id, dto.getOffsetX(), dto.getOffsetY()); - drawable.setZoneId(GUID.valueOf(dto.getZoneId())); - drawable.setRadius(dto.getRadius()); - var vertex = dto.getVertex(); - drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); - drawable.setDirection(AbstractTemplate.Direction.valueOf(dto.getDirection())); - if (dto.hasName()) drawable.setName(dto.getName().getValue()); - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - return drawable; - } - case LINE_TEMPLATE -> { - var dto = drawableDto.getLineTemplate(); - var id = GUID.valueOf(dto.getId()); - var drawable = new LineTemplate(id); - drawable.setZoneId(GUID.valueOf(dto.getZoneId())); - drawable.setRadius(dto.getRadius()); - var vertex = dto.getVertex(); - drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); - if (!dto.getQuadrant().isEmpty()) { - drawable.setQuadrant(AbstractTemplate.Quadrant.valueOf(dto.getQuadrant())); - } - drawable.setMouseSlopeGreater(dto.getMouseSlopeGreater()); - var pathVertex = dto.getPathVertex(); - drawable.setPathVertex(new ZonePoint(pathVertex.getX(), pathVertex.getY())); - drawable.setDoubleWide(dto.getDoubleWide()); - if (dto.hasName()) drawable.setName(dto.getName().getValue()); - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - return drawable; - } - case WALL_TEMPLATE -> { - var dto = drawableDto.getWallTemplate(); - var id = GUID.valueOf(dto.getId()); - var drawable = new WallTemplate(id); - drawable.setZoneId(GUID.valueOf(dto.getZoneId())); - drawable.setRadius(dto.getRadius()); - var vertex = dto.getVertex(); - drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); - drawable.setMouseSlopeGreater(dto.getMouseSlopeGreater()); - var pathVertex = dto.getPathVertex(); - drawable.setPathVertex(new ZonePoint(pathVertex.getX(), pathVertex.getY())); - drawable.setDoubleWide(dto.getDoubleWide()); - if (dto.hasName()) drawable.setName(dto.getName().getValue()); - drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); - - var cellpoints = new ArrayList(); - for (var point : dto.getPointsList()) - cellpoints.add(new CellPoint(point.getX(), point.getY())); - drawable.setPath(cellpoints); - - return drawable; - } + return switch (drawableDto.getDrawableTypeCase()) { + case SHAPE_DRAWABLE -> ShapeDrawable.fromDto(drawableDto.getShapeDrawable()); + case RECTANGLE_DRAWABLE -> Rectangle.fromDto(drawableDto.getRectangleDrawable()); + case OVAL_DRAWABLE -> Oval.fromDto(drawableDto.getOvalDrawable()); + case CROSS_DRAWABLE -> Cross.fromDto(drawableDto.getCrossDrawable()); + case DRAWN_LABEL -> DrawnLabel.fromDto(drawableDto.getDrawnLabel()); + case LINE_SEGMENT -> LineSegment.fromDto(drawableDto.getLineSegment()); + case DRAWABLES_GROUP -> DrawablesGroup.fromDto(drawableDto.getDrawablesGroup()); + case RADIUS_CELL_TEMPLATE -> RadiusCellTemplate.fromDto(drawableDto.getRadiusCellTemplate()); + case LINE_CELL_TEMPLATE -> LineCellTemplate.fromDto(drawableDto.getLineCellTemplate()); + case RADIUS_TEMPLATE -> RadiusTemplate.fromDto(drawableDto.getRadiusTemplate()); + case BURST_TEMPLATE -> BurstTemplate.fromDto(drawableDto.getBurstTemplate()); + case CONE_TEMPLATE -> ConeTemplate.fromDto(drawableDto.getConeTemplate()); + case BLAST_TEMPLATE -> BlastTemplate.fromDto(drawableDto.getBlastTemplate()); + case LINE_TEMPLATE -> LineTemplate.fromDto(drawableDto.getLineTemplate()); + case WALL_TEMPLATE -> WallTemplate.fromDto(drawableDto.getWallTemplate()); default -> { LogManager.getLogger(Drawable.class) .warn("unknown DrawableDto type: " + drawableDto.getDrawableTypeCase()); - return null; + yield null; } - } + }; } } diff --git a/src/main/java/net/rptools/maptool/model/drawing/DrawablesGroup.java b/src/main/java/net/rptools/maptool/model/drawing/DrawablesGroup.java index bd8eb4f018..4065a5d74b 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/DrawablesGroup.java +++ b/src/main/java/net/rptools/maptool/model/drawing/DrawablesGroup.java @@ -18,8 +18,11 @@ import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.geom.Area; +import java.util.ArrayList; import java.util.List; +import javax.annotation.Nonnull; import net.rptools.maptool.model.GUID; +import net.rptools.maptool.model.Zone; import net.rptools.maptool.server.proto.drawing.DrawableDto; import net.rptools.maptool.server.proto.drawing.DrawablesGroupDto; @@ -39,15 +42,29 @@ public DrawablesGroup(GUID id, List drawableList) { this.drawableList = drawableList; } + public DrawablesGroup(DrawablesGroup other) { + super(other); + + this.drawableList = new ArrayList<>(other.drawableList.size()); + for (final var element : other.drawableList) { + this.drawableList.add(new DrawnElement(element)); + } + } + + @Override + public Drawable copy() { + return new DrawablesGroup(this); + } + public List getDrawableList() { return drawableList; } @Override - public Rectangle getBounds() { + public Rectangle getBounds(Zone zone) { Rectangle bounds = null; for (DrawnElement element : drawableList) { - Rectangle drawnBounds = new Rectangle(element.getDrawable().getBounds()); + Rectangle drawnBounds = new Rectangle(element.getDrawable().getBounds(zone)); // Handle pen size Pen pen = element.getPen(); int penSize = (int) (pen.getThickness() / 2 + 1); @@ -64,18 +81,18 @@ public Rectangle getBounds() { } @Override - public Area getArea() { - Area area = null; + public @Nonnull Area getArea(Zone zone) { + Area area = new Area(); for (DrawnElement element : drawableList) { boolean isEraser = element.getPen().isEraser(); - if (area == null) { - if (!isEraser) area = new Area(element.getDrawable().getArea()); - } else { - if (isEraser) { - area.subtract(element.getDrawable().getArea()); - } else { - area.add(element.getDrawable().getArea()); + + if (isEraser) { + // Optimization: erasing from nothing is a no-op. + if (!area.isEmpty()) { + area.subtract(element.getDrawable().getArea(zone)); } + } else { + area.add(element.getDrawable().getArea(zone)); } } return area; @@ -92,16 +109,29 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setDrawablesGroup(dto).build(); } + public static DrawablesGroup fromDto(DrawablesGroupDto dto) { + var id = GUID.valueOf(dto.getId()); + var elements = new ArrayList(); + var elementDtos = dto.getDrawnElementsList(); + elementDtos.forEach(e -> elements.add(DrawnElement.fromDto(e))); + var drawable = new DrawablesGroup(id, elements); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + return drawable; + } + @Override - protected void draw(Graphics2D g) { + protected void draw(Zone zone, Graphics2D g) { // This should never be called for (DrawnElement element : drawableList) { - element.getDrawable().draw(g, element.getPen()); + element.getDrawable().draw(zone, g, element.getPen()); } } @Override - protected void drawBackground(Graphics2D g) { + protected void drawBackground(Zone zone, Graphics2D g) { // This should never be called } } diff --git a/src/main/java/net/rptools/maptool/model/drawing/DrawnElement.java b/src/main/java/net/rptools/maptool/model/drawing/DrawnElement.java index 3cbd9971f3..c96197ebc0 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/DrawnElement.java +++ b/src/main/java/net/rptools/maptool/model/drawing/DrawnElement.java @@ -27,6 +27,10 @@ public DrawnElement(Drawable drawable, Pen pen) { this.pen = pen; } + public DrawnElement(DrawnElement other) { + this(other.drawable.copy(), new Pen(other.pen)); + } + public Drawable getDrawable() { return drawable; } diff --git a/src/main/java/net/rptools/maptool/model/drawing/DrawnLabel.java b/src/main/java/net/rptools/maptool/model/drawing/DrawnLabel.java index 7e33424dd5..b1f246145d 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/DrawnLabel.java +++ b/src/main/java/net/rptools/maptool/model/drawing/DrawnLabel.java @@ -15,14 +15,17 @@ package net.rptools.maptool.model.drawing; import com.google.protobuf.StringValue; +import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.geom.Area; +import javax.annotation.Nonnull; import javax.swing.CellRendererPane; +import javax.swing.text.Style; import net.rptools.maptool.client.swing.TwoToneTextPane; -import net.rptools.maptool.client.tool.drawing.DrawnTextTool; import net.rptools.maptool.model.GUID; +import net.rptools.maptool.model.Zone; import net.rptools.maptool.server.Mapper; import net.rptools.maptool.server.proto.drawing.DrawableDto; import net.rptools.maptool.server.proto.drawing.DrawnLabelDto; @@ -35,13 +38,13 @@ public class DrawnLabel extends AbstractDrawing { /** The bounds of the display rectangle */ - private Rectangle bounds = new Rectangle(); + private final Rectangle bounds; /** Text being painted. */ - private String text; + private final String text; /** The font used to paint the text. */ - private String font; + private final String font; /** The pane used to render the text */ private transient CellRendererPane renderer; @@ -49,20 +52,6 @@ public class DrawnLabel extends AbstractDrawing { /** The text pane used to paint the text. */ private transient TwoToneTextPane textPane; - /** - * Create a new drawn label. - * - * @param theText Text to be drawn - * @param theBounds The bounds containing the text. - * @param aFont The font used to draw the text as a string that can be passed to {@link - * Font#decode(java.lang.String)}. - */ - public DrawnLabel(String theText, Rectangle theBounds, String aFont) { - text = theText; - bounds = theBounds; - font = aFont; - } - public DrawnLabel(GUID id, String theText, Rectangle theBounds, String aFont) { super(id); text = theText; @@ -70,6 +59,18 @@ public DrawnLabel(GUID id, String theText, Rectangle theBounds, String aFont) { font = aFont; } + public DrawnLabel(DrawnLabel other) { + super(other); + this.bounds = new Rectangle(other.bounds); + this.text = other.text; + this.font = other.font; + } + + @Override + public Drawable copy() { + return new DrawnLabel(this); + } + public String getText() { return text; } @@ -78,32 +79,26 @@ public String getFont() { return font; } - /** - * @see net.rptools.maptool.model.drawing.Drawable#draw(java.awt.Graphics2D, - * net.rptools.maptool.model.drawing.Pen) - */ - public void draw(Graphics2D aG) { + public void draw(Zone zone, Graphics2D aG) { if (renderer == null) { renderer = new CellRendererPane(); - textPane = DrawnTextTool.createTextPane(bounds, null, font); + textPane = createTextPane(bounds, font); textPane.setText(text); } renderer.paintComponent(aG, textPane, null, bounds); } @Override - protected void drawBackground(Graphics2D g) {} + protected void drawBackground(Zone zone, Graphics2D g) {} - /** - * @see net.rptools.maptool.model.drawing.Drawable#getBounds() - */ - public Rectangle getBounds() { + @Override + public Rectangle getBounds(Zone zone) { return bounds; } - public Area getArea() { - // TODO Auto-generated method stub - return null; + @Override + public @Nonnull Area getArea(Zone zone) { + return new Area(); } @Override @@ -111,7 +106,7 @@ public DrawableDto toDto() { var dto = DrawnLabelDto.newBuilder(); dto.setId(getId().toString()) .setLayer(getLayer().name()) - .setBounds(Mapper.map(getBounds())) + .setBounds(Mapper.map(bounds)) .setText(getText()) .setFont(getFont()); @@ -119,4 +114,35 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setDrawnLabel(dto).build(); } + + public static DrawnLabel fromDto(DrawnLabelDto dto) { + var id = GUID.valueOf(dto.getId()); + var drawable = new DrawnLabel(id, dto.getText(), Mapper.map(dto.getBounds()), dto.getFont()); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + return drawable; + } + + /** + * Create a text pane with the passed bounds, pen, and font data + * + * @param bounds Bounds of the new text pane + * @param font Font used to pain the text + * @return A text pane used to draw text + */ + private static TwoToneTextPane createTextPane(Rectangle bounds, String font) { + // Create a text component and place it on the renderer's component + TwoToneTextPane textPane = new TwoToneTextPane(); + textPane.setBounds(bounds); + textPane.setOpaque(false); + textPane.setBackground(new Color(0, 0, 0, 0)); // Transparent + + // Create a style for the component + Style style = textPane.addStyle("default", null); + TwoToneTextPane.setFont(style, Font.decode(font)); + textPane.setLogicalStyle(style); + return textPane; + } } diff --git a/src/main/java/net/rptools/maptool/model/drawing/LineCellTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/LineCellTemplate.java index 1a5a5f87c7..fec9380ed0 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/LineCellTemplate.java +++ b/src/main/java/net/rptools/maptool/model/drawing/LineCellTemplate.java @@ -18,13 +18,11 @@ import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.geom.Area; -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; -import net.rptools.maptool.client.MapTool; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import net.rptools.maptool.model.CellPoint; import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.Zone; @@ -43,22 +41,16 @@ public class LineCellTemplate extends AbstractTemplate { *-------------------------------------------------------------------------------------------*/ /** This vertex is used to determine the path. */ - private ZonePoint pathVertex; + private ZonePoint pathVertex = null; /** The calculated path for this line. */ - private List path; + private transient List path; /** The pool of points. */ - private List pool; + private transient List pool; - /** - * The line is drawn in this quadrant. A string is used as a hack to get around the hessian - * library's problem w/ serialization of enums - */ - private String quadrant = null; - - /** Flag used to determine mouse position relative to vertex position */ - private boolean mouseSlopeGreater; + /** The line is drawn in this quadrant. */ + private transient Quadrant quadrant = null; public LineCellTemplate() {} @@ -66,6 +58,11 @@ public LineCellTemplate(GUID id) { super(id); } + public LineCellTemplate(LineCellTemplate other) { + super(other); + this.pathVertex = new ZonePoint(other.pathVertex); + } + /*--------------------------------------------------------------------------------------------- * Overridden AbstractTemplate Methods *-------------------------------------------------------------------------------------------*/ @@ -92,8 +89,15 @@ protected void paintBorder( // Have to scan 3 points behind and ahead, since that is the maximum number of points // that can be added to the path from any single intersection. boolean[] noPaint = new boolean[4]; + final var path = getPath(); + if (path == null) { + return; + } + for (int i = pElement - 3; i < pElement + 3; i++) { - if (i < 0 || i >= path.size() || i == pElement) continue; + if (i < 0 || i >= path.size() || i == pElement) { + continue; + } CellPoint p = path.get(i); // Ignore diagonal cells and cells that are not adjacent @@ -112,28 +116,18 @@ protected void paintBorder( if (!noPaint[3]) paintCloseHorizontalBorder(g, xOff, yOff, gridSize, getQuadrant()); } - /** - * @see net.rptools.maptool.model.drawing.AbstractTemplate#paint(java.awt.Graphics2D, boolean, - * boolean) - */ @Override - protected void paint(Graphics2D g, boolean border, boolean area) { - if (MapTool.getCampaign().getZone(getZoneId()) == null) { + protected void paint(Zone zone, Graphics2D g, boolean border, boolean area) { + if (zone == null) { return; } - // Need to paint? We need a line and to translate the painting - if (pathVertex == null) { - return; - } - if (getRadius() == 0) { - return; - } - if (calcPath() == null) { + final var path = getPath(); + if (path == null) { return; } // Paint each element in the path - int gridSize = MapTool.getCampaign().getZone(getZoneId()).getGrid().getSize(); + int gridSize = zone.getGrid().getSize(); ListIterator i = path.listIterator(); while (i.hasNext()) { CellPoint p = i.next(); @@ -141,13 +135,20 @@ protected void paint(Graphics2D g, boolean border, boolean area) { int yOff = p.y * gridSize; int distance = getDistance(p.x, p.y); - if (quadrant.equals(Quadrant.NORTH_EAST.name())) { - yOff = yOff - gridSize; - } else if (quadrant.equals(Quadrant.SOUTH_WEST.name())) { - xOff = xOff - gridSize; - } else if (quadrant.equals(Quadrant.NORTH_WEST.name())) { - xOff = xOff - gridSize; - yOff = yOff - gridSize; + switch (getQuadrant()) { + case NORTH_EAST -> { + yOff = yOff - gridSize; + } + case SOUTH_WEST -> { + xOff = xOff - gridSize; + } + case NORTH_WEST -> { + xOff = xOff - gridSize; + yOff = yOff - gridSize; + } + case SOUTH_EAST -> { + // Nothing to do. + } } // Paint what is needed. @@ -188,86 +189,70 @@ public void setRadius(int squares) { /** * Calculate the path * + *

The path is always calculated as if it were in the south-east quadrant. I.e., the x and y + * coordinates of the path points will never decrease. + * * @return The new path or null if there is no path. */ - protected List calcPath() { - if (getRadius() == 0) { - return null; - } - if (pathVertex == null) { - return null; - } + protected @Nullable List calcPath() { int radius = getRadius(); + ZonePoint vertex = getVertex(); + if (radius == 0 || vertex == null || pathVertex == null) { + return null; + } // Is there a slope? - ZonePoint vertex = getVertex(); if (vertex.equals(pathVertex)) { return null; } - double dx = pathVertex.x - vertex.x; - double dy = pathVertex.y - vertex.y; - setQuadrant( - (dx < 0) - ? (dy < 0 ? Quadrant.NORTH_WEST : Quadrant.SOUTH_WEST) - : (dy < 0 ? Quadrant.NORTH_EAST : Quadrant.SOUTH_EAST)); + double dx = Math.abs(pathVertex.x - vertex.x); + double dy = Math.abs(pathVertex.y - vertex.y); + final boolean isShallowSlope = dx >= dy; + // To start, a half cell deviation is enough to switch rows. + double deviationInY = 0.5; + + final var path = new ArrayList(); // Start the line at 0,0 - clearPath(); - path = new ArrayList(); - path.add(getPointFromPool(0, 0)); - - MathContext mc = MathContext.DECIMAL128; - MathContext rmc = new MathContext(MathContext.DECIMAL64.getPrecision(), RoundingMode.CEILING); - if (dx != 0 && dy != 0) { - BigDecimal m = BigDecimal.valueOf(dy).divide(BigDecimal.valueOf(dx), mc).abs(); - - // Find the path - CellPoint p = path.get(path.size() - 1); - while (getDistance(p.x, p.y) < radius) { - int x = p.x; - int y = p.y; - - // Which border does the point exit the cell? - double xValue = BigDecimal.valueOf(y + 1).divide(m, mc).round(rmc).doubleValue(); - double yValue = BigDecimal.valueOf(x + 1).multiply(m, mc).round(rmc).doubleValue(); - - if (xValue == x + 1 && yValue == y + 1) { - // Special case, right on the diagonal - path.add(getPointFromPool(x + 1, y + 1)); - } else if (Math.round(xValue) == x + 1) { - path.add(getPointFromPool(x + 1, y + 1)); - } else if (Math.round(xValue) == x) { - path.add(getPointFromPool(x, y + 1)); - } else if (Math.round(yValue) == y + 1) { - path.add(getPointFromPool(x + 1, y + 1)); - } else if (Math.round(yValue) == y) { - path.add(getPointFromPool(x + 1, y)); - } else { - return path; - } // endif - p = path.get(path.size() - 1); - } // endwhile - - // Clear the last of the pool - if (pool != null) { - pool.clear(); - pool = null; - } // endif - } else { - // Straight line - int xInc = dx != 0 ? 1 : 0; - int yInc = dy != 0 ? 1 : 0; - int x = xInc; - int y = yInc; - int xTouch = (dx != 0) ? 0 : -1; - int yTouch = (dy != 0) ? 0 : -1; - while (getDistance(x, y) <= radius) { - path.add(getPointFromPool(x, y)); - x += xInc; - y += yInc; - } // endwhile - } // endif + CellPoint p = getPointFromPool(0, 0); + path.add(p); + + // In this loop we pretend we have a shallow slope. If that's not true, we'll fix it afterward. + double slope = isShallowSlope ? (dy / dx) : (dx / dy); + assert slope >= 0; + while (getDistance(p.x, p.y) < radius) { + p = getPointFromPool(p.x, p.y); + + // Step to the next column. + ++p.x; + + // Step to the next row if the ideal line has deviated enough. + // y-value always goes up, so we don't need to check the < 0 case. + deviationInY += slope; + if (deviationInY >= 1) { + ++p.y; + deviationInY -= 1; + } + + path.add(p); + } + + if (!isShallowSlope) { + // All our x-values should be y-values and vice versa. So swap them all. + for (final var point : path) { + final var tmp = point.x; + point.x = point.y; + point.y = tmp; + } + } + + // Clear out the last of the pool. + if (pool != null) { + pool.clear(); + pool = null; + } + return path; } @@ -278,7 +263,7 @@ protected List calcPath() { * @param y The y coordinate of the new point. * @return The new point. */ - public CellPoint getPointFromPool(int x, int y) { + private CellPoint getPointFromPool(int x, int y) { CellPoint p = null; if (pool != null) { p = pool.remove(pool.size() - 1); @@ -292,22 +277,13 @@ public CellPoint getPointFromPool(int x, int y) { return p; } - /** - * Add a point back to the pool. - * - * @param p Add this point back - */ - public void addPointToPool(CellPoint p) { - if (pool != null) pool.add(p); - } - /** * Get the pathVertex for this LineTemplate. * * @return Returns the current value of pathVertex. */ public ZonePoint getPathVertex() { - return pathVertex; + return pathVertex == null ? null : new ZonePoint(pathVertex); } /** @@ -324,7 +300,8 @@ public void setPathVertex(ZonePoint pathVertex) { } /** Clear the current path. This will cause it to be recalculated during the next draw. */ - public void clearPath() { + private void clearPath() { + quadrant = null; if (path != null) { pool = path; } @@ -336,134 +313,110 @@ public void clearPath() { * * @return Returns the current value of quadrant. */ - public Quadrant getQuadrant() { - if (quadrant != null) { - return Quadrant.valueOf(quadrant); - } - return null; - } - - /** - * Set the value of quadrant for this LineTemplate. - * - * @param quadrant The quadrant to set. - */ - public void setQuadrant(Quadrant quadrant) { - if (quadrant != null) { - this.quadrant = quadrant.name(); - } else { - this.quadrant = null; + private @Nonnull Quadrant getQuadrant() { + if (quadrant == null) { + final var vertex = getVertex(); + if (vertex == null || pathVertex == null || pathVertex.equals(vertex)) { + // Not a valid line, so quadrant is meaningless. Just pick one. + quadrant = Quadrant.NORTH_WEST; + } else { + double dx = pathVertex.x - vertex.x; + double dy = pathVertex.y - vertex.y; + quadrant = + (dx < 0) + ? (dy < 0 ? Quadrant.NORTH_WEST : Quadrant.SOUTH_WEST) + : (dy < 0 ? Quadrant.NORTH_EAST : Quadrant.SOUTH_EAST); + } } - } - /** - * Get the mouseSlopeGreater for this LineTemplate. - * - * @return Returns the current value of mouseSlopeGreater. - */ - public boolean isMouseSlopeGreater() { - return mouseSlopeGreater; - } - - /** - * Set the value of mouseSlopeGreater for this LineTemplate. - * - * @param aMouseSlopeGreater The mouseSlopeGreater to set. - */ - public void setMouseSlopeGreater(boolean aMouseSlopeGreater) { - mouseSlopeGreater = aMouseSlopeGreater; + return quadrant; } /** * @return Getter for path */ - public List getPath() { - return path; - } + private @Nullable List getPath() { + if (path == null) { + path = calcPath(); + } - /** - * @param path Setter for the path to set - */ - public void setPath(List path) { - this.path = path; + return path; } /*--------------------------------------------------------------------------------------------- * Drawable Interface Methods *-------------------------------------------------------------------------------------------*/ - /** - * @see net.rptools.maptool.model.drawing.Drawable#getBounds() - */ - public Rectangle getBounds() { + @Override + public Drawable copy() { + return new LineCellTemplate(this); + } + + @Override + public Rectangle getBounds(Zone zone) { // Get all of the numbers needed for the calculation - if (MapTool.getCampaign().getZone(getZoneId()) == null) { + if (zone == null) { return new Rectangle(); } - int gridSize = MapTool.getCampaign().getZone(getZoneId()).getGrid().getSize(); - ZonePoint vertex = getVertex(); - - // Find the point that is farthest away in the path, then adjust - ZonePoint minp = null; - ZonePoint maxp = null; + // The end of the path is the point further away from vertex. + final var path = getPath(); if (path == null) { - calcPath(); - if (path == null) { - // If the calculated path is still null, then the line is invalid. - return new Rectangle(); - } + // If the path is null, the line is invalid. + return new Rectangle(); } - for (CellPoint pt : path) { - ZonePoint p = MapTool.getCampaign().getZone(getZoneId()).getGrid().convert(pt); - p = new ZonePoint(vertex.x + p.x, vertex.y + p.y); - if (minp == null) { - minp = new ZonePoint(p.x, p.y); - maxp = new ZonePoint(p.x, p.y); - } - minp.x = Math.min(minp.x, p.x); - minp.y = Math.min(minp.y, p.y); + var first = new CellPoint(path.getFirst()); + var last = new CellPoint(path.getLast()); - maxp.x = Math.max(maxp.x, p.x); - maxp.y = Math.max(maxp.y, p.y); - } - maxp.x += gridSize; - maxp.y += gridSize; - - // The path is only calculated for the south-east quadrant, so - // appropriately reflect the bounding box around the starting vertex. - if (getXMult(getQuadrant()) < 0) { - minp.x -= gridSize; - maxp.x -= gridSize; - } - if (getYMult(getQuadrant()) < 0) { - minp.y -= gridSize; - maxp.y -= gridSize; - } - int width = (maxp.x - minp.x); - int height = (maxp.y - minp.y); + // `first` should be (0, 0), but let's not rely on that. + final var quadrant = getQuadrant(); + first.x *= getXMult(quadrant); + last.x *= getXMult(quadrant); + first.y *= getYMult(quadrant); + last.y *= getYMult(quadrant); + + // Now convert to zone points. + ZonePoint firstZonePoint = zone.getGrid().convert(first); + ZonePoint lastZonePoint = zone.getGrid().convert(last); + + ZonePoint vertex = getVertex(); + int gridSize = zone.getGrid().getSize(); + ZonePoint minZonePoint = + new ZonePoint( + vertex.x + Math.min(firstZonePoint.x, lastZonePoint.x), + vertex.y + Math.min(firstZonePoint.y, lastZonePoint.y)); + ZonePoint maxZonePoint = + new ZonePoint( + vertex.x + Math.max(firstZonePoint.x, lastZonePoint.x) + gridSize, + vertex.y + Math.max(firstZonePoint.y, lastZonePoint.y) + gridSize); // Account for pen size // We don't really know what the pen size will be, so give a very rough // overestimate // We'll have to figure this out someday - minp.x -= 10; - minp.y -= 10; - width += 20; - height += 20; - - return new Rectangle(minp.x, minp.y, width, height); + minZonePoint.x -= 10; + minZonePoint.y -= 10; + maxZonePoint.x += 10; + maxZonePoint.y += 10; + + return new Rectangle( + minZonePoint.x, + minZonePoint.y, + maxZonePoint.x - minZonePoint.x, + maxZonePoint.y - minZonePoint.y); } @Override - public Area getArea() { - if (path == null) { - calcPath(); + public @Nonnull Area getArea(Zone zone) { + if (zone == null) { + return new Area(); } - Zone zone = MapTool.getCampaign().getZone(getZoneId()); - if (path == null || zone == null || getRadius() == 0 || pathVertex == null) { + + final var path = getPath(); + if (path == null) { return new Area(); } + // Create an area by merging all the squares along the path Area result = new Area(); int gridSize = zone.getGrid().getSize(); @@ -476,13 +429,20 @@ public Area getArea() { int xOff = p.x * gridSize; int yOff = p.y * gridSize; - if (quadrant.equals(Quadrant.NORTH_EAST.name())) { - yOff = yOff - gridSize; - } else if (quadrant.equals(Quadrant.SOUTH_WEST.name())) { - xOff = xOff - gridSize; - } else if (quadrant.equals(Quadrant.NORTH_WEST.name())) { - xOff = xOff - gridSize; - yOff = yOff - gridSize; + switch (q) { + case NORTH_EAST -> { + yOff = yOff - gridSize; + } + case SOUTH_WEST -> { + xOff = xOff - gridSize; + } + case NORTH_WEST -> { + xOff = xOff - gridSize; + yOff = yOff - gridSize; + } + case SOUTH_EAST -> { + // Nothing to do. + } } int rx = getVertex().x + getXMult(q) * xOff + ((getXMult(q) - 1) / 2) * gridSize; @@ -494,24 +454,14 @@ public Area getArea() { @Override public DrawableDto toDto() { - - if (getQuadrant() == null) { - calcPath(); // force calculation of the quadrent - } - var dto = LineCellTemplateDto.newBuilder(); dto.setId(getId().toString()) .setLayer(getLayer().name()) - .setZoneId(getZoneId().toString()) .setRadius(getRadius()) - .setVertex(getVertex().toDto()) - .setMouseSlopeGreater(isMouseSlopeGreater()); + .setVertex(getVertex().toDto()); - if (getQuadrant() != null) { - dto.setQuadrant(getQuadrant().name()); - } - if (getPathVertex() != null) { - dto.setPathVertex(getPathVertex().toDto()); + if (pathVertex != null) { + dto.setPathVertex(pathVertex.toDto()); } if (getName() != null) { @@ -520,4 +470,19 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setLineCellTemplate(dto).build(); } + + public static LineCellTemplate fromDto(LineCellTemplateDto dto) { + var id = GUID.valueOf(dto.getId()); + var drawable = new LineCellTemplate(id); + drawable.setRadius(dto.getRadius()); + var vertex = dto.getVertex(); + drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); + var pathVertex = dto.getPathVertex(); + drawable.setPathVertex(new ZonePoint(pathVertex.getX(), pathVertex.getY())); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + return drawable; + } } diff --git a/src/main/java/net/rptools/maptool/model/drawing/LineSegment.java b/src/main/java/net/rptools/maptool/model/drawing/LineSegment.java index 4b1faa9bc7..68c268e396 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/LineSegment.java +++ b/src/main/java/net/rptools/maptool/model/drawing/LineSegment.java @@ -23,8 +23,10 @@ import java.awt.geom.GeneralPath; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import javax.annotation.Nonnull; import net.rptools.maptool.model.GUID; +import net.rptools.maptool.model.Zone; import net.rptools.maptool.server.Mapper; import net.rptools.maptool.server.proto.drawing.DrawableDto; import net.rptools.maptool.server.proto.drawing.LineSegmentDrawableDto; @@ -51,6 +53,21 @@ public LineSegment(GUID id, float width, boolean squareCap) { this.squareCap = squareCap; } + public LineSegment(LineSegment other) { + super(other); + this.width = other.width; + this.squareCap = other.squareCap; + + for (final var point : other.points) { + this.points.add(new Point(point)); + } + } + + @Override + public Drawable copy() { + return new LineSegment(this); + } + @SuppressWarnings("ConstantValue") private Object readResolve() { if (width == null) { @@ -73,11 +90,12 @@ public List getPoints() { return points; } - public Area getArea() { + @Override + public @Nonnull Area getArea(Zone zone) { if (area == null) { area = createLineArea(); } - return area; + return Objects.requireNonNullElseGet(area, Area::new); } @Override @@ -94,8 +112,23 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setLineSegment(dto).build(); } + public static LineSegment fromDto(LineSegmentDrawableDto dto) { + var id = GUID.valueOf(dto.getId()); + var drawable = new LineSegment(id, dto.getWidth(), dto.getSquareCap()); + var points = drawable.getPoints(); + var pointDtos = dto.getPointsList(); + pointDtos.forEach(p -> points.add(Mapper.map(p))); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + return drawable; + } + private Area createLineArea() { - if (points.size() < 1) return null; + if (points.size() < 1) { + return null; + } GeneralPath gp = null; for (Point point : points) { if (gp == null) { @@ -110,24 +143,20 @@ private Area createLineArea() { } @Override - protected void draw(Graphics2D g) { + protected void draw(Zone zone, Graphics2D g) { width = ((BasicStroke) g.getStroke()).getLineWidth(); squareCap = ((BasicStroke) g.getStroke()).getEndCap() == BasicStroke.CAP_SQUARE; - Area area = getArea(); - if (area != null) g.fill(area); + Area area = getArea(zone); + g.fill(area); } @Override - protected void drawBackground(Graphics2D g) { + protected void drawBackground(Zone zone, Graphics2D g) { // do nothing } - /* - * (non-Javadoc) - * - * @see net.rptools.maptool.model.drawing.Drawable#getBounds() - */ - public Rectangle getBounds() { + @Override + public Rectangle getBounds(Zone zone) { if (lastPointCount == points.size()) { return cachedBounds; } diff --git a/src/main/java/net/rptools/maptool/model/drawing/LineTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/LineTemplate.java index 4ebfab6c4d..dff43ffb95 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/LineTemplate.java +++ b/src/main/java/net/rptools/maptool/model/drawing/LineTemplate.java @@ -24,8 +24,8 @@ import java.util.ArrayList; import java.util.List; import java.util.ListIterator; +import javax.annotation.Nonnull; import net.rptools.maptool.client.AppState; -import net.rptools.maptool.client.MapTool; import net.rptools.maptool.model.CellPoint; import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.Zone; @@ -72,6 +72,30 @@ public LineTemplate(GUID id) { super(id); } + public LineTemplate(LineTemplate other) { + super(other); + + this.doubleWide = other.doubleWide; + this.pathVertex = new ZonePoint(other.pathVertex); + + if (other.path != null) { + this.path = new ArrayList<>(other.path.size()); + for (final var cellPoint : other.path) { + this.path.add(new CellPoint(cellPoint)); + } + } + + if (other.pool != null) { + this.pool = new ArrayList<>(other.pool.size()); + for (final var cellPoint : other.pool) { + this.pool.add(new CellPoint(cellPoint)); + } + } + + this.quadrant = other.quadrant; + this.mouseSlopeGreater = other.mouseSlopeGreater; + } + /*--------------------------------------------------------------------------------------------- * Overridden AbstractTemplate Methods *-------------------------------------------------------------------------------------------*/ @@ -118,22 +142,18 @@ protected void paintBorder( if (!noPaint[3]) paintCloseHorizontalBorder(g, xOff, yOff, gridSize, getQuadrant()); } - /** - * @see net.rptools.maptool.model.drawing.AbstractTemplate#paint(java.awt.Graphics2D, boolean, - * boolean) - */ @Override - protected void paint(Graphics2D g, boolean border, boolean area) { - if (MapTool.getCampaign().getZone(getZoneId()) == null) { + protected void paint(Zone zone, Graphics2D g, boolean border, boolean area) { + if (zone == null) { return; } // Need to paint? We need a line and to translate the painting - if (pathVertex == null) return; - if (getRadius() == 0) return; - if (calcPath() == null) return; + if (pathVertex == null || getRadius() == 0 || calcPath() == null) { + return; + } // Paint each element in the path - int gridSize = MapTool.getCampaign().getZone(getZoneId()).getGrid().getSize(); + int gridSize = zone.getGrid().getSize(); ListIterator i = path.listIterator(); while (i.hasNext()) { CellPoint p = i.next(); @@ -384,15 +404,18 @@ public void setPath(List path) { * Drawable Interface Methods *-------------------------------------------------------------------------------------------*/ - /** - * @see net.rptools.maptool.model.drawing.Drawable#getBounds() - */ - public Rectangle getBounds() { + @Override + public Drawable copy() { + return new LineTemplate(this); + } + + @Override + public Rectangle getBounds(Zone zone) { // Get all of the numbers needed for the calculation - if (MapTool.getCampaign().getZone(getZoneId()) == null) { + if (zone == null) { return new Rectangle(); } - int gridSize = MapTool.getCampaign().getZone(getZoneId()).getGrid().getSize(); + int gridSize = zone.getGrid().getSize(); ZonePoint vertex = getVertex(); // Find the point that is farthest away in the path, then adjust @@ -406,7 +429,7 @@ public Rectangle getBounds() { } } for (CellPoint pt : path) { - ZonePoint p = MapTool.getCampaign().getZone(getZoneId()).getGrid().convert(pt); + ZonePoint p = zone.getGrid().convert(pt); p = new ZonePoint(vertex.x + p.x, vertex.y + p.y); if (minp == null) { @@ -452,11 +475,10 @@ public Rectangle getBounds() { } @Override - public Area getArea() { + public @Nonnull Area getArea(Zone zone) { if (path == null) { calcPath(); } - Zone zone = MapTool.getCampaign().getZone(getZoneId()); if (path == null || zone == null || getRadius() == 0 || pathVertex == null) { return new Area(); } @@ -483,7 +505,6 @@ public DrawableDto toDto() { var dto = LineTemplateDto.newBuilder(); dto.setId(getId().toString()) .setLayer(getLayer().name()) - .setZoneId(getZoneId().toString()) .setRadius(getRadius()) .setVertex(getVertex().toDto()) .setMouseSlopeGreater(isMouseSlopeGreater()) @@ -499,4 +520,24 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setLineTemplate(dto).build(); } + + public static LineTemplate fromDto(LineTemplateDto dto) { + var id = GUID.valueOf(dto.getId()); + var drawable = new LineTemplate(id); + drawable.setRadius(dto.getRadius()); + var vertex = dto.getVertex(); + drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); + if (!dto.getQuadrant().isEmpty()) { + drawable.setQuadrant(AbstractTemplate.Quadrant.valueOf(dto.getQuadrant())); + } + drawable.setMouseSlopeGreater(dto.getMouseSlopeGreater()); + var pathVertex = dto.getPathVertex(); + drawable.setPathVertex(new ZonePoint(pathVertex.getX(), pathVertex.getY())); + drawable.setDoubleWide(dto.getDoubleWide()); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + return drawable; + } } diff --git a/src/main/java/net/rptools/maptool/model/drawing/Oval.java b/src/main/java/net/rptools/maptool/model/drawing/Oval.java index 060b6c369b..9123ed0c9f 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/Oval.java +++ b/src/main/java/net/rptools/maptool/model/drawing/Oval.java @@ -18,7 +18,9 @@ import java.awt.Graphics2D; import java.awt.geom.Area; import java.awt.geom.Ellipse2D; +import javax.annotation.Nonnull; import net.rptools.maptool.model.GUID; +import net.rptools.maptool.model.Zone; import net.rptools.maptool.server.Mapper; import net.rptools.maptool.server.proto.drawing.DrawableDto; import net.rptools.maptool.server.proto.drawing.OvalDrawableDto; @@ -39,8 +41,17 @@ public Oval(GUID id, int x, int y, int width, int height) { super(id, x, y, width, height); } + public Oval(Oval other) { + super(other); + } + @Override - protected void draw(Graphics2D g) { + public Drawable copy() { + return new Oval(this); + } + + @Override + protected void draw(Zone zone, Graphics2D g) { int minX = Math.min(startPoint.x, endPoint.x); int minY = Math.min(startPoint.y, endPoint.y); @@ -51,7 +62,7 @@ protected void draw(Graphics2D g) { } @Override - protected void drawBackground(Graphics2D g) { + protected void drawBackground(Zone zone, Graphics2D g) { int minX = Math.min(startPoint.x, endPoint.x); int minY = Math.min(startPoint.y, endPoint.y); @@ -62,8 +73,8 @@ protected void drawBackground(Graphics2D g) { } @Override - public Area getArea() { - java.awt.Rectangle r = getBounds(); + public @Nonnull Area getArea(Zone zone) { + java.awt.Rectangle r = getBounds(zone); return new Area(new Ellipse2D.Double(r.x, r.y, r.width, r.height)); } @@ -78,4 +89,17 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setOvalDrawable(dto).build(); } + + public static Oval fromDto(OvalDrawableDto dto) { + var id = GUID.valueOf(dto.getId()); + var startPoint = dto.getStartPoint(); + var endPoint = dto.getEndPoint(); + var drawable = + new Oval(id, startPoint.getX(), startPoint.getY(), endPoint.getX(), endPoint.getY()); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + return drawable; + } } diff --git a/src/main/java/net/rptools/maptool/model/drawing/RadiusCellTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/RadiusCellTemplate.java index cb527e89cf..5f4af61aa3 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/RadiusCellTemplate.java +++ b/src/main/java/net/rptools/maptool/model/drawing/RadiusCellTemplate.java @@ -17,9 +17,8 @@ import com.google.protobuf.StringValue; import java.awt.Graphics2D; import java.awt.Rectangle; -import java.awt.Shape; import java.awt.geom.Area; -import net.rptools.maptool.client.MapTool; +import javax.annotation.Nonnull; import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.ZonePoint; @@ -33,19 +32,21 @@ * @author naciron */ public class RadiusCellTemplate extends AbstractTemplate { - - /** Renderer for the blast. The {@link Shape} is just a rectangle. */ - private final ShapeDrawable renderer = new ShapeDrawable(new Rectangle()); - - /** Renderer for the blast. The {@link Shape} is just a rectangle. */ - private final ShapeDrawable vertexRenderer = new ShapeDrawable(new Rectangle()); - public RadiusCellTemplate() {} public RadiusCellTemplate(GUID id) { super(id); } + public RadiusCellTemplate(RadiusCellTemplate other) { + super(other); + } + + @Override + public Drawable copy() { + return new RadiusCellTemplate(this); + } + /** * Paint the border at a specific radius. * @@ -150,20 +151,13 @@ protected void paintArea( } } - /** - * Paint the border or area of the template - * - * @param g Where to paint - * @param border Paint the border? - * @param area Paint the area? - */ - protected void paint(Graphics2D g, boolean border, boolean area) { + @Override + protected void paint(Zone zone, Graphics2D g, boolean border, boolean area) { int radius = getRadius(); - GUID zoneId = getZoneId(); - if (radius == 0) return; - Zone zone = MapTool.getCampaign().getZone(zoneId); - if (zone == null) return; + if (zone == null || radius == 0) { + return; + } // Find the proper distance int gridSize = zone.getGrid().getSize(); @@ -175,8 +169,12 @@ protected void paint(Graphics2D g, boolean border, boolean area) { int yOff = y * gridSize; // Template specific painting - if (border) paintBorder(g, x, y, xOff, yOff, gridSize, getDistance(x, y)); - if (area) paintArea(g, x, y, xOff, yOff, gridSize, getDistance(x, y)); + if (border) { + paintBorder(g, x, y, xOff, yOff, gridSize, getDistance(x, y)); + } + if (area) { + paintArea(g, x, y, xOff, yOff, gridSize, getDistance(x, y)); + } } // endfor } // endfor } @@ -221,15 +219,8 @@ protected int getYMult(Quadrant q) { * Drawable Interface Methods *-------------------------------------------------------------------------------------------*/ - /** - * @see net.rptools.maptool.model.drawing.Drawable#getBounds() - */ - public Rectangle getBounds() { - if (getZoneId() == null) { - // This avoids a NPE when loading up a campaign - return new Rectangle(); - } - Zone zone = MapTool.getCampaign().getZone(getZoneId()); + @Override + public Rectangle getBounds(Zone zone) { if (zone == null) { return new Rectangle(); } @@ -245,40 +236,6 @@ public Rectangle getBounds() { return new Rectangle(x, y, w, h); } - /** - * This methods adjusts the rectangle in the renderer to match the new radius, vertex, or - * direction. Due to the fact that it is impossible to draw to the cardinal directions evenly when - * the radius is an even number and still stay in the squares, that case isn't allowed. - */ - private void adjustShape() { - if (getZoneId() == null) return; - Zone zone; - if (MapTool.isHostingServer()) { - zone = MapTool.getServer().getCampaign().getZone(getZoneId()); - } else { - zone = MapTool.getCampaign().getZone(getZoneId()); - } - if (zone == null) return; - - int gridSize = zone.getGrid().getSize(); - Rectangle r = (Rectangle) vertexRenderer.getShape(); - r.setBounds(getVertex().x, getVertex().y, gridSize, gridSize); - r = (Rectangle) renderer.getShape(); - r.setBounds(getVertex().x, getVertex().y, gridSize, gridSize); - r.x -= getRadius() * gridSize; - r.y -= getRadius() * gridSize; - r.width = r.height = (getRadius() * 2 + 1) * gridSize; - } - - /** - * @see net.rptools.maptool.model.drawing.AbstractTemplate#setRadius(int) - */ - @Override - public void setRadius(int squares) { - super.setRadius(squares); - adjustShape(); - } - /** * Get the distance to a specific coordinate. * @@ -294,22 +251,8 @@ public int getDistance(int x, int y) { return distance; } - /** - * @see - * net.rptools.maptool.model.drawing.AbstractTemplate#setVertex(net.rptools.maptool.model.ZonePoint) - */ - @Override - public void setVertex(ZonePoint vertex) { - super.setVertex(vertex); - adjustShape(); - } - @Override - public Area getArea() { - if (getZoneId() == null) { - return new Area(); - } - Zone zone = getCampaign().getZone(getZoneId()); + public @Nonnull Area getArea(Zone zone) { if (zone == null) { return new Area(); } @@ -344,7 +287,6 @@ public DrawableDto toDto() { var dto = RadiusCellTemplateDto.newBuilder(); dto.setId(getId().toString()) .setLayer(getLayer().name()) - .setZoneId(getZoneId().toString()) .setRadius(getRadius()) .setVertex(getVertex().toDto()); @@ -352,4 +294,17 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setRadiusCellTemplate(dto).build(); } + + public static RadiusCellTemplate fromDto(RadiusCellTemplateDto dto) { + var id = GUID.valueOf(dto.getId()); + var drawable = new RadiusCellTemplate(id); + drawable.setRadius(dto.getRadius()); + var vertex = dto.getVertex(); + drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + return drawable; + } } diff --git a/src/main/java/net/rptools/maptool/model/drawing/RadiusTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/RadiusTemplate.java index 2455ea25bd..5542cac729 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/RadiusTemplate.java +++ b/src/main/java/net/rptools/maptool/model/drawing/RadiusTemplate.java @@ -17,10 +17,8 @@ import com.google.protobuf.StringValue; import java.awt.*; import java.awt.Rectangle; -import java.awt.geom.AffineTransform; import java.awt.geom.Area; -import java.awt.geom.PathIterator; -import net.rptools.maptool.client.MapTool; +import javax.annotation.Nonnull; import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.ZonePoint; @@ -42,6 +40,15 @@ public RadiusTemplate(GUID id) { super(id); } + public RadiusTemplate(RadiusTemplate other) { + super(other); + } + + @Override + public Drawable copy() { + return new RadiusTemplate(this); + } + /** * Paint the border at a specific radius. * @@ -118,15 +125,8 @@ protected void paintArea( * Drawable Interface Methods *-------------------------------------------------------------------------------------------*/ - /** - * @see net.rptools.maptool.model.drawing.Drawable#getBounds() - */ - public Rectangle getBounds() { - if (getZoneId() == null) { - // This avoids a NPE when loading up a campaign - return new Rectangle(); - } - Zone zone = MapTool.getCampaign().getZone(getZoneId()); + @Override + public Rectangle getBounds(Zone zone) { if (zone == null) { return new Rectangle(); } @@ -137,15 +137,8 @@ public Rectangle getBounds() { vertex.x - quadrantSize, vertex.y - quadrantSize, quadrantSize * 2, quadrantSize * 2); } - public PathIterator getPathIterator() { - return getArea().getPathIterator(new AffineTransform()); - } - - public Area getArea() { - if (getZoneId() == null) { - return new Area(); - } - Zone zone = getCampaign().getZone(getZoneId()); + @Override + public @Nonnull Area getArea(Zone zone) { if (zone == null) { return new Area(); } @@ -175,7 +168,6 @@ public DrawableDto toDto() { var dto = RadiusTemplateDto.newBuilder(); dto.setId(getId().toString()) .setLayer(getLayer().name()) - .setZoneId(getZoneId().toString()) .setRadius(getRadius()) .setVertex(getVertex().toDto()); @@ -183,4 +175,17 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setRadiusTemplate(dto).build(); } + + public static RadiusTemplate fromDto(RadiusTemplateDto dto) { + var id = GUID.valueOf(dto.getId()); + var drawable = new RadiusTemplate(id); + drawable.setRadius(dto.getRadius()); + var vertex = dto.getVertex(); + drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + return drawable; + } } diff --git a/src/main/java/net/rptools/maptool/model/drawing/Rectangle.java b/src/main/java/net/rptools/maptool/model/drawing/Rectangle.java index ade163f6da..b0dbfa771e 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/Rectangle.java +++ b/src/main/java/net/rptools/maptool/model/drawing/Rectangle.java @@ -19,7 +19,9 @@ import java.awt.Point; import java.awt.RenderingHints; import java.awt.geom.Area; +import javax.annotation.Nonnull; import net.rptools.maptool.model.GUID; +import net.rptools.maptool.model.Zone; import net.rptools.maptool.server.Mapper; import net.rptools.maptool.server.proto.drawing.DrawableDto; import net.rptools.maptool.server.proto.drawing.RectangleDrawableDto; @@ -41,8 +43,20 @@ public Rectangle(int startX, int startY, int endX, int endY) { endPoint = new Point(endX, endY); } - public Area getArea() { - return new Area(getBounds()); + public Rectangle(Rectangle other) { + super(other); + this.startPoint = new Point(other.startPoint); + this.endPoint = new Point(other.endPoint); + } + + @Override + public Drawable copy() { + return new Rectangle(this); + } + + @Override + public @Nonnull Area getArea(Zone zone) { + return new Area(getBounds(zone)); } @Override @@ -59,12 +73,21 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setRectangleDrawable(dto).build(); } - /* - * (non-Javadoc) - * - * @see net.rptools.maptool.model.drawing.Drawable#getBounds() - */ - public java.awt.Rectangle getBounds() { + public static Rectangle fromDto(RectangleDrawableDto dto) { + var id = GUID.valueOf(dto.getId()); + var startPoint = dto.getStartPoint(); + var endPoint = dto.getEndPoint(); + var drawable = + new Rectangle(id, startPoint.getX(), startPoint.getY(), endPoint.getX(), endPoint.getY()); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + return drawable; + } + + @Override + public java.awt.Rectangle getBounds(Zone zone) { if (bounds == null) { int x = Math.min(startPoint.x, endPoint.x); int y = Math.min(startPoint.y, endPoint.y); @@ -85,7 +108,7 @@ public Point getEndPoint() { } @Override - protected void draw(Graphics2D g) { + protected void draw(Zone zone, Graphics2D g) { int minX = Math.min(startPoint.x, endPoint.x); int minY = Math.min(startPoint.y, endPoint.y); @@ -99,7 +122,7 @@ protected void draw(Graphics2D g) { } @Override - protected void drawBackground(Graphics2D g) { + protected void drawBackground(Zone zone, Graphics2D g) { int minX = Math.min(startPoint.x, endPoint.x); int minY = Math.min(startPoint.y, endPoint.y); diff --git a/src/main/java/net/rptools/maptool/model/drawing/ShapeDrawable.java b/src/main/java/net/rptools/maptool/model/drawing/ShapeDrawable.java index 10c1e2688d..fede411fa6 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/ShapeDrawable.java +++ b/src/main/java/net/rptools/maptool/model/drawing/ShapeDrawable.java @@ -16,10 +16,14 @@ import com.google.protobuf.StringValue; import java.awt.Graphics2D; +import java.awt.Polygon; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.geom.Area; +import java.awt.geom.RectangularShape; +import javax.annotation.Nonnull; import net.rptools.maptool.model.GUID; +import net.rptools.maptool.model.Zone; import net.rptools.maptool.server.Mapper; import net.rptools.maptool.server.proto.drawing.DrawableDto; import net.rptools.maptool.server.proto.drawing.ShapeDrawableDto; @@ -44,20 +48,39 @@ public ShapeDrawable(Shape shape) { this(shape, true); } + public ShapeDrawable(ShapeDrawable other) { + super(other); + this.useAntiAliasing = other.useAntiAliasing; + this.shape = + switch (other.shape) { + // Covers Rectangle, Ellipse2D, etc. + case RectangularShape r -> (Shape) r.clone(); + case Polygon p -> new Polygon(p.xpoints, p.ypoints, p.npoints); + case Area a -> new Area(a); + default -> other.shape; // Assume anything else cannot be copied but is also okay. + }; + } + + @Override + public Drawable copy() { + return new ShapeDrawable(this); + } + public boolean getUseAntiAliasing() { return useAntiAliasing; } - /* - * (non-Javadoc) - * - * @see net.rptools.maptool.model.drawing.Drawable#getBounds() - */ public java.awt.Rectangle getBounds() { return shape.getBounds(); } - public Area getArea() { + @Override + public java.awt.Rectangle getBounds(Zone zone) { + return getBounds(); + } + + @Override + public @Nonnull Area getArea(Zone zone) { return new Area(shape); } @@ -76,15 +99,26 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setShapeDrawable(dto).build(); } + public static ShapeDrawable fromDto(ShapeDrawableDto dto) { + var shape = Mapper.map(dto.getShape()); + var id = GUID.valueOf(dto.getId()); + var drawable = new ShapeDrawable(id, shape, dto.getUseAntiAliasing()); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + return drawable; + } + @Override - protected void draw(Graphics2D g) { + protected void draw(Zone zone, Graphics2D g) { Object oldAA = applyAA(g); g.draw(shape); restoreAA(g, oldAA); } @Override - protected void drawBackground(Graphics2D g) { + protected void drawBackground(Zone zone, Graphics2D g) { Object oldAA = applyAA(g); g.fill(shape); restoreAA(g, oldAA); diff --git a/src/main/java/net/rptools/maptool/model/drawing/WallTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/WallTemplate.java index dfe3a0c4be..eb0697aaa1 100644 --- a/src/main/java/net/rptools/maptool/model/drawing/WallTemplate.java +++ b/src/main/java/net/rptools/maptool/model/drawing/WallTemplate.java @@ -15,9 +15,11 @@ package net.rptools.maptool.model.drawing; import com.google.protobuf.StringValue; +import java.util.ArrayList; import java.util.List; import net.rptools.maptool.model.CellPoint; import net.rptools.maptool.model.GUID; +import net.rptools.maptool.model.Zone; import net.rptools.maptool.model.ZonePoint; import net.rptools.maptool.server.proto.drawing.DrawableDto; import net.rptools.maptool.server.proto.drawing.WallTemplateDto; @@ -40,6 +42,15 @@ public WallTemplate(GUID id) { setPathVertex(new ZonePoint(0, 0)); } + public WallTemplate(WallTemplate other) { + super(other); + } + + @Override + public Drawable copy() { + return new WallTemplate(this); + } + /** * @see net.rptools.maptool.model.drawing.AbstractTemplate#getRadius() */ @@ -80,7 +91,6 @@ public DrawableDto toDto() { var dto = WallTemplateDto.newBuilder(); dto.setId(getId().toString()) .setLayer(getLayer().name()) - .setZoneId(getZoneId().toString()) .setRadius(getRadius()) .setVertex(getVertex().toDto()) .setMouseSlopeGreater(isMouseSlopeGreater()) @@ -93,4 +103,28 @@ public DrawableDto toDto() { return DrawableDto.newBuilder().setWallTemplate(dto).build(); } + + public static WallTemplate fromDto(WallTemplateDto dto) { + var id = GUID.valueOf(dto.getId()); + var drawable = new WallTemplate(id); + drawable.setRadius(dto.getRadius()); + var vertex = dto.getVertex(); + drawable.setVertex(new ZonePoint(vertex.getX(), vertex.getY())); + drawable.setMouseSlopeGreater(dto.getMouseSlopeGreater()); + var pathVertex = dto.getPathVertex(); + drawable.setPathVertex(new ZonePoint(pathVertex.getX(), pathVertex.getY())); + drawable.setDoubleWide(dto.getDoubleWide()); + if (dto.hasName()) { + drawable.setName(dto.getName().getValue()); + } + drawable.setLayer(Zone.Layer.valueOf(dto.getLayer())); + + var cellpoints = new ArrayList(); + for (var point : dto.getPointsList()) { + cellpoints.add(new CellPoint(point.getX(), point.getY())); + } + drawable.setPath(cellpoints); + + return drawable; + } } diff --git a/src/main/java/net/rptools/maptool/model/library/LibraryManager.java b/src/main/java/net/rptools/maptool/model/library/LibraryManager.java index 3bd991ecdc..f5507f78d3 100644 --- a/src/main/java/net/rptools/maptool/model/library/LibraryManager.java +++ b/src/main/java/net/rptools/maptool/model/library/LibraryManager.java @@ -74,7 +74,7 @@ public class LibraryManager { private static final AddOnSlashCommandManager addOnSlashCommandManager = new AddOnSlashCommandManager(); - static { + public static void init() { libraryTokenManager.init(); builtInLibraryManager.loadBuiltIns(); new MapToolEventBus().getMainEventBus().register(addOnSlashCommandManager); diff --git a/src/main/java/net/rptools/maptool/model/player/DefaultPlayerDatabase.java b/src/main/java/net/rptools/maptool/model/player/DefaultPlayerDatabase.java index fa06326016..82a9f58f5a 100644 --- a/src/main/java/net/rptools/maptool/model/player/DefaultPlayerDatabase.java +++ b/src/main/java/net/rptools/maptool/model/player/DefaultPlayerDatabase.java @@ -29,13 +29,13 @@ * This class provides the implementation for the default player database, where any one can connect * as long as they know the role password. This follows the standard behaviour for 1.9 and earlier. */ -public class DefaultPlayerDatabase implements PlayerDatabase { +public class DefaultPlayerDatabase implements ServerSidePlayerDatabase { private final CipherUtil.Key playerPassword; private final CipherUtil.Key gmPassword; private final LoggedInPlayers loggedInPlayers = new LoggedInPlayers(); - DefaultPlayerDatabase(String playerPassword, String gmPassword) + public DefaultPlayerDatabase(String playerPassword, String gmPassword) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, @@ -109,15 +109,10 @@ public String getBlockedReason(Player player) { } @Override - public Set getOnlinePlayers() throws InterruptedException, InvocationTargetException { + public Set getOnlinePlayers() { return loggedInPlayers.getPlayers(); } - @Override - public boolean recordsOnlyConnectedPlayers() { - return true; - } - @Override public AuthMethod getAuthMethod(Player player) { return AuthMethod.PASSWORD; // Will always be password based diff --git a/src/main/java/net/rptools/maptool/model/player/LocalPlayer.java b/src/main/java/net/rptools/maptool/model/player/LocalPlayer.java index b3190c6dac..db8d24e005 100644 --- a/src/main/java/net/rptools/maptool/model/player/LocalPlayer.java +++ b/src/main/java/net/rptools/maptool/model/player/LocalPlayer.java @@ -17,6 +17,7 @@ import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; +import net.rptools.maptool.client.AppPreferences; import net.rptools.maptool.client.AppState; import net.rptools.maptool.util.cipher.CipherUtil; @@ -26,6 +27,10 @@ public class LocalPlayer extends Player { private final String plainTextPassword; private CipherUtil.Key password; + public LocalPlayer() throws NoSuchAlgorithmException, InvalidKeySpecException { + this(AppPreferences.getDefaultUserName(), Role.GM, ""); + } + public LocalPlayer(String name, Role role, String plainTextPassword) throws NoSuchAlgorithmException, InvalidKeySpecException { super(name, role, null); // Superclass takes care of plainTextPassword info @@ -59,10 +64,6 @@ public CipherUtil.Key getPassword() { return password; } - public String getPlainTextPassword() { - return plainTextPassword; - } - @Override public Player getTransferablePlayer() { return new Player(getName(), getRole(), getPassword()); diff --git a/src/main/java/net/rptools/maptool/model/player/LocalPlayerDatabase.java b/src/main/java/net/rptools/maptool/model/player/LocalPlayerDatabase.java index 3f2479d772..832672b012 100644 --- a/src/main/java/net/rptools/maptool/model/player/LocalPlayerDatabase.java +++ b/src/main/java/net/rptools/maptool/model/player/LocalPlayerDatabase.java @@ -15,30 +15,20 @@ package net.rptools.maptool.model.player; import java.lang.reflect.InvocationTargetException; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; import java.util.HashSet; -import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import net.rptools.lib.MD5Key; -import net.rptools.maptool.model.player.Player.Role; -import net.rptools.maptool.server.ServerConfig; import net.rptools.maptool.util.cipher.CipherUtil; -import net.rptools.maptool.util.cipher.CipherUtil.Key; import net.rptools.maptool.util.cipher.PublicPrivateKeyStore; /** This class provides the implementation for the "database" for the client local player. */ public class LocalPlayerDatabase implements PlayerDatabase { - private LocalPlayer localPlayer; + private final LocalPlayer localPlayer; private final LoggedInPlayers loggedInPlayers = new LoggedInPlayers(); - LocalPlayerDatabase() throws NoSuchAlgorithmException, InvalidKeySpecException { - localPlayer = new LocalPlayer("None", Role.GM, ServerConfig.getPersonalServerGMPassword()); - } - - public synchronized void setLocalPlayer(LocalPlayer player) { + public LocalPlayerDatabase(LocalPlayer player) { localPlayer = player; } @@ -61,48 +51,6 @@ public Player getPlayer(String playerName) { } } - @Override - public Optional getPlayerPassword(String playerName) { - LocalPlayer player = (LocalPlayer) getPlayer(playerName); - if (player != null && player.getName().equals(playerName)) { - return Optional.of(player.getPassword()); - } - return Optional.empty(); - } - - @Override - public byte[] getPlayerPasswordSalt(String playerName) { - LocalPlayer player = (LocalPlayer) getPlayer(playerName); - if (player != null && player.getName().equals(playerName)) { - return player.getPassword().salt(); - } - return new byte[0]; - } - - @Override - public Player getPlayerWithRole(String playerName, Role role) - throws NoSuchAlgorithmException, InvalidKeySpecException { - LocalPlayer player = (LocalPlayer) getPlayer(playerName); - if (player != null && player.getName().equals(playerName)) { - player.setRole(role); - } else { - player = - new LocalPlayer( - playerName, - role, - role == Role.GM - ? ServerConfig.getPersonalServerGMPassword() - : ServerConfig.getPersonalServerPlayerPassword()); - } - setLocalPlayer(player); - return player; - } - - @Override - public Optional getRolePassword(Role role) { - return Optional.empty(); - } - @Override public boolean supportsDisabling() { return false; @@ -129,15 +77,10 @@ public String getBlockedReason(Player player) { } @Override - public Set getOnlinePlayers() throws InterruptedException, InvocationTargetException { + public Set getOnlinePlayers() { return new HashSet<>(loggedInPlayers.getPlayers()); } - @Override - public boolean recordsOnlyConnectedPlayers() { - return true; - } - @Override public AuthMethod getAuthMethod(Player player) { return AuthMethod.PASSWORD; // This will always be password authentication diff --git a/src/main/java/net/rptools/maptool/model/player/PasswordFilePlayerDatabase.java b/src/main/java/net/rptools/maptool/model/player/PasswordFilePlayerDatabase.java index 0bd6ff4a87..fa9e719d28 100644 --- a/src/main/java/net/rptools/maptool/model/player/PasswordFilePlayerDatabase.java +++ b/src/main/java/net/rptools/maptool/model/player/PasswordFilePlayerDatabase.java @@ -66,7 +66,7 @@ import org.apache.logging.log4j.Logger; public final class PasswordFilePlayerDatabase - implements PlayerDatabase, PersistedPlayerDatabase, PlayerDBPropertyChange { + implements ServerSidePlayerDatabase, PersistedPlayerDatabase { private static final Logger log = LogManager.getLogger(PasswordFilePlayerDatabase.class); private static final String PUBLIC_KEY_DIR = "keys"; @@ -89,11 +89,6 @@ public final class PasswordFilePlayerDatabase private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this); - public PasswordFilePlayerDatabase(File passwordFile) - throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { - this(passwordFile, null); - } - PasswordFilePlayerDatabase(File passwordFile, File additionalUsers) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { Objects.requireNonNull(passwordFile); @@ -725,12 +720,29 @@ public boolean isPlayerRegistered(String name) @Override public void playerSignedIn(Player player) { + var alreadyExists = playerExists(player.getName()); loggedInPlayers.playerSignedIn(player); + + if (!alreadyExists) { + propertyChangeSupport.firePropertyChange( + PlayerDBPropertyChange.PROPERTY_CHANGE_PLAYER_ADDED, null, player.getName()); + } else { + propertyChangeSupport.firePropertyChange( + PlayerDBPropertyChange.PROPERTY_CHANGE_PLAYER_CHANGED, null, player.getName()); + } } @Override public void playerSignedOut(Player player) { loggedInPlayers.playerSignedOut(player); + + if (playerExists(player.getName())) { + propertyChangeSupport.firePropertyChange( + PlayerDBPropertyChange.PROPERTY_CHANGE_PLAYER_CHANGED, null, player.getName()); + } else { + propertyChangeSupport.firePropertyChange( + PlayerDBPropertyChange.PROPERTY_CHANGE_PLAYER_REMOVED, player, null); + } } @Override @@ -739,7 +751,7 @@ public boolean isPlayerConnected(String name) { } @Override - public Set getAllPlayers() throws InterruptedException, InvocationTargetException { + public Set getAllPlayers() { Set players = new HashSet<>(getOnlinePlayers()); players.addAll( @@ -751,15 +763,10 @@ public Set getAllPlayers() throws InterruptedException, InvocationTarget } @Override - public Set getOnlinePlayers() throws InterruptedException, InvocationTargetException { + public Set getOnlinePlayers() { return new HashSet<>(loggedInPlayers.getPlayers()); } - @Override - public boolean recordsOnlyConnectedPlayers() { - return false; - } - /** * Adds a player to the database with the specified password. The {@link #commitChanges} method * must be called to commit these changes to persistent storage. diff --git a/src/main/java/net/rptools/maptool/model/player/PersistedPlayerDatabase.java b/src/main/java/net/rptools/maptool/model/player/PersistedPlayerDatabase.java index ce423dd658..f27aef1a9d 100644 --- a/src/main/java/net/rptools/maptool/model/player/PersistedPlayerDatabase.java +++ b/src/main/java/net/rptools/maptool/model/player/PersistedPlayerDatabase.java @@ -23,7 +23,7 @@ import net.rptools.maptool.model.player.Player.Role; /** Interface implemented by player databases that persist their information between runs. */ -public interface PersistedPlayerDatabase { +public interface PersistedPlayerDatabase extends PlayerDatabase, PlayerDBPropertyChange { /** * Disables the specified player. This will not boot the player from the server. diff --git a/src/main/java/net/rptools/maptool/model/player/PersonalServerPlayerDatabase.java b/src/main/java/net/rptools/maptool/model/player/PersonalServerPlayerDatabase.java index 68de2e3473..cc5ad33261 100644 --- a/src/main/java/net/rptools/maptool/model/player/PersonalServerPlayerDatabase.java +++ b/src/main/java/net/rptools/maptool/model/player/PersonalServerPlayerDatabase.java @@ -14,31 +14,22 @@ */ package net.rptools.maptool.model.player; -import java.lang.reflect.InvocationTargetException; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import net.rptools.lib.MD5Key; -import net.rptools.maptool.client.AppPreferences; import net.rptools.maptool.model.player.Player.Role; -import net.rptools.maptool.server.ServerConfig; import net.rptools.maptool.util.cipher.CipherUtil; -public class PersonalServerPlayerDatabase implements PlayerDatabase { +public class PersonalServerPlayerDatabase implements ServerSidePlayerDatabase { private final LocalPlayer player; private final LoggedInPlayers loggedInPlayers = new LoggedInPlayers(); - public PersonalServerPlayerDatabase() throws NoSuchAlgorithmException, InvalidKeySpecException { - player = - new LocalPlayer( - AppPreferences.getDefaultUserName(), - Role.GM, - ServerConfig.getPersonalServerGMPassword()); + public PersonalServerPlayerDatabase(LocalPlayer player) { + this.player = player; } @Override @@ -46,9 +37,12 @@ public boolean playerExists(String playerName) { return true; // Player always exists no matter what the name } + public LocalPlayer getPlayer() { + return player; + } + @Override - public Player getPlayer(String playerName) - throws NoSuchAlgorithmException, InvalidKeySpecException { + public Player getPlayer(String playerName) { return player; } @@ -99,8 +93,7 @@ public CompletableFuture hasPublicKey(Player player, MD5Key md5key) { } @Override - public boolean isPlayerRegistered(String name) - throws InterruptedException, InvocationTargetException { + public boolean isPlayerRegistered(String name) { return player != null && player.getName() != null && player.getName().equals(name); } @@ -125,15 +118,10 @@ public String getBlockedReason(Player player) { } @Override - public Set getOnlinePlayers() throws InterruptedException, InvocationTargetException { + public Set getOnlinePlayers() { return new HashSet<>(loggedInPlayers.getPlayers()); } - @Override - public boolean recordsOnlyConnectedPlayers() { - return true; - } - @Override public boolean isBlocked(Player player) { return false; @@ -149,8 +137,7 @@ public Optional getRolePassword(Player.Role role) { } @Override - public Player getPlayerWithRole(String playerName, Player.Role role) - throws NoSuchAlgorithmException, InvalidKeySpecException { + public Player getPlayerWithRole(String playerName, Player.Role role) { return player; // There is no non GM personal server player so just return the GM } } diff --git a/src/main/java/net/rptools/maptool/model/player/PlayerDBPropertyChange.java b/src/main/java/net/rptools/maptool/model/player/PlayerDBPropertyChange.java index 7e36327281..f58063ff0e 100644 --- a/src/main/java/net/rptools/maptool/model/player/PlayerDBPropertyChange.java +++ b/src/main/java/net/rptools/maptool/model/player/PlayerDBPropertyChange.java @@ -16,7 +16,7 @@ import java.beans.PropertyChangeListener; -interface PlayerDBPropertyChange { +public interface PlayerDBPropertyChange { /** * Property change event name for when a player is added. Some databases may not support this diff --git a/src/main/java/net/rptools/maptool/model/player/PlayerDatabase.java b/src/main/java/net/rptools/maptool/model/player/PlayerDatabase.java index 95ccdd78fc..9ff9771b8f 100644 --- a/src/main/java/net/rptools/maptool/model/player/PlayerDatabase.java +++ b/src/main/java/net/rptools/maptool/model/player/PlayerDatabase.java @@ -17,7 +17,6 @@ import java.lang.reflect.InvocationTargetException; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; -import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -55,42 +54,6 @@ enum AuthMethod { */ Player getPlayer(String playerName) throws NoSuchAlgorithmException, InvalidKeySpecException; - /** - * Returns the {@link CipherUtil.Key} for the player. If the database only supports role based - * passwords the returned value will be empty. - * - * @param playerName The name of the player to check. - * @return the {@link CipherUtil.Key} to use. - */ - Optional getPlayerPassword(String playerName); - - /** - * Returns the salt used for the player's password. - * - * @param playerName the name of the player to get the password salt for. - * @return the salt used for the password. - */ - byte[] getPlayerPasswordSalt(String playerName); - - /** - * Returns the player overriding the role in the database with the specified role. - * - * @param playerName The name of the player to retrieve. - * @param role The role for the player. - * @return The player. - */ - Player getPlayerWithRole(String playerName, Player.Role role) - throws NoSuchAlgorithmException, InvalidKeySpecException; - - /** - * Returns the password required for the role. If role authentication is not supported this will - * be empty. - * - * @param role The role to retrieve the password for. - * @return The password for the role. - */ - Optional getRolePassword(Player.Role role); - /** * Returns if this player database supports disabling players. * @@ -135,7 +98,7 @@ Player getPlayerWithRole(String playerName, Player.Role role) * * @return The players that are known to the database. */ - default Set getAllPlayers() throws InterruptedException, InvocationTargetException { + default Set getAllPlayers() { return getOnlinePlayers(); } @@ -144,14 +107,7 @@ default Set getAllPlayers() throws InterruptedException, InvocationTarge * * @return The players that are currently connected. */ - Set getOnlinePlayers() throws InterruptedException, InvocationTargetException; - - /** - * Returns if this player database records information about only currently connected players. - * - * @return if this player database records information about only currently connected players. - */ - boolean recordsOnlyConnectedPlayers(); + Set getOnlinePlayers(); /** * Returns the authentication method for the player. diff --git a/src/main/java/net/rptools/maptool/model/player/PlayerDatabaseFactory.java b/src/main/java/net/rptools/maptool/model/player/PlayerDatabaseFactory.java index e078447edb..0e7f1ad621 100644 --- a/src/main/java/net/rptools/maptool/model/player/PlayerDatabaseFactory.java +++ b/src/main/java/net/rptools/maptool/model/player/PlayerDatabaseFactory.java @@ -14,162 +14,67 @@ */ package net.rptools.maptool.model.player; -import java.beans.PropertyChangeListener; -import java.beans.PropertyChangeSupport; import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; import javax.crypto.NoSuchPaddingException; import net.rptools.maptool.client.AppUtil; -import net.rptools.maptool.server.ServerConfig; public class PlayerDatabaseFactory { - - public enum PlayerDatabaseType { - PERSONAL_SERVER, + private enum PlayerDatabaseType { LOCAL_PLAYER, DEFAULT, PASSWORD_FILE } - interface DatabaseChangeTypeSupport { - /** - * Adds a property change listener for player database events. Only one type is valid {@link - * PlayerDBPropertyChange#PROPERTY_CHANGE_DATABASE_CHANGED}. - * - * @param listener The property change listener to add. - */ - void addPropertyChangeListener(PropertyChangeListener listener); - - /** - * Removes a property change listener for player database events. Only one type is valid {@link - * PlayerDBPropertyChange#PROPERTY_CHANGE_DATABASE_CHANGED}. - * - * @param listener The property change listener to remove. - */ - void removePropertyChangeListener(PropertyChangeListener listener); - - /** - * Notifies that the database hase changed. - * - * @param oldDb the old database. - * @param newDb the new database. - */ - void databaseChanged(PlayerDatabase oldDb, PlayerDatabase newDb); - } - - private static PlayerDatabase currentPlayerDatabase; - private static final Map playerDatabaseMap = new ConcurrentHashMap<>(); - private static final ReentrantLock lock = new ReentrantLock(); - - private static final DatabaseChangeTypeSupport databaseChangeTypeSupport = - new DatabaseChangeTypeSupport() { - - private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this); - - @Override - public void addPropertyChangeListener(PropertyChangeListener listener) { - propertyChangeSupport.addPropertyChangeListener(listener); - } - - @Override - public void removePropertyChangeListener(PropertyChangeListener listener) { - propertyChangeSupport.removePropertyChangeListener(listener); - } - - @Override - public void databaseChanged(PlayerDatabase oldDb, PlayerDatabase newDb) { - propertyChangeSupport.firePropertyChange( - PlayerDBPropertyChange.PROPERTY_CHANGE_DATABASE_CHANGED, oldDb, newDb); - } - }; - - private static final File PASSWORD_FILE = - AppUtil.getAppHome("config").toPath().resolve("passwords.json").toFile(); - private static final File PASSWORD_ADDITION_FILE = - AppUtil.getAppHome("config").toPath().resolve("passwords_add.json").toFile(); - private static ServerConfig serverConfig; - - public static void setServerConfig(ServerConfig config) { - try { - lock.lock(); - serverConfig = config; - } finally { - lock.unlock(); - } - } - - public static PlayerDatabase getCurrentPlayerDatabase() { - try { - lock.lock(); - return currentPlayerDatabase; - } finally { - lock.unlock(); - } + public static LocalPlayerDatabase getLocalPlayerDatabase(LocalPlayer player) { + return new LocalPlayerDatabase(player); } - public static void setCurrentPlayerDatabase(PlayerDatabaseType playerDatabaseType) { - try { - lock.lock(); - var oldPlayerDatabase = getCurrentPlayerDatabase(); - currentPlayerDatabase = getPlayerDatabase(playerDatabaseType); - databaseChangeTypeSupport.databaseChanged(oldPlayerDatabase, currentPlayerDatabase); - } finally { - lock.unlock(); - } + public static PersonalServerPlayerDatabase getPersonalServerPlayerDatabase(LocalPlayer player) { + return new PersonalServerPlayerDatabase(player); } - private static ServerConfig getServerConfig() { + public static DefaultPlayerDatabase getDefaultPlayerDatabase( + String playerPassword, String gmPassword) { try { - lock.lock(); - return serverConfig; - } finally { - lock.unlock(); - } - } - - public static PlayerDatabase getPlayerDatabase(PlayerDatabaseType databaseType) { - switch (databaseType) { - case LOCAL_PLAYER: - case PASSWORD_FILE: - return playerDatabaseMap.computeIfAbsent( - databaseType, PlayerDatabaseFactory::createPlayerDatabase); - default: - return createPlayerDatabase(databaseType); - } - } - - static DatabaseChangeTypeSupport getDatabaseChangeTypeSupport() { - return databaseChangeTypeSupport; - } - - private static PlayerDatabase createPlayerDatabase(PlayerDatabaseType databaseType) { - try { - switch (databaseType) { - case LOCAL_PLAYER: - return new LocalPlayerDatabase(); - case PASSWORD_FILE: - return new PasswordFilePlayerDatabase(PASSWORD_FILE, PASSWORD_ADDITION_FILE); - case PERSONAL_SERVER: - return new PersonalServerPlayerDatabase(); - default: - ServerConfig config = getServerConfig(); - return new DefaultPlayerDatabase(config.getPlayerPassword(), config.getGmPassword()); - } - } catch (IOException - | NoSuchAlgorithmException + return new DefaultPlayerDatabase(playerPassword, gmPassword); + } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | InvalidKeyException e) { throw new IllegalStateException(e); } } + + public static PasswordFilePlayerDatabase getPasswordFilePlayerDatabase() { + return (PasswordFilePlayerDatabase) + getStashed( + PlayerDatabaseType.PASSWORD_FILE, + () -> { + try { + Path configRoot = AppUtil.getAppHome("config").toPath(); + File passwordFile = configRoot.resolve("passwords.json").toFile(); + File additionalFile = configRoot.resolve("passwords_add.json").toFile(); + + return new PasswordFilePlayerDatabase(passwordFile, additionalFile); + } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new IllegalStateException(e); + } + }); + } + + private static PlayerDatabase getStashed( + PlayerDatabaseType databaseType, Supplier supplier) { + return playerDatabaseMap.computeIfAbsent(databaseType, type -> supplier.get()); + } } diff --git a/src/main/java/net/rptools/maptool/model/player/PlayerDatabaseInfo.java b/src/main/java/net/rptools/maptool/model/player/PlayerDatabaseInfo.java deleted file mode 100644 index dde4b62631..0000000000 --- a/src/main/java/net/rptools/maptool/model/player/PlayerDatabaseInfo.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.model.player; - -public record PlayerDatabaseInfo( - boolean supportsBlocking, - boolean supportsIndividualPasswords, - boolean supportsAsymmetricalKeys, - boolean recordsOnlyConnectedPlayers) {} diff --git a/src/main/java/net/rptools/maptool/model/player/Players.java b/src/main/java/net/rptools/maptool/model/player/Players.java index 7bb63434ec..8ac034b8ca 100644 --- a/src/main/java/net/rptools/maptool/model/player/Players.java +++ b/src/main/java/net/rptools/maptool/model/player/Players.java @@ -14,9 +14,6 @@ */ package net.rptools.maptool.model.player; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.beans.PropertyChangeSupport; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -49,71 +46,13 @@ public enum ChangePlayerStatus { NOT_SUPPORTED } - /** - * Property change event name for when a player is added. Some databases may not support this - * event, so you will also need to listen to {@link #PROPERTY_CHANGE_DATABASE_CHANGED} for changes - * to players in the database. - */ - public static final String PROPERTY_CHANGE_PLAYER_ADDED = - PlayerDBPropertyChange.PROPERTY_CHANGE_PLAYER_ADDED; - - /** - * Property change event name for when a player is removed. Some databases may not support this - * event, so you will also need to listen to {@link #PROPERTY_CHANGE_DATABASE_CHANGED} for changes - * to players in the database. - */ - public static final String PROPERTY_CHANGE_PLAYER_REMOVED = - PlayerDBPropertyChange.PROPERTY_CHANGE_PLAYER_REMOVED; - - /** Property change event name for when a player is changed. */ - public static final String PROPERTY_CHANGE_PLAYER_CHANGED = - PlayerDBPropertyChange.PROPERTY_CHANGE_PLAYER_CHANGED; - - /** - * Property change event name for when the database is changed or there are mas updates. Some - * databases may only support this event and not player added/removed/changed - */ - public static final String PROPERTY_CHANGE_DATABASE_CHANGED = - PlayerDBPropertyChange.PROPERTY_CHANGE_DATABASE_CHANGED; - /** Instance for logging messages. */ private static final Logger log = LogManager.getLogger(Players.class); - /** instance variable for property change support. */ - private static final PropertyChangeSupport propertyChangeSupport = - new PropertyChangeSupport(Players.class); + private final PlayerDatabase playerDatabase; - private static final PropertyChangeListener databaseChangeListener = - new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - propertyChangeSupport.firePropertyChange(evt); - } - }; - - private static final PropertyChangeListener databaseTypeChangeListener = - new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - PlayerDatabase oldDb = (PlayerDatabase) evt.getOldValue(); - PlayerDatabase newDb = (PlayerDatabase) evt.getNewValue(); - - if (oldDb instanceof PlayerDBPropertyChange playerdb) { - playerdb.removePropertyChangeListener(databaseChangeListener); - } - - if (newDb instanceof PlayerDBPropertyChange playerdb) { - playerdb.addPropertyChangeListener(databaseChangeListener); - } - } - }; - - static { - PlayerDatabaseFactory.getDatabaseChangeTypeSupport() - .addPropertyChangeListener(databaseTypeChangeListener); - if (PlayerDatabaseFactory.getCurrentPlayerDatabase() instanceof PlayerDBPropertyChange pdb) { - pdb.addPropertyChangeListener(databaseChangeListener); - } + public Players(PlayerDatabase playerDatabase) { + this.playerDatabase = playerDatabase; } /** @@ -160,29 +99,6 @@ public CompletableFuture> getDatabasePlayers() { return CompletableFuture.supplyAsync(this::getPlayersInfo); } - /** - * Returns the information about the current player database capabilities. - * - * @return the information about the current player database capabilities. - */ - public CompletableFuture getDatabaseCapabilities() { - return CompletableFuture.supplyAsync(this::getPlayerDatabaseInfo); - } - - /** - * Returns the information about the current player databases capabilities. - * - * @return the information about the current player databases capabilities. - */ - private PlayerDatabaseInfo getPlayerDatabaseInfo() { - PlayerDatabase playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); - return new PlayerDatabaseInfo( - playerDatabase.supportsDisabling(), - !playerDatabase.supportsRolePasswords(), - playerDatabase.supportsAsymmetricalKeys(), - playerDatabase.recordsOnlyConnectedPlayers()); - } - /** * Returns information about the specified player. * @@ -191,7 +107,6 @@ private PlayerDatabaseInfo getPlayerDatabaseInfo() { */ private PlayerInfo getPlayerInfo(String name) { try { - var playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); if (!playerDatabase.playerExists(name)) { return null; } @@ -216,8 +131,8 @@ private PlayerInfo getPlayerInfo(String name) { blocked = true; } } - boolean connected = PlayerDatabaseFactory.getCurrentPlayerDatabase().isPlayerConnected(name); + boolean connected = playerDatabase.isPlayerConnected(name); AuthMethod authMethod = playerDatabase.getAuthMethod(player); boolean persisted = false; if (playerDatabase instanceof PersistedPlayerDatabase persistedPlayerDatabase) { @@ -244,7 +159,6 @@ private PlayerInfo getPlayerInfo(String name) { */ private Set getPlayersInfo() { Set players = new HashSet<>(); - var playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); try { for (Player p : playerDatabase.getAllPlayers()) { players.add(getPlayerInfo(p.getName())); @@ -260,22 +174,13 @@ private Set getPlayersInfo() { return players.stream().filter(Objects::nonNull).collect(Collectors.toSet()); } - /** - * Returns if the current player database only records players that are connected. - * - * @return {@code true} if the player database only records players while they are connected. - */ - public boolean recordsOnlyConnectedPlayers() { - return PlayerDatabaseFactory.getCurrentPlayerDatabase().recordsOnlyConnectedPlayers(); - } - /** * Returns if this current player database supports per player passwords. * * @return {@code true} if the player database supports per player passwords. */ - public boolean supportsPerPlayerPasswords() { - return !PlayerDatabaseFactory.getCurrentPlayerDatabase().supportsRolePasswords(); + private boolean supportsPerPlayerPasswords() { + return !playerDatabase.supportsRolePasswords(); } /** @@ -283,8 +188,8 @@ public boolean supportsPerPlayerPasswords() { * * @return {@code true} if the player database supports asymmetric keys. */ - public boolean supportsAsymmetricKeys() { - return PlayerDatabaseFactory.getCurrentPlayerDatabase().supportsAsymmetricalKeys(); + private boolean supportsAsymmetricKeys() { + return playerDatabase.supportsAsymmetricalKeys(); } /** @@ -301,7 +206,6 @@ public ChangePlayerStatus addPlayerWithPassword(String name, Role role, String p return ChangePlayerStatus.NOT_SUPPORTED; } - var playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); if (playerDatabase instanceof PersistedPlayerDatabase playerDb) { try { playerDb.addPlayerSharedPassword(name, role, password); @@ -336,7 +240,6 @@ public ChangePlayerStatus addPlayerWithPublicKey(String name, Role role, String } var pkeys = new HashSet<>(Arrays.asList(CipherUtil.splitPublicKeys(publicKeyString))); - var playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); if (playerDatabase instanceof PersistedPlayerDatabase playerDb) { try { playerDb.addPlayerAsymmetricKey(name, role, pkeys); @@ -371,7 +274,6 @@ public ChangePlayerStatus setPublicKeys(String name, String publicKeyString) { } var pkeys = new HashSet<>(Arrays.asList(CipherUtil.splitPublicKeys(publicKeyString))); - var playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); if (playerDatabase instanceof PersistedPlayerDatabase playerDb) { try { playerDb.setAsymmetricKeys(name, pkeys); @@ -399,7 +301,6 @@ public ChangePlayerStatus setPublicKeys(String name, String publicKeyString) { * @return {@link ChangePlayerStatus#OK} if successful, otherwise the reason for the failure. */ public ChangePlayerStatus setPassword(String name, String password) { - var playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); if (playerDatabase instanceof PersistedPlayerDatabase playerDb) { try { playerDb.setSharedPassword(name, password); @@ -428,7 +329,6 @@ public ChangePlayerStatus setPassword(String name, String password) { * @return {@link ChangePlayerStatus#OK} if successful, otherwise the reason for the failure. */ public ChangePlayerStatus setRole(String name, Role role) { - var playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); if (playerDatabase instanceof PersistedPlayerDatabase playerDb) { playerDb.setRole(name, role); return ChangePlayerStatus.OK; @@ -447,7 +347,6 @@ public ChangePlayerStatus setRole(String name, Role role) { * @return {@link ChangePlayerStatus#OK} if successful, otherwise the reason for the failure. */ public ChangePlayerStatus blockPlayer(String name, String reason) { - var playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); if (playerDatabase instanceof PersistedPlayerDatabase playerDb) { playerDb.blockPlayer(name, reason); return ChangePlayerStatus.OK; @@ -464,7 +363,6 @@ public ChangePlayerStatus blockPlayer(String name, String reason) { * @return {@link ChangePlayerStatus#OK} if successful, otherwise the reason for the failure. */ public ChangePlayerStatus unblockPlayer(String name) { - var playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); if (playerDatabase instanceof PersistedPlayerDatabase playerDb) { playerDb.unblockPlayer(name); return ChangePlayerStatus.OK; @@ -473,119 +371,4 @@ public ChangePlayerStatus unblockPlayer(String name) { return ChangePlayerStatus.NOT_SUPPORTED; } } - - /** - * Removes the specified player from the database. - * - * @param name the name of the player to remove. - * @return {@link ChangePlayerStatus#OK} if successful, otherwise the reason for the failure. - */ - public ChangePlayerStatus removePlayer(String name) { - var playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); - if (playerDatabase instanceof PersistedPlayerDatabase playerDb) { - playerDb.deletePlayer(name); - return ChangePlayerStatus.OK; - } else { - log.error(I18N.getText("msg.error.playerDB.cantUpdatePlayer", name)); - return ChangePlayerStatus.NOT_SUPPORTED; - } - } - - /** - * Adds a property change listener for player database events. - * - * @param listener The property change listener to add. - */ - public static void addPropertyChangeListener(PropertyChangeListener listener) { - propertyChangeSupport.addPropertyChangeListener(listener); - } - - /** - * Removes a property change listener for player database events. - * - * @param listener The property change listener to remove. - */ - public static void removePropertyChangeListener(PropertyChangeListener listener) { - propertyChangeSupport.removePropertyChangeListener(listener); - } - - /** - * Commits the pending changes writing them out to the persistent storage. - * - * @throws NoSuchPaddingException if there is an error hashing the password. - * @throws NoSuchAlgorithmException if there is an error hashing the password. - * @throws InvalidKeySpecException if there is an error hashing the password. - * @throws PasswordDatabaseException if there is an error adding the player to the file. - * @throws InvalidKeyException if there is an error hashing the password. - */ - public void commitChanges() - throws NoSuchPaddingException, - NoSuchAlgorithmException, - InvalidKeySpecException, - PasswordDatabaseException, - InvalidKeyException { - if (PlayerDatabaseFactory.getCurrentPlayerDatabase() instanceof PersistedPlayerDatabase db) { - db.commitChanges(); - } - } - - /** - * Rolls back any pending changes that haven't been written to the file. - * - * @throws NoSuchPaddingException if there is an error hashing the password. - * @throws NoSuchAlgorithmException if there is an error hashing the password. - * @throws InvalidKeySpecException if there is an error hashing the password. - * @throws PasswordDatabaseException if there is an error adding the player to the file. - * @throws InvalidKeyException i - */ - public void rollbackChanges() - throws NoSuchPaddingException, - NoSuchAlgorithmException, - InvalidKeySpecException, - PasswordDatabaseException, - InvalidKeyException { - if (PlayerDatabaseFactory.getCurrentPlayerDatabase() instanceof PersistedPlayerDatabase db) { - db.rollbackChanges(); - } - } - - /** - * Notify that a player has signed in. - * - * @param player the player that has signed in. - */ - public void playerSignedIn(Player player) { - PlayerDatabase playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); - var oldInfo = getPlayerInfo(player.getName()); - playerDatabase.playerSignedIn(player); - var newInfo = getPlayerInfo(player.getName()); - if (newInfo != null) { - if (oldInfo != null) { - propertyChangeSupport.firePropertyChange( - PlayerDBPropertyChange.PROPERTY_CHANGE_PLAYER_CHANGED, oldInfo, newInfo); - } else { - propertyChangeSupport.firePropertyChange(PROPERTY_CHANGE_PLAYER_ADDED, null, newInfo); - } - } - } - - /** - * Notify that a player has signed out. - * - * @param player the player that has signed out. - */ - public void playerSignedOut(Player player) { - PlayerDatabase playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); - var oldInfo = getPlayerInfo(player.getName()); - playerDatabase.playerSignedOut(player); - var newInfo = getPlayerInfo(player.getName()); - if (oldInfo != null) { - if (newInfo != null) { - propertyChangeSupport.firePropertyChange( - PlayerDBPropertyChange.PROPERTY_CHANGE_PLAYER_CHANGED, oldInfo, newInfo); - } else { - propertyChangeSupport.firePropertyChange(PROPERTY_CHANGE_PLAYER_REMOVED, oldInfo, null); - } - } - } } diff --git a/src/main/java/net/rptools/maptool/model/player/ServerSidePlayerDatabase.java b/src/main/java/net/rptools/maptool/model/player/ServerSidePlayerDatabase.java new file mode 100644 index 0000000000..a597be6fd6 --- /dev/null +++ b/src/main/java/net/rptools/maptool/model/player/ServerSidePlayerDatabase.java @@ -0,0 +1,62 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code 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. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.model.player; + +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Optional; +import net.rptools.maptool.util.cipher.CipherUtil; + +/** + * A {@link net.rptools.maptool.model.player.PlayerDatabase} augmented with functinoality only + * needed by servers (password validation, etc). + */ +public interface ServerSidePlayerDatabase extends PlayerDatabase { + /** + * Returns the {@link net.rptools.maptool.util.cipher.CipherUtil.Key} for the player. If the + * database only supports role based passwords the returned value will be empty. + * + * @param playerName The name of the player to check. + * @return the {@link net.rptools.maptool.util.cipher.CipherUtil.Key} to use. + */ + Optional getPlayerPassword(String playerName); + + /** + * Returns the salt used for the player's password. + * + * @param playerName the name of the player to get the password salt for. + * @return the salt used for the password. + */ + byte[] getPlayerPasswordSalt(String playerName); + + /** + * Returns the player overriding the role in the database with the specified role. + * + * @param playerName The name of the player to retrieve. + * @param role The role for the player. + * @return The player. + */ + Player getPlayerWithRole(String playerName, Player.Role role) + throws NoSuchAlgorithmException, InvalidKeySpecException; + + /** + * Returns the password required for the role. If role authentication is not supported this will + * be empty. + * + * @param role The role to retrieve the password for. + * @return The password for the role. + */ + Optional getRolePassword(Player.Role role); +} diff --git a/src/main/java/net/rptools/maptool/model/sheet/stats/StatSheet.java b/src/main/java/net/rptools/maptool/model/sheet/stats/StatSheet.java index 733a6eb6e6..6f043dc0e7 100644 --- a/src/main/java/net/rptools/maptool/model/sheet/stats/StatSheet.java +++ b/src/main/java/net/rptools/maptool/model/sheet/stats/StatSheet.java @@ -15,6 +15,7 @@ package net.rptools.maptool.model.sheet.stats; import java.net.URL; +import java.util.Objects; import java.util.Set; /** @@ -28,4 +29,36 @@ * @param namespace The namespace of the add-on that provides the spreadsheet. */ public record StatSheet( - String name, String description, URL entry, Set propertyTypes, String namespace) {} + String name, String description, URL entry, Set propertyTypes, String namespace) { + @Override + public boolean equals(Object obj) { + if (!(obj instanceof StatSheet other)) { + return false; + } + if (!Objects.equals(name, other.name)) { + return false; + } + if (!Objects.equals(description, other.description)) { + return false; + } + if (!Objects.equals(propertyTypes, other.propertyTypes)) { + return false; + } + if (!Objects.equals(namespace, other.namespace)) { + return false; + } + + // Finally the URLs + var thisUrl = entry == null ? null : entry.toString(); + var otherUrl = other.entry == null ? null : other.entry.toString(); + + return Objects.equals(thisUrl, otherUrl); + } + + @Override + public int hashCode() { + // Hash the URL as a string. E.g., we don't need hostname resolution just for a hashcode. + return Objects.hash( + name, description, entry == null ? null : entry.toString(), propertyTypes, namespace); + } +} diff --git a/src/main/java/net/rptools/maptool/server/ClientHandshake.java b/src/main/java/net/rptools/maptool/server/ClientHandshake.java index 984e33e9db..09d5e58087 100644 --- a/src/main/java/net/rptools/maptool/server/ClientHandshake.java +++ b/src/main/java/net/rptools/maptool/server/ClientHandshake.java @@ -24,9 +24,11 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; +import java.util.function.BiConsumer; import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; @@ -35,6 +37,7 @@ import net.rptools.clientserver.simple.connection.Connection; import net.rptools.lib.MD5Key; import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.MapToolClient; import net.rptools.maptool.language.I18N; import net.rptools.maptool.model.Asset; import net.rptools.maptool.model.AssetManager; @@ -43,12 +46,7 @@ import net.rptools.maptool.model.gamedata.GameDataImporter; import net.rptools.maptool.model.library.LibraryManager; import net.rptools.maptool.model.library.addon.AddOnLibraryImporter; -import net.rptools.maptool.model.player.LocalPlayer; -import net.rptools.maptool.model.player.LocalPlayerDatabase; -import net.rptools.maptool.model.player.Player; import net.rptools.maptool.model.player.Player.Role; -import net.rptools.maptool.model.player.PlayerDatabaseFactory; -import net.rptools.maptool.model.player.PlayerDatabaseFactory.PlayerDatabaseType; import net.rptools.maptool.server.proto.*; import net.rptools.maptool.server.proto.HandshakeMsg.MessageTypeCase; import net.rptools.maptool.util.cipher.CipherUtil; @@ -58,29 +56,19 @@ import org.apache.logging.log4j.Logger; /** Class that implements the client side of the handshake. */ -public class ClientHandshake implements Handshake, MessageHandler { +public class ClientHandshake implements Handshake, MessageHandler { /** Instance used for log messages. */ private static final Logger log = LogManager.getLogger(ClientHandshake.class); - /** The index in the array for the GM handshake challenge, only used for role based auth */ - private static final int GM_CHALLENGE = 0; + private final CompletableFuture future = new CompletableFuture<>(); + private CompletionStage stage = future; - /** The index in the array for the Player handshake challenge, only used for role based auth */ - private static final int PLAYER_CHALLENGE = 1; + private final MapToolClient client; /** The connection for the handshake. */ private final Connection connection; - /** The player for the client. */ - private final LocalPlayer player; - - /** Observers that want to be notified when the status changes. */ - private final List observerList = new CopyOnWriteArrayList<>(); - - /** Message for any error that has occurred, {@code null} if no error has occurred. */ - private String errorMessage; - /** PIN for sending public key to client */ private String pin; @@ -88,18 +76,36 @@ public class ClientHandshake implements Handshake, MessageHandler { private WindowListener easyConnectWindowListener; - /** - * Any exception that occurred that causes an error, {@code null} if no exception which causes an - * error has occurred. - */ - private Exception exception; - /** The current state of the handshake process. */ private State currentState = State.AwaitingUseAuthType; - public ClientHandshake(Connection connection, LocalPlayer player) { + public ClientHandshake(MapToolClient client, Connection connection) { + this.client = client; this.connection = connection; - this.player = player; + + whenComplete( + (result, error) -> { + connection.removeMessageHandler(this); + SwingUtilities.invokeLater(this::closeEasyConnectDialog); + }); + } + + @Override + public void whenComplete(BiConsumer callback) { + stage = + stage.whenComplete( + (result, error) -> { + // Hand back the original exception, not the wrapped one. + if (error instanceof CompletionException e) { + error = e.getCause(); + } + callback.accept(result, error); + }); + } + + private void setCurrentState(State state) { + log.debug("Transitioning from {} to {}", currentState, state); + currentState = state; } private synchronized JDialog getEasyConnectDialog() { @@ -119,22 +125,37 @@ private synchronized void setEasyConnectWindowListener(WindowListener easyConnec } @Override - public void startHandshake() throws ExecutionException, InterruptedException { - var md5key = CipherUtil.publicKeyMD5(new PublicPrivateKeyStore().getKeys().get().publicKey()); + public void startHandshake() { + connection.addMessageHandler(this); + startHandshakeInternal(); + } + + private void startHandshakeInternal() { + MD5Key md5key; + try { + md5key = CipherUtil.publicKeyMD5(new PublicPrivateKeyStore().getKeys().get().publicKey()); + } catch (ExecutionException | InterruptedException e) { + // Report the error the same way as any other handshake error. + var errorMessage = I18N.getText("Handshake.msg.failedToGetPublicKey"); + setCurrentState(State.Error); + future.completeExceptionally(new Failure(errorMessage, e)); + return; + } + var clientInitMsg = ClientInitMsg.newBuilder() - .setPlayerName(player.getName()) + .setPlayerName(client.getPlayer().getName()) .setVersion(MapTool.getVersion()) .setPublicKeyMd5(md5key.toString()); var handshakeMsg = HandshakeMsg.newBuilder().setClientInitMsg(clientInitMsg).build(); - - sendMessage(handshakeMsg); - currentState = State.AwaitingUseAuthType; + sendMessage(State.AwaitingUseAuthType, handshakeMsg); } - private void sendMessage(HandshakeMsg message) { + private void sendMessage(State newState, HandshakeMsg message) { + setCurrentState(newState); + var msgType = message.getMessageTypeCase(); - log.debug(connection.getId() + " sent: " + msgType); + log.debug("{} sent: {}", connection.getId(), msgType); connection.sendMessage(message.toByteArray()); } @@ -144,10 +165,11 @@ public void handleMessage(String id, byte[] message) { var handshakeMsg = HandshakeMsg.parseFrom(message); var msgType = handshakeMsg.getMessageTypeCase(); - log.debug(id + " got: " + msgType); + log.debug("{} got: {}", id, msgType); if (msgType == MessageTypeCase.HANDSHAKE_RESPONSE_CODE_MSG) { HandshakeResponseCodeMsg code = handshakeMsg.getHandshakeResponseCodeMsg(); + String errorMessage; if (code.equals(HandshakeResponseCodeMsg.INVALID_PASSWORD)) { errorMessage = I18N.getText("Handshake.msg.incorrectPassword"); } else if (code.equals(HandshakeResponseCodeMsg.INVALID_PUBLIC_KEY)) { @@ -161,7 +183,7 @@ public void handleMessage(String id, byte[] message) { } else { errorMessage = I18N.getText("Handshake.msg.invalidHandshake"); } - notifyObservers(); + future.completeExceptionally(new Failure(errorMessage)); return; } @@ -172,49 +194,46 @@ public void handleMessage(String id, byte[] message) { } else if (msgType == MessageTypeCase.USE_AUTH_TYPE_MSG) { handle(handshakeMsg.getUseAuthTypeMsg()); } else if (msgType == MessageTypeCase.PLAYER_BLOCKED_MSG) { - errorMessage = + var errorMessage = I18N.getText( "Handshake.msg.playerBlocked", handshakeMsg.getPlayerBlockedMsg().getReason()); - notifyObservers(); + future.completeExceptionally(new Failure(errorMessage)); } else { - errorMessage = I18N.getText("Handshake.msg.invalidHandshake"); - currentState = State.Error; - notifyObservers(); + var errorMessage = I18N.getText("Handshake.msg.invalidHandshake"); + setCurrentState(State.Error); + future.completeExceptionally(new Failure(errorMessage)); } break; case AwaitingPublicKeyAddition: if (msgType == MessageTypeCase.PUBLIC_KEY_ADDED_MSG) { handle(handshakeMsg.getPublicKeyAddedMsg()); } else { - errorMessage = I18N.getText("Handshake.msg.gmDeniedRequest"); - currentState = State.Error; - notifyObservers(); + var errorMessage = I18N.getText("Handshake.msg.gmDeniedRequest"); + setCurrentState(State.Error); + future.completeExceptionally(new Failure(errorMessage)); } break; case AwaitingConnectionSuccessful: if (msgType == MessageTypeCase.CONNECTION_SUCCESSFUL_MSG) { handle(handshakeMsg.getConnectionSuccessfulMsg()); } else { - errorMessage = I18N.getText("Handshake.msg.invalidHandshake"); - currentState = State.Error; - notifyObservers(); + var errorMessage = I18N.getText("Handshake.msg.invalidHandshake"); + setCurrentState(State.Error); + future.completeExceptionally(new Failure(errorMessage)); } break; } } catch (Exception e) { - log.warn(e.toString()); - exception = e; - currentState = State.Error; - errorMessage = I18N.getText("Handshake.msg.incorrectPassword"); - notifyObservers(); + log.warn("Unexpected exception during client handshake", e); + setCurrentState(State.Error); + future.completeExceptionally(new Failure("Handshake.msg.unexpectedError", e)); } } - private void handle(PublicKeyAddedMsg publicKeyAddedMsg) - throws ExecutionException, InterruptedException { + private void handle(PublicKeyAddedMsg publicKeyAddedMsg) { SwingUtilities.invokeLater(this::closeEasyConnectDialog); - startHandshake(); + startHandshakeInternal(); } private void handle(RequestPublicKeyMsg requestPublicKeyMsg) { @@ -222,8 +241,9 @@ private void handle(RequestPublicKeyMsg requestPublicKeyMsg) { var publicKey = new PublicPrivateKeyStore().getKeys().join(); var publicKeyUploadBuilder = PublicKeyUploadMsg.newBuilder(); publicKeyUploadBuilder.setPublicKey(publicKey.getEncodedPublicKeyText()); - sendMessage(HandshakeMsg.newBuilder().setPublicKeyUploadMsg(publicKeyUploadBuilder).build()); - currentState = State.AwaitingPublicKeyAddition; + sendMessage( + State.AwaitingPublicKeyAddition, + HandshakeMsg.newBuilder().setPublicKeyUploadMsg(publicKeyUploadBuilder).build()); SwingUtilities.invokeLater( () -> { JOptionPane pane = new JOptionPane(); @@ -244,7 +264,7 @@ public void windowClosed(WindowEvent e) { HandshakeMsg.newBuilder() .setHandshakeResponseCodeMsg(HandshakeResponseCodeMsg.INVALID_PUBLIC_KEY) .build(); - sendMessage(msg); + sendMessage(State.Error, msg); } }; dialog.addWindowListener(windowListener); @@ -270,7 +290,9 @@ private void handle(UseAuthTypeMsg useAuthTypeMsg) CipherUtil.Key publicKey = new PublicPrivateKeyStore().getKeys().get(); var handshakeChallenge = HandshakeChallenge.fromAsymmetricChallengeBytes( - player.getName(), useAuthTypeMsg.getChallenge(0).toByteArray(), publicKey); + client.getPlayer().getName(), + useAuthTypeMsg.getChallenge(0).toByteArray(), + publicKey); var expectedResponse = handshakeChallenge.getExpectedResponse(); clientAuthMsg = clientAuthMsg.setChallengeResponse(ByteString.copyFrom(expectedResponse)); } else { @@ -278,15 +300,18 @@ private void handle(UseAuthTypeMsg useAuthTypeMsg) byte[] responseIv = new byte[CipherUtil.CIPHER_BLOCK_SIZE]; rnd.nextBytes(responseIv); - player.setPasswordSalt(useAuthTypeMsg.getSalt().toByteArray()); + client.getPlayer().setPasswordSalt(useAuthTypeMsg.getSalt().toByteArray()); var iv = useAuthTypeMsg.getIv().toByteArray(); for (int i = 0; i < useAuthTypeMsg.getChallengeCount(); i++) { try { - Key key = player.getPassword(); + Key key = client.getPlayer().getPassword(); // Key key = playerDatabase.getPlayerPassword(player.getName()).get(); var handshakeChallenge = HandshakeChallenge.fromSymmetricChallengeBytes( - player.getName(), useAuthTypeMsg.getChallenge(i).toByteArray(), key, iv); + client.getPlayer().getName(), + useAuthTypeMsg.getChallenge(i).toByteArray(), + key, + iv); var expectedResponse = handshakeChallenge.getExpectedResponse(responseIv); clientAuthMsg = clientAuthMsg @@ -303,27 +328,25 @@ private void handle(UseAuthTypeMsg useAuthTypeMsg) } var handshakeMsg = HandshakeMsg.newBuilder().setClientAuthMessage(clientAuthMsg).build(); - sendMessage(handshakeMsg); - currentState = State.AwaitingConnectionSuccessful; + sendMessage(State.AwaitingConnectionSuccessful, handshakeMsg); } private void handle(ConnectionSuccessfulMsg connectionSuccessfulMsg) throws IOException { var policy = ServerPolicy.fromDto(connectionSuccessfulMsg.getServerPolicyDto()); - MapTool.setServerPolicy(policy); - player.setRole(connectionSuccessfulMsg.getRoleDto() == RoleDto.GM ? Role.GM : Role.PLAYER); + client.setServerPolicy(policy); + client + .getPlayer() + .setRole(connectionSuccessfulMsg.getRoleDto() == RoleDto.GM ? Role.GM : Role.PLAYER); MapTool.getFrame() .getToolbarPanel() .getMapselect() - .setVisible((!policy.getMapSelectUIHidden()) || MapTool.getPlayer().isGM()); - if ((!policy.getDisablePlayerAssetPanel()) || MapTool.getPlayer().isGM()) { + .setVisible((!policy.getMapSelectUIHidden()) || client.getPlayer().isGM()); + if ((!policy.getDisablePlayerAssetPanel()) || client.getPlayer().isGM()) { MapTool.getFrame().getAssetPanel().enableAssets(); } else { MapTool.getFrame().getAssetPanel().disableAssets(); } if (!MapTool.isHostingServer()) { - PlayerDatabaseFactory.setCurrentPlayerDatabase(PlayerDatabaseType.LOCAL_PLAYER); - var playerDb = (LocalPlayerDatabase) PlayerDatabaseFactory.getCurrentPlayerDatabase(); - playerDb.setLocalPlayer(player); if (!MapTool.isPersonalServer()) { new CampaignManager().clearCampaignData(); if (connectionSuccessfulMsg.hasGameDataDto()) { @@ -362,51 +385,8 @@ private void handle(ConnectionSuccessfulMsg connectionSuccessfulMsg) throws IOEx } } } - currentState = State.Success; - notifyObservers(); - } - - @Override - public void addObserver(HandshakeObserver observer) { - observerList.add(observer); - } - - @Override - public void removeObserver(HandshakeObserver observer) { - observerList.remove(observer); - } - - /** Notifies observers that the handshake has completed or errored out.. */ - private void notifyObservers() { - SwingUtilities.invokeLater(this::closeEasyConnectDialog); - for (var observer : observerList) { - observer.onCompleted(this); - } - } - - @Override - public boolean isSuccessful() { - return currentState == State.Success; - } - - @Override - public String getErrorMessage() { - return errorMessage; - } - - @Override - public Connection getConnection() { - return connection; - } - - @Override - public Exception getException() { - return exception; - } - - @Override - public Player getPlayer() { - return player; + setCurrentState(State.Success); + future.complete(null); } private void closeEasyConnectDialog() { diff --git a/src/main/java/net/rptools/maptool/server/Handshake.java b/src/main/java/net/rptools/maptool/server/Handshake.java index 7cc26cdafd..8adbb68c37 100644 --- a/src/main/java/net/rptools/maptool/server/Handshake.java +++ b/src/main/java/net/rptools/maptool/server/Handshake.java @@ -14,68 +14,23 @@ */ package net.rptools.maptool.server; -import java.util.concurrent.ExecutionException; -import net.rptools.clientserver.simple.MessageHandler; -import net.rptools.clientserver.simple.connection.Connection; -import net.rptools.maptool.model.player.Player; +import java.util.function.BiConsumer; -public interface Handshake extends MessageHandler { +public interface Handshake { - /** - * Returns if the handshake has been successful or not. - * - * @return {@code true} if the handshake has been successful, {code false} if it has failed or is - * still in progress. - */ - boolean isSuccessful(); + void whenComplete(BiConsumer callback); - /** - * Returns the message for the error -- if any -- that occurred during the handshake. - * - * @return the message for the error that occurred during handshake. - */ - String getErrorMessage(); + /** Starts the handshake process. */ + void startHandshake(); - /** - * Returns the connection for this {@code ServerHandshake}. - * - * @return the connection for this {@code ServerHandshake}. - */ - Connection getConnection(); + class Failure extends Exception { + // TODO When we have access to I18N, force this to be translatable. + public Failure(String message) { + super(message); + } - /** - * Returns the exception -- if any -- that occurred during processing of the handshake. - * - * @return the exception that occurred during the processing of the handshake. - */ - Exception getException(); - - /** - * Returns the player associated with the handshake. - * - * @return the player associated with the handshake. - */ - Player getPlayer(); - - /** - * Adds an observer to the handshake process. - * - * @param observer the observer of the handshake process. - */ - void addObserver(HandshakeObserver observer); - - /** - * Removes an observer from the handshake process. - * - * @param observer the observer of the handshake process. - */ - void removeObserver(HandshakeObserver observer); - - /** - * Starts the handshake process. - * - * @throws ExecutionException when there is an exception in the background task. - * @throws InterruptedException when the background task is interrupted. - */ - void startHandshake() throws ExecutionException, InterruptedException; + public Failure(String message, Throwable cause) { + super(message, cause); + } + } } diff --git a/src/main/java/net/rptools/maptool/server/HandshakeObserver.java b/src/main/java/net/rptools/maptool/server/HandshakeObserver.java deleted file mode 100644 index e8e62d66a0..0000000000 --- a/src/main/java/net/rptools/maptool/server/HandshakeObserver.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.server; - -public interface HandshakeObserver { - - void onCompleted(Handshake handshake); -} diff --git a/src/main/java/net/rptools/maptool/server/MapToolServer.java b/src/main/java/net/rptools/maptool/server/MapToolServer.java index be03bd4393..aeac1666e2 100644 --- a/src/main/java/net/rptools/maptool/server/MapToolServer.java +++ b/src/main/java/net/rptools/maptool/server/MapToolServer.java @@ -14,17 +14,22 @@ */ package net.rptools.maptool.server; -import static net.rptools.maptool.model.player.PlayerDatabaseFactory.PlayerDatabaseType.PERSONAL_SERVER; - import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Random; +import javax.annotation.Nullable; import javax.swing.SwingUtilities; +import net.rptools.clientserver.ConnectionFactory; +import net.rptools.clientserver.simple.DisconnectHandler; +import net.rptools.clientserver.simple.MessageHandler; import net.rptools.clientserver.simple.connection.Connection; +import net.rptools.clientserver.simple.server.Router; +import net.rptools.clientserver.simple.server.Server; import net.rptools.clientserver.simple.server.ServerObserver; +import net.rptools.maptool.client.AppConstants; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.MapToolRegistry; import net.rptools.maptool.client.ui.connectioninfodialog.ConnectionInfoDialog; @@ -33,12 +38,17 @@ import net.rptools.maptool.model.Campaign; import net.rptools.maptool.model.GUID; import net.rptools.maptool.model.TextMessage; -import net.rptools.maptool.model.player.PlayerDatabase; -import net.rptools.maptool.model.player.PlayerDatabaseFactory; +import net.rptools.maptool.model.player.Player; +import net.rptools.maptool.model.player.ServerSidePlayerDatabase; import net.rptools.maptool.server.proto.Message; +import net.rptools.maptool.server.proto.PlayerConnectedMsg; +import net.rptools.maptool.server.proto.PlayerDisconnectedMsg; +import net.rptools.maptool.server.proto.SetCampaignMsg; import net.rptools.maptool.server.proto.UpdateAssetTransferMsg; import net.rptools.maptool.transfer.AssetProducer; import net.rptools.maptool.transfer.AssetTransferManager; +import net.rptools.maptool.util.UPnPUtil; +import net.tsc.servicediscovery.ServiceAnnouncer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -49,98 +59,255 @@ public class MapToolServer { private static final Logger log = LogManager.getLogger(MapToolServer.class); private static final int ASSET_CHUNK_SIZE = 5 * 1024; - private final MapToolServerConnection conn; + public enum State { + New, + Started, + Stopped + } + + private final Server server; + private final MessageHandler messageHandler; + private final Router router; private final ServerConfig config; - private final PlayerDatabase playerDatabase; + private final ServerSidePlayerDatabase playerDatabase; + + /** Maps connection IDs to their associated player */ + private final Map playerMap = Collections.synchronizedMap(new HashMap<>()); private final Map assetManagerMap = Collections.synchronizedMap(new HashMap()); - private final Map connectionMap = - Collections.synchronizedMap(new HashMap()); private final AssetProducerThread assetProducerThread; + private final boolean useUPnP; + private final ServiceAnnouncer announcer; private Campaign campaign; private ServerPolicy policy; private HeartbeatThread heartbeatThread; - - public MapToolServer(ServerConfig config, ServerPolicy policy, PlayerDatabase playerDb) - throws IOException { + private final DisconnectHandler onConnectionDisconnected; + private final ServerObserver serverObserver; + + private State currentState; + + public MapToolServer( + String id, + Campaign campaign, + @Nullable ServerConfig config, + boolean useUPnP, + ServerPolicy policy, + ServerSidePlayerDatabase playerDb) { this.config = config; - this.policy = policy; - playerDatabase = playerDb; - conn = new MapToolServerConnection(this, playerDatabase, new ServerMessageHandler(this)); + this.useUPnP = useUPnP; + this.policy = new ServerPolicy(policy); + this.playerDatabase = playerDb; + + this.announcer = + config == null + ? null + : new ServiceAnnouncer(id, config.getPort(), AppConstants.SERVICE_GROUP); + + server = ConnectionFactory.getInstance().createServer(this.config); + messageHandler = new ServerMessageHandler(this); + this.router = new Router(); - campaign = new Campaign(); + // Make sure the server has a different copy than the client. + this.campaign = new Campaign(campaign); assetProducerThread = new AssetProducerThread(); - assetProducerThread.start(); - // Start a heartbeat if requested - if (config.isServerRegistered()) { - heartbeatThread = new HeartbeatThread(); - heartbeatThread.start(); + currentState = State.New; + + this.onConnectionDisconnected = this::releaseClientConnection; + this.serverObserver = this::connectionAdded; + } + + /** + * Transition from any state except {@code newState} to {@code newState}. + * + * @param newState The new state to set. + */ + private boolean transitionToState(State newState) { + if (currentState == newState) { + log.warn( + "Failed to transition to state {} because that is already the current state", newState); + return false; + } else { + currentState = newState; + return true; } } - public void configureClientConnection(Connection connection) { - String id = connection.getId(); - assetManagerMap.put(id, new AssetTransferManager()); - connectionMap.put(id, connection); + /** + * Transition from {@code expectedState} to {@code newState}. + * + * @param expectedState The state to transition from + * @param newState The new state to set. + */ + private boolean transitionToState(State expectedState, State newState) { + if (currentState != expectedState) { + log.warn( + "Failed to transition from state {} to state {} because the current state is actually {}", + expectedState, + newState, + currentState); + return false; + } else { + currentState = newState; + return true; + } } - public Connection getClientConnection(String id) { - return connectionMap.get(id); + public ServerSidePlayerDatabase getPlayerDatabase() { + return playerDatabase; } - public String getConnectionId(String playerId) { - return conn.getConnectionId(playerId); + public State getState() { + return currentState; } - /** - * Forceably disconnects a client and cleans up references to it - * - * @param id the connection ID - */ - public void releaseClientConnection(String id) { - Connection connection = getClientConnection(id); - if (connection != null) { - connection.close(); + private String getConnectionId(String playerId) { + synchronized (playerMap) { + for (Map.Entry entry : playerMap.entrySet()) { + if (entry.getValue().getName().equalsIgnoreCase(playerId)) { + return entry.getKey(); + } + } } - assetManagerMap.remove(id); - connectionMap.remove(id); + return null; } - public void addAssetProducer(String connectionId, AssetProducer producer) { - AssetTransferManager manager = assetManagerMap.get(connectionId); - manager.addProducer(producer); + private Player getPlayer(String playerId) { + synchronized (playerMap) { + for (Player player : playerMap.values()) { + if (player.getName().equalsIgnoreCase(playerId)) { + return player; + } + } + } + return null; + } + + public boolean isPersonalServer() { + return config == null; + } + + public boolean isServerRegistered() { + return config == null ? false : config.isServerRegistered(); + } + + public String getName() { + return config == null ? "" : config.getServerName(); + } + + public int getPort() { + return config == null ? -1 : config.getPort(); + } + + private void connectionAdded(Connection conn) { + var handshake = + new ServerHandshake( + this, conn, playerDatabase, config != null && config.getUseEasyConnect()); + + handshake.whenComplete( + (player, error) -> { + if (error != null) { + log.error("Client closing: bad handshake", error); + releaseClientConnection(conn); + } else { + log.debug("About to add new client"); + addRemoteConnection(conn, player); + } + }); + // Make sure the client is allowed + handshake.startHandshake(); } - public void addObserver(ServerObserver observer) { - if (observer != null) { - conn.addObserver(observer); + private void installConnection(Connection conn, Player player) { + playerMap.put(conn.getId().toUpperCase(), player); + + conn.addMessageHandler(messageHandler); + conn.addDisconnectHandler(onConnectionDisconnected); + + // Make sure any stale connections are gone to avoid conflicts, then add the new one. + for (var reaped : router.reapClients()) { + try { + releaseClientConnection(reaped); + } catch (Exception e) { + // Don't want to raise an error if notification of removing a dead connection failed + } } + router.addConnection(conn); + + assetManagerMap.put(conn.getId(), new AssetTransferManager()); + + synchronized (playerMap) { + for (Player remotePlayer : playerMap.values()) { + var msg = PlayerConnectedMsg.newBuilder().setPlayer(remotePlayer.toDto()); + sendMessage(conn.getId(), Message.newBuilder().setPlayerConnectedMsg(msg).build()); + } + } + var msg = PlayerConnectedMsg.newBuilder().setPlayer(player.getTransferablePlayer().toDto()); + broadcastMessage(Message.newBuilder().setPlayerConnectedMsg(msg).build()); + } + + public void addLocalConnection(Connection conn, Player localPlayer) { + installConnection(conn, localPlayer); + } + + private void addRemoteConnection(Connection conn, Player connPlayer) { + installConnection(conn, connPlayer); + + var msg2 = SetCampaignMsg.newBuilder().setCampaign(campaign.toDto()); + sendMessage(conn.getId(), Message.newBuilder().setSetCampaignMsg(msg2).build()); } - public void removeObserver(ServerObserver observer) { - conn.removeObserver(observer); + public void bootPlayer(String playerId) { + var connectionId = getConnectionId(playerId); + var connection = router.getConnection(connectionId); + if (connection == null) { + return; + } + + releaseClientConnection(connection); } - public boolean isHostId(String playerId) { - return config.getHostPlayerId() != null && config.getHostPlayerId().equals(playerId); + /** + * Cleans up references to a disconnected client connection. + * + * @param connection the connection to release + */ + private void releaseClientConnection(Connection connection) { + connection.removeDisconnectHandler(onConnectionDisconnected); + + connection.close(); + router.removeConnection(connection); + assetManagerMap.remove(connection.getId()); + + // Notify everyone else about the disconnection. + var player = playerMap.remove(connection.getId().toUpperCase()); + if (player != null) { + var msg = + PlayerDisconnectedMsg.newBuilder().setPlayer(player.getTransferablePlayer().toDto()); + broadcastMessage( + new String[] {connection.getId()}, + Message.newBuilder().setPlayerDisconnectedMsg(msg).build()); + } } - public MapToolServerConnection getConnection() { - return conn; + public void addAssetProducer(String connectionId, AssetProducer producer) { + AssetTransferManager manager = assetManagerMap.get(connectionId); + manager.addProducer(producer); } - public boolean isPlayerConnected(String id) { - return conn.getPlayer(id) != null; + public boolean isPlayerConnected(String playerId) { + return getPlayer(playerId) != null; } public void updatePlayerStatus(String playerName, GUID zoneId, boolean loaded) { - var player = conn.getPlayer(playerName); - player.setLoaded(loaded); - player.setZoneId(zoneId); + var player = getPlayer(playerName); + if (player != null) { + player.setLoaded(loaded); + player.setZoneId(zoneId); + } } public void setCampaign(Campaign campaign) { @@ -156,46 +323,143 @@ public Campaign getCampaign() { } public ServerPolicy getPolicy() { - return policy; + return new ServerPolicy(policy); } public void updateServerPolicy(ServerPolicy policy) { - this.policy = policy; - } - - public ServerConfig getConfig() { - return config; + this.policy = new ServerPolicy(policy); } public void stop() { - conn.close(); + if (!transitionToState(State.Stopped)) { + return; + } + + server.close(); + for (var connection : router.removeAll()) { + connection.removeDisconnectHandler(onConnectionDisconnected); + connection.close(); + } + + assetManagerMap.clear(); + if (heartbeatThread != null) { heartbeatThread.shutdown(); } if (assetProducerThread != null) { assetProducerThread.shutdown(); } - } - private static final Random random = new Random(); + if (announcer != null) { + announcer.stop(); + } + + // Unregister ourselves + if (config != null && config.isServerRegistered()) { + try { + MapToolRegistry.getInstance().unregisterInstance(); + } catch (Throwable t) { + MapTool.showError("While unregistering server instance", t); + } + } + + // Close UPnP port mapping if used + if (useUPnP && config != null) { + int port = config.getPort(); + UPnPUtil.closePort(port); + } + } public void start() throws IOException { - conn.open(); + if (!transitionToState(State.New, State.Started)) { + return; + } + + server.addObserver(serverObserver); + try { + server.start(); + } catch (IOException e) { + // Make sure we're in a reasonable state before propagating. + log.error("Failed to start server", e); + transitionToState(State.Stopped); + server.removeObserver(serverObserver); + throw e; + } + + // Use UPnP to open port in router + if (useUPnP && config != null) { + UPnPUtil.openPort(config.getPort()); + } + + // Registered ? + if (config != null && config.isServerRegistered()) { + try { + MapToolRegistry.RegisterResponse result = + MapToolRegistry.getInstance() + .registerInstance(config.getServerName(), config.getPort(), config.getUseWebRTC()); + if (result == MapToolRegistry.RegisterResponse.NAME_EXISTS) { + MapTool.showError("msg.error.alreadyRegistered"); + } else { + heartbeatThread = new HeartbeatThread(config.getPort()); + heartbeatThread.start(); + } + // TODO: I don't like this + } catch (Exception e) { + MapTool.showError("msg.error.failedCannotRegisterServer", e); + } + } + + if (announcer != null) { + announcer.start(); + } + + assetProducerThread.start(); + } + + public void sendMessage(String id, Message message) { + log.debug("{} sent to {}: {}", getName(), id, message.getMessageTypeCase()); + router.sendMessage(id, message.toByteArray()); + } + + public void sendMessage(String id, Object channel, Message message) { + log.debug( + "{} sent to {}: {} ({})", getName(), id, message.getMessageTypeCase(), channel.toString()); + router.sendMessage(id, channel, message.toByteArray()); + } + + public void broadcastMessage(Message message) { + log.debug("{} broadcast: {}", getName(), message.getMessageTypeCase()); + router.broadcastMessage(message.toByteArray()); + } + + public void broadcastMessage(String[] exclude, Message message) { + log.debug( + "{} broadcast: {} except to {}", + getName(), + message.getMessageTypeCase(), + String.join(",", exclude)); + router.broadcastMessage(exclude, message.toByteArray()); } private class HeartbeatThread extends Thread { + private static final Random random = new Random(); + + private final int port; private boolean stop = false; private static final int HEARTBEAT_DELAY = 10 * 60 * 1000; // 10 minutes private static final int HEARTBEAT_FLUX = 20 * 1000; // 20 seconds private boolean ever_had_an_error = false; + public HeartbeatThread(int port) { + this.port = port; + } + @Override public void run() { int WARNING_TIME = 2; // number of heartbeats before popup warning int errors = 0; String IP_addr = ConnectionInfoDialog.getExternalAddress(); - int port = getConfig().getPort(); while (!stop) { try { @@ -282,11 +546,10 @@ public void run() { if (chunk != null) { lookForMore = true; var msg = UpdateAssetTransferMsg.newBuilder().setChunk(chunk); - getConnection() - .sendMessage( - entry.getKey(), - MapToolConstants.Channel.IMAGE, - Message.newBuilder().setUpdateAssetTransferMsg(msg).build()); + sendMessage( + entry.getKey(), + MapToolConstants.Channel.IMAGE, + Message.newBuilder().setUpdateAssetTransferMsg(msg).build()); } } if (lookForMore) { @@ -307,14 +570,4 @@ public void shutdown() { stop = true; } } - - //// - // STANDALONE SERVER - public static void main(String[] args) throws IOException { - // This starts the server thread. - PlayerDatabaseFactory.setCurrentPlayerDatabase(PERSONAL_SERVER); - PlayerDatabase playerDatabase = PlayerDatabaseFactory.getCurrentPlayerDatabase(); - MapToolServer server = - new MapToolServer(new ServerConfig(), new ServerPolicy(), playerDatabase); - } } diff --git a/src/main/java/net/rptools/maptool/server/MapToolServerConnection.java b/src/main/java/net/rptools/maptool/server/MapToolServerConnection.java deleted file mode 100644 index 30ac5b3c26..0000000000 --- a/src/main/java/net/rptools/maptool/server/MapToolServerConnection.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.server; - -import java.io.IOException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import net.rptools.clientserver.ConnectionFactory; -import net.rptools.clientserver.simple.connection.Connection; -import net.rptools.clientserver.simple.server.HandshakeProvider; -import net.rptools.clientserver.simple.server.Server; -import net.rptools.clientserver.simple.server.ServerObserver; -import net.rptools.maptool.model.player.Player; -import net.rptools.maptool.model.player.PlayerDatabase; -import net.rptools.maptool.server.proto.Message; -import net.rptools.maptool.server.proto.PlayerConnectedMsg; -import net.rptools.maptool.server.proto.PlayerDisconnectedMsg; -import net.rptools.maptool.server.proto.SetCampaignMsg; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * @author trevor - */ -public class MapToolServerConnection - implements ServerObserver, HandshakeProvider, HandshakeObserver { - private static final Logger log = LogManager.getLogger(MapToolServerConnection.class); - private final Map playerMap = new ConcurrentHashMap<>(); - private final Map handshakeMap = new ConcurrentHashMap<>(); - private final MapToolServer server; - private final Server connection; - private final PlayerDatabase playerDatabase; - private final boolean useEasyConnect; - - public MapToolServerConnection( - MapToolServer server, PlayerDatabase playerDatabase, ServerMessageHandler handler) - throws IOException { - this.connection = - ConnectionFactory.getInstance().createServer(server.getConfig(), this, handler); - this.server = server; - this.playerDatabase = playerDatabase; - this.useEasyConnect = server.getConfig().getUseEasyConnect(); - addObserver(this); - } - - /* - * (non-Javadoc) - * - * @see net.rptools.clientserver.simple.server.ServerConnection# handleConnectionHandshake(java.net.Socket) - */ - public Handshake getConnectionHandshake(Connection conn) { - var handshake = new ServerHandshake(conn, playerDatabase, useEasyConnect); - handshakeMap.put(conn, handshake); - handshake.addObserver(this); - conn.addMessageHandler(handshake); - return handshake; - } - - @Override - public void releaseHandshake(Connection conn) { - var handshake = handshakeMap.get(conn); - handshakeMap.remove(conn); - conn.removeMessageHandler(handshake); - } - - public Player getPlayer(String id) { - for (Player player : playerMap.values()) { - if (player.getName().equalsIgnoreCase(id)) { - return player; - } - } - return null; - } - - public String getConnectionId(String playerId) { - for (Map.Entry entry : playerMap.entrySet()) { - if (entry.getValue().getName().equalsIgnoreCase(playerId)) { - return entry.getKey(); - } - } - return null; - } - - //// - // SERVER OBSERVER - - /** Handle late connections */ - public void connectionAdded(Connection conn) { - server.configureClientConnection(conn); - - Player connectedPlayer = playerMap.get(conn.getId().toUpperCase()); - for (Player player : playerMap.values()) { - var msg = PlayerConnectedMsg.newBuilder().setPlayer(player.toDto()); - server - .getConnection() - .sendMessage(conn.getId(), Message.newBuilder().setPlayerConnectedMsg(msg).build()); - } - var msg = - PlayerConnectedMsg.newBuilder().setPlayer(connectedPlayer.getTransferablePlayer().toDto()); - server - .getConnection() - .broadcastMessage(Message.newBuilder().setPlayerConnectedMsg(msg).build()); - - var msg2 = SetCampaignMsg.newBuilder().setCampaign(server.getCampaign().toDto()); - server - .getConnection() - .sendMessage(conn.getId(), Message.newBuilder().setSetCampaignMsg(msg2).build()); - } - - public void connectionRemoved(Connection conn) { - server.releaseClientConnection(conn.getId()); - var player = playerMap.get(conn.getId().toUpperCase()).getTransferablePlayer(); - var msg = PlayerDisconnectedMsg.newBuilder().setPlayer(player.toDto()); - server - .getConnection() - .broadcastMessage( - new String[] {conn.getId()}, - Message.newBuilder().setPlayerDisconnectedMsg(msg).build()); - playerMap.remove(conn.getId().toUpperCase()); - } - - public void sendMessage(String id, Message message) { - log.debug( - server.getConfig().getServerName() - + " sent to " - + id - + ": " - + message.getMessageTypeCase()); - connection.sendMessage(id, message.toByteArray()); - } - - public void sendMessage(String id, Object channel, Message message) { - log.debug( - server.getConfig().getServerName() - + " sent to " - + id - + ":" - + message.getMessageTypeCase() - + " (" - + channel.toString() - + ")"); - connection.sendMessage(id, channel, message.toByteArray()); - } - - public void broadcastMessage(Message message) { - log.debug(server.getConfig().getServerName() + " broadcast: " + message.getMessageTypeCase()); - connection.broadcastMessage(message.toByteArray()); - } - - public void broadcastMessage(String[] exclude, Message message) { - log.debug( - server.getConfig().getServerName() - + " broadcast: " - + message.getMessageTypeCase() - + " except to " - + String.join(",", exclude)); - connection.broadcastMessage(exclude, message.toByteArray()); - } - - public void open() throws IOException { - connection.start(); - } - - public void close() { - connection.close(); - } - - public void addObserver(ServerObserver observer) { - connection.addObserver(observer); - } - - public void removeObserver(ServerObserver observer) { - connection.removeObserver(observer); - } - - @Override - public void onCompleted(Handshake handshake) { - handshake.removeObserver(this); - if (handshake.isSuccessful()) { - Player player = handshake.getPlayer(); - - if (player != null) { - playerMap.put(handshake.getConnection().getId().toUpperCase(), player); - } - } else { - var exception = handshake.getException(); - if (exception != null) log.error("Handshake failure: " + exception, exception); - } - } -} diff --git a/src/main/java/net/rptools/maptool/server/ServerConfig.java b/src/main/java/net/rptools/maptool/server/ServerConfig.java index 9807b3acba..818f9a31f4 100644 --- a/src/main/java/net/rptools/maptool/server/ServerConfig.java +++ b/src/main/java/net/rptools/maptool/server/ServerConfig.java @@ -14,69 +14,18 @@ */ package net.rptools.maptool.server; -import java.io.IOException; -import java.net.ServerSocket; -import java.util.Random; -import net.rptools.maptool.util.PasswordGenerator; - public class ServerConfig { public static final int DEFAULT_PORT = 51234; - public static final int PORT_RANGE_START = 4000; - public static final int PORT_RANGE_END = 20000; - - private static final String personalServerGMPassword; - - private static final String personalServerPlayerPassword; - - static { - PasswordGenerator passwordGenerator = new PasswordGenerator(); - // Generate a random password for personal server - personalServerGMPassword = passwordGenerator.getPassword(); - String playerPass = passwordGenerator.getPassword(); - if (playerPass.equals(personalServerGMPassword)) { // super unlikely but just to play safe - personalServerPlayerPassword = playerPass + "!"; - } else { - personalServerPlayerPassword = playerPass; - } - } - - private int port; - private String hostPlayerId; + private final int port; + private final String hostPlayerId; private final String gmPassword; private final String playerPassword; - private boolean personalServer; - private String serverName; - private String hostName; + private final String serverName; + private final String hostName; private final boolean useEasyConnect; private final boolean useWebRTC; - public static String getPersonalServerGMPassword() { - return personalServerGMPassword; - } - - public static String getPersonalServerPlayerPassword() { - return personalServerPlayerPassword; - } - - public ServerConfig() { - playerPassword = getPersonalServerPlayerPassword(); - gmPassword = getPersonalServerGMPassword(); - useEasyConnect = false; - useWebRTC = false; - } - - public ServerConfig( - String hostPlayerId, - String gmPassword, - String playerPassword, - int port, - String serverName, - String hostName, - boolean useWebRTC) { - this(hostPlayerId, gmPassword, playerPassword, port, serverName, hostName, false, useWebRTC); - } - public ServerConfig( String hostPlayerId, String gmPassword, @@ -101,38 +50,17 @@ public String getHostPlayerId() { } public boolean isServerRegistered() { - return serverName != null && serverName.length() > 0; + return serverName != null && !serverName.isEmpty(); } public String getServerName() { return serverName; } - public boolean gmPasswordMatches(String password) { - return safeCompare(gmPassword, password); - } - - public boolean playerPasswordMatches(String password) { - return safeCompare(playerPassword, password); - } - - public boolean isPersonalServer() { - return personalServer; - } - public int getPort() { return port; } - public static ServerConfig createPersonalServerConfig() { - ServerConfig config = new ServerConfig(); - config.serverName = "Personal server"; - config.hostName = "localhost"; - config.personalServer = true; - config.port = findOpenPort(PORT_RANGE_START, PORT_RANGE_END); - return config; - } - public String getGmPassword() { return gmPassword; } @@ -152,45 +80,4 @@ public boolean getUseEasyConnect() { public boolean getUseWebRTC() { return useWebRTC; } - - private static Random r = new Random(); - - private static int findOpenPort(int rangeLow, int rangeHigh) { - // Presumably there will always be at least one open port between low and high - while (true) { - ServerSocket ss = null; - try { - int port = rangeLow + (int) ((rangeHigh - rangeLow) * r.nextFloat()); - ss = new ServerSocket(port); - - // This port was open before we took it, so we'll just close it and use this one - // LATER: This isn't super exact, it's conceivable that another process will take - // the port between our closing it and the server opening it. But that's the best - // we can do at the moment until the server is refactored. - return port; - } catch (Exception e) { - // Just keep trying - } finally { - if (ss != null) { - try { - ss.close(); - } catch (IOException ioe) { - // No big deal - ioe.printStackTrace(); - } - } - } - } - } - - private static boolean safeCompare(String s1, String s2) { - if (s1 == null) { - s1 = ""; - } - if (s2 == null) { - s2 = ""; - } - - return s1.equals(s2); - } } diff --git a/src/main/java/net/rptools/maptool/server/ServerHandshake.java b/src/main/java/net/rptools/maptool/server/ServerHandshake.java index 5fe1768808..919339615a 100644 --- a/src/main/java/net/rptools/maptool/server/ServerHandshake.java +++ b/src/main/java/net/rptools/maptool/server/ServerHandshake.java @@ -21,10 +21,11 @@ import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; -import java.util.List; import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; +import java.util.function.BiConsumer; import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; @@ -41,8 +42,8 @@ import net.rptools.maptool.model.player.Player; import net.rptools.maptool.model.player.Player.Role; import net.rptools.maptool.model.player.PlayerAwaitingApproval; -import net.rptools.maptool.model.player.PlayerDatabase; import net.rptools.maptool.model.player.PlayerDatabase.AuthMethod; +import net.rptools.maptool.model.player.ServerSidePlayerDatabase; import net.rptools.maptool.server.proto.AuthTypeEnum; import net.rptools.maptool.server.proto.ClientAuthMsg; import net.rptools.maptool.server.proto.ClientInitMsg; @@ -63,40 +64,33 @@ import org.apache.logging.log4j.Logger; /** Class used to handle the server side part of the connection handshake. */ -public class ServerHandshake implements Handshake, MessageHandler { +public class ServerHandshake implements Handshake, MessageHandler { /** Instance used for log messages. */ private static final Logger log = LogManager.getLogger(ServerHandshake.class); + private final CompletableFuture future = new CompletableFuture<>(); + private CompletionStage stage = future; + /** The database used for retrieving players. */ - private final PlayerDatabase playerDatabase; + private final ServerSidePlayerDatabase playerDatabase; + + private final MapToolServer server; /** The connection to the client. */ private final Connection connection; - /** Observers that want to be notified when the status changes. */ - private final List observerList = new CopyOnWriteArrayList<>(); - /** The index in the array for the GM handshake challenge, only used for role based auth */ private static final int GM_CHALLENGE = 0; /** The index in the array for the Player handshake challenge, only used for role based auth */ private static final int PLAYER_CHALLENGE = 1; - /** Message for any error that has occurred, {@code null} if no error has occurred. */ - private String errorMessage; - /** The pin for the new public key easy connect request. */ private String easyConnectPin; /** The username for the new public key easy connect request. */ private String easyConnectName; - /** - * Any exception that occurred that causes an error, {@code null} if no exception which causes an - * error has occurred. - */ - private Exception exception; - /** The player that this connection is for. */ private Player player; @@ -118,69 +112,43 @@ public class ServerHandshake implements Handshake, MessageHandler { * @param useEasyConnect If true, the client will use the easy connect method. */ public ServerHandshake( - Connection connection, PlayerDatabase playerDatabase, boolean useEasyConnect) { + MapToolServer server, + Connection connection, + ServerSidePlayerDatabase playerDatabase, + boolean useEasyConnect) { + this.server = server; this.connection = connection; this.playerDatabase = playerDatabase; this.useEasyConnect = useEasyConnect; - } - - @Override - public boolean isSuccessful() { - return currentState == State.Success; - } - - @Override - public synchronized String getErrorMessage() { - return errorMessage; - } - @Override - public synchronized Connection getConnection() { - return connection; - } + whenComplete( + (result, error) -> { + connection.removeMessageHandler(this); - @Override - public synchronized Exception getException() { - return exception; + if (getEasyConnectName() != null) { + SwingUtilities.invokeLater( + () -> + MapTool.getFrame() + .getConnectionPanel() + .removeAwaitingApproval(easyConnectName)); + } + }); } @Override - public synchronized Player getPlayer() { - return player; + public void whenComplete(BiConsumer callback) { + stage = stage.whenComplete(callback); } private synchronized void setPlayer(Player player) { this.player = player; } - private synchronized void setErrorMessage(String errorMessage) { - this.errorMessage = errorMessage; - } - - private synchronized void setException(Exception exception) { - this.exception = exception; - } - private synchronized void setCurrentState(State state) { + log.debug("Transitioning from {} to {}", currentState, state); currentState = state; } - private synchronized State getCurrentState() { - return currentState; - } - - private synchronized void setEasyConnectPin(String pin) { - easyConnectPin = pin; - } - - private String getEasyConnectPin() { - return easyConnectPin; - } - - private synchronized void setEasyConnectName(String name) { - easyConnectName = name; - } - private synchronized String getEasyConnectName() { return easyConnectName; } @@ -193,16 +161,17 @@ private synchronized String getEasyConnectName() { */ private void sendErrorResponseAndNotify(HandshakeResponseCodeMsg errorCode) { var msg = HandshakeMsg.newBuilder().setHandshakeResponseCodeMsg(errorCode).build(); - sendMessage(msg); - setCurrentState(State.PlayerBlocked); + sendMessage(State.PlayerBlocked, msg); // Do not notify users as it will disconnect and client won't get message instead wait // for client to disconnect after getting this message, if they don't then it will fail // with invalid handshake. } - private void sendMessage(HandshakeMsg message) { + private void sendMessage(State newState, HandshakeMsg message) { + setCurrentState(newState); + var msgType = message.getMessageTypeCase(); - log.info("Server sent to " + connection.getId() + ": " + msgType); + log.debug("Server sent to {}: {}", connection.getId(), msgType); connection.sendMessage(message.toByteArray()); } @@ -212,32 +181,30 @@ public void handleMessage(String id, byte[] message) { var handshakeMsg = HandshakeMsg.parseFrom(message); var msgType = handshakeMsg.getMessageTypeCase(); - log.info("from " + id + " got: " + msgType); + log.debug("from {} got: {}", id, msgType); if (msgType == MessageTypeCase.HANDSHAKE_RESPONSE_CODE_MSG) { HandshakeResponseCodeMsg code = handshakeMsg.getHandshakeResponseCodeMsg(); + String errorMessage; if (code.equals(HandshakeResponseCodeMsg.INVALID_PASSWORD)) { - setErrorMessage(I18N.getText("Handshake.msg.incorrectPassword")); + errorMessage = I18N.getText("Handshake.msg.incorrectPassword"); } else if (code.equals(HandshakeResponseCodeMsg.INVALID_PUBLIC_KEY)) { - setErrorMessage(I18N.getText("Handshake.msg.incorrectPublicKey")); + errorMessage = I18N.getText("Handshake.msg.incorrectPublicKey"); } else { - setErrorMessage(I18N.getText("Handshake.msg.invalidHandshake")); + errorMessage = I18N.getText("Handshake.msg.invalidHandshake"); } - - notifyObservers(); + future.completeExceptionally(new Failure(errorMessage)); return; } switch (currentState) { case PlayerBlocked: - setErrorMessage(I18N.getText("Handshake.msg.invalidHandshake")); sendErrorResponseAndNotify(HandshakeResponseCodeMsg.INVALID_HANDSHAKE); break; case AwaitingClientInit: if (msgType == HandshakeMsg.MessageTypeCase.CLIENT_INIT_MSG) { handle(handshakeMsg.getClientInitMsg()); } else { - setErrorMessage(I18N.getText("Handshake.msg.invalidHandshake")); sendErrorResponseAndNotify(HandshakeResponseCodeMsg.INVALID_HANDSHAKE); } break; @@ -246,7 +213,6 @@ public void handleMessage(String id, byte[] message) { if (msgType == MessageTypeCase.CLIENT_AUTH_MESSAGE) { handle(handshakeMsg.getClientAuthMessage()); } else { - setErrorMessage(I18N.getText("Handshake.msg.invalidHandshake")); sendErrorResponseAndNotify(HandshakeResponseCodeMsg.INVALID_HANDSHAKE); } break; @@ -256,11 +222,9 @@ public void handleMessage(String id, byte[] message) { } } } catch (Exception e) { - log.warn(e.toString()); - setException(e); + log.warn("Unexpected exception during server handshake", e); setCurrentState(State.Error); - setErrorMessage(e.getMessage()); - notifyObservers(); + future.completeExceptionally(new Failure(I18N.getText("Handshake.msg.unexpectedError"), e)); } } @@ -285,7 +249,7 @@ private void acceptNewPublicKey(PlayerAwaitingApproval p) { if (getEasyConnectName() == null) { return; // Protect from event being fired more than once } - setEasyConnectName(null); + easyConnectName = null; try { var playerDb = (PersistedPlayerDatabase) playerDatabase; var pl = playerDatabase.getPlayer(p.name()); @@ -297,12 +261,13 @@ private void acceptNewPublicKey(PlayerAwaitingApproval p) { playerDb.setRole(pl.getName(), p.role()); setPlayer(playerDatabase.getPlayer(pl.getName())); } - playerDb.commitChanges(); } + playerDb.commitChanges(); + var publicKeyAddedMsgBuilder = PublicKeyAddedMsg.newBuilder(); publicKeyAddedMsgBuilder.setPublicKey(p.publicKey()); - sendMessage(HandshakeMsg.newBuilder().setPublicKeyAddedMsg(publicKeyAddedMsgBuilder).build()); - setCurrentState(State.AwaitingClientInit); + var msg = HandshakeMsg.newBuilder().setPublicKeyAddedMsg(publicKeyAddedMsgBuilder).build(); + sendMessage(State.AwaitingClientInit, msg); } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException @@ -321,7 +286,6 @@ private void handle(ClientAuthMsg clientAuthMessage) InterruptedException, NoSuchPaddingException, IllegalBlockSizeException, - NoSuchAlgorithmException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { @@ -355,18 +319,16 @@ private void handle(ClientAuthMsg clientAuthMessage) } private void sendConnectionSuccessful() throws ExecutionException, InterruptedException { - var server = MapTool.getServer(); var connectionSuccessfulMsg = ConnectionSuccessfulMsg.newBuilder() - .setRoleDto(getPlayer().isGM() ? RoleDto.GM : RoleDto.PLAYER) + .setRoleDto(player.isGM() ? RoleDto.GM : RoleDto.PLAYER) .setServerPolicyDto(server.getPolicy().toDto()) .setGameDataDto(new DataStoreManager().toDto().get()) .setAddOnLibraryListDto(new LibraryManager().addOnLibrariesToDto().get()); var handshakeMsg = HandshakeMsg.newBuilder().setConnectionSuccessfulMsg(connectionSuccessfulMsg).build(); - sendMessage(handshakeMsg); - setCurrentState(State.Success); - notifyObservers(); + sendMessage(State.Success, handshakeMsg); + future.complete(player); } /** @@ -390,15 +352,12 @@ private void handle(ClientInitMsg clientInitMsg) BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { - var server = MapTool.getServer(); if (server.isPlayerConnected(clientInitMsg.getPlayerName())) { - setErrorMessage(I18N.getText("Handshake.msg.duplicateName")); sendErrorResponseAndNotify(HandshakeResponseCodeMsg.PLAYER_ALREADY_CONNECTED); return; } if (!MapTool.isDevelopment() && !MapTool.getVersion().equals(clientInitMsg.getVersion())) { - setErrorMessage(I18N.getText("Handshake.msg.wrongVersion")); sendErrorResponseAndNotify(HandshakeResponseCodeMsg.WRONG_VERSION); } @@ -407,7 +366,6 @@ private void handle(ClientInitMsg clientInitMsg) try { setPlayer(playerDatabase.getPlayer(clientInitMsg.getPlayerName())); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - setErrorMessage(I18N.getText("Handshake.msg.encodeInitFail", clientInitMsg.getPlayerName())); // Error fetching player is sent to client as invalid password intentionally. sendErrorResponseAndNotify(HandshakeResponseCodeMsg.INVALID_PASSWORD); return; @@ -419,7 +377,6 @@ private void handle(ClientInitMsg clientInitMsg) return; } // Unknown player is sent to client as invalid password intentionally. - setErrorMessage(I18N.getText("Handshake.msg.unknownPlayer", clientInitMsg.getPlayerName())); sendErrorResponseAndNotify(HandshakeResponseCodeMsg.INVALID_PASSWORD); return; } @@ -428,14 +385,12 @@ private void handle(ClientInitMsg clientInitMsg) var blockedMsg = PlayerBlockedMsg.newBuilder().setReason(playerDatabase.getBlockedReason(player)).build(); var msg = HandshakeMsg.newBuilder().setPlayerBlockedMsg(blockedMsg).build(); - sendMessage(msg); - setCurrentState(State.Error); + sendMessage(State.Error, msg); return; } if (playerDatabase.getAuthMethod(player) == AuthMethod.ASYMMETRIC_KEY) { - var state = sendAsymmetricKeyAuthType(); - setCurrentState(state); + sendAsymmetricKeyAuthType(); } else { handshakeChallenges = new HandshakeChallenge[2]; if (playerDatabase.supportsRolePasswords()) { @@ -443,7 +398,6 @@ private void handle(ClientInitMsg clientInitMsg) } else { sendSharedPasswordAuthType(); } - setCurrentState(State.AwaitingClientPasswordAuth); } } @@ -452,8 +406,8 @@ private void requestPublicKey(String playerName) { easyConnectPin = String.format("%04d", new SecureRandom().nextInt(9999)); easyConnectName = playerName; requestPublicKeyBuilder.setPin(easyConnectPin); - sendMessage(HandshakeMsg.newBuilder().setRequestPublicKeyMsg(requestPublicKeyBuilder).build()); - currentState = State.AwaitingPublicKey; + var msg = HandshakeMsg.newBuilder().setRequestPublicKeyMsg(requestPublicKeyBuilder).build(); + sendMessage(State.AwaitingPublicKey, msg); } /** @@ -490,7 +444,7 @@ private void sendSharedPasswordAuthType() .setIv(ByteString.copyFrom(iv)) .addChallenge(ByteString.copyFrom(handshakeChallenges[0].getChallenge())); var handshakeMsg = HandshakeMsg.newBuilder().setUseAuthTypeMsg(authTypeMsg).build(); - sendMessage(handshakeMsg); + sendMessage(State.AwaitingClientPasswordAuth, handshakeMsg); } /** @@ -539,13 +493,12 @@ private void sendRoleSharedPasswordAuthType() .addChallenge( ByteString.copyFrom(handshakeChallenges[PLAYER_CHALLENGE].getChallenge())); var handshakeMsg = HandshakeMsg.newBuilder().setUseAuthTypeMsg(authTypeMsg).build(); - sendMessage(handshakeMsg); + sendMessage(State.AwaitingClientPasswordAuth, handshakeMsg); } /** * Send the authentication type message when using asymmetric keys * - * @return the new state for the state machine. * @throws ExecutionException when there is an error fetching the public key. * @throws InterruptedException when there is an error fetching the public key. * @throws NoSuchPaddingException when there is an error during encryption. @@ -554,7 +507,7 @@ private void sendRoleSharedPasswordAuthType() * @throws BadPaddingException when there is an error during encryption. * @throws InvalidKeyException when there is an error during encryption. */ - private State sendAsymmetricKeyAuthType() + private void sendAsymmetricKeyAuthType() throws ExecutionException, InterruptedException, NoSuchPaddingException, @@ -570,7 +523,6 @@ private State sendAsymmetricKeyAuthType() } else { sendErrorResponseAndNotify(HandshakeResponseCodeMsg.INVALID_PUBLIC_KEY); } - return State.AwaitingPublicKey; } CipherUtil.Key publicKey = playerDatabase.getPublicKey(player, playerPublicKeyMD5).get(); String password = new PasswordGenerator().getPassword(); @@ -582,39 +534,12 @@ private State sendAsymmetricKeyAuthType() .setAuthType(AuthTypeEnum.ASYMMETRIC_KEY) .addChallenge(ByteString.copyFrom(handshakeChallenges[0].getChallenge())); var handshakeMsg = HandshakeMsg.newBuilder().setUseAuthTypeMsg(authTypeMsg).build(); - sendMessage(handshakeMsg); - return State.AwaitingClientPublicKeyAuth; - } - - /** - * Adds an observer to the handshake process. - * - * @param observer the observer of the handshake process. - */ - public synchronized void addObserver(HandshakeObserver observer) { - observerList.add(observer); - } - - /** - * Removes an observer from the handshake process. - * - * @param observer the observer of the handshake process. - */ - public synchronized void removeObserver(HandshakeObserver observer) { - observerList.remove(observer); - } - - /** Notifies observers that the handshake has completed or errored out.. */ - private synchronized void notifyObservers() { - if (getEasyConnectName() != null) { - SwingUtilities.invokeLater( - () -> MapTool.getFrame().getConnectionPanel().removeAwaitingApproval(easyConnectName)); - } - for (var observer : observerList) observer.onCompleted(this); + sendMessage(State.AwaitingClientPublicKeyAuth, handshakeMsg); } @Override public void startHandshake() { + connection.addMessageHandler(this); setCurrentState(State.AwaitingClientInit); } diff --git a/src/main/java/net/rptools/maptool/server/ServerMessageHandler.java b/src/main/java/net/rptools/maptool/server/ServerMessageHandler.java index c181e141f2..df66daae4c 100644 --- a/src/main/java/net/rptools/maptool/server/ServerMessageHandler.java +++ b/src/main/java/net/rptools/maptool/server/ServerMessageHandler.java @@ -282,7 +282,6 @@ private void handle(UpdateGmMacrosMsg msg) { msg.getMacrosList().stream() .map(MacroButtonProperties::fromDto) .collect(Collectors.toList()); - MapTool.getCampaign().setGmMacroButtonPropertiesArray(campaignMacros); server.getCampaign().setGmMacroButtonPropertiesArray(campaignMacros); }); } @@ -294,7 +293,6 @@ private void handle(UpdateCampaignMacrosMsg msg) { msg.getMacrosList().stream() .map(MacroButtonProperties::fromDto) .collect(Collectors.toList()); - MapTool.getCampaign().setMacroButtonPropertiesArray(campaignMacros); server.getCampaign().setMacroButtonPropertiesArray(campaignMacros); }); } @@ -416,10 +414,12 @@ private void handle(SetZoneGridSizeMsg msg) { EventQueue.invokeLater( () -> { Zone zone = server.getCampaign().getZone(GUID.valueOf(msg.getZoneGuid())); - Grid grid = zone.getGrid(); - grid.setSize(msg.getSize()); - grid.setOffset(msg.getXOffset(), msg.getYOffset()); - zone.setGridColor(msg.getColor()); + if (zone != null) { + Grid grid = zone.getGrid(); + grid.setSize(msg.getSize()); + grid.setOffset(msg.getXOffset(), msg.getYOffset()); + zone.setGridColor(msg.getColor()); + } }); } @@ -690,7 +690,7 @@ private void handle(AddTopologyMsg addTopologyMsg) { private void handle(BootPlayerMsg bootPlayerMsg) { // And just to be sure, remove them from the server - server.releaseClientConnection(server.getConnectionId(bootPlayerMsg.getPlayerName())); + server.bootPlayer(bootPlayerMsg.getPlayerName()); } private void handle(String id, UpdatePlayerStatusMsg updatePlayerStatusMsg) { @@ -704,11 +704,11 @@ private void handle(String id, UpdatePlayerStatusMsg updatePlayerStatusMsg) { } private void sendToClients(String excludedId, Message message) { - server.getConnection().broadcastMessage(new String[] {excludedId}, message); + server.broadcastMessage(new String[] {excludedId}, message); } private void sendToAllClients(Message message) { - server.getConnection().broadcastMessage(message); + server.broadcastMessage(message); } private void bringTokensToFront(GUID zoneGUID, Set tokenSet) { @@ -750,12 +750,10 @@ private void getAsset(String id, MD5Key assetID) { AssetManager.getAssetInfo(assetID).getProperty(AssetManager.NAME), AssetManager.getAssetCacheFile(assetID)); var msg = StartAssetTransferMsg.newBuilder().setHeader(producer.getHeader().toDto()); - server - .getConnection() - .sendMessage( - id, - MapToolConstants.Channel.IMAGE, - Message.newBuilder().setStartAssetTransferMsg(msg).build()); + server.sendMessage( + id, + MapToolConstants.Channel.IMAGE, + Message.newBuilder().setStartAssetTransferMsg(msg).build()); server.addAssetProducer(id, producer); } catch (IllegalArgumentException iae) { @@ -764,14 +762,14 @@ private void getAsset(String id, MD5Key assetID) { // image instead of blowing up Asset asset = Asset.createBrokenImageAsset(assetID); var msg = PutAssetMsg.newBuilder().setAsset(asset.toDto()); - server.getConnection().sendMessage(id, Message.newBuilder().setPutAssetMsg(msg).build()); + server.sendMessage(id, Message.newBuilder().setPutAssetMsg(msg).build()); } } private void getZone(String id, GUID zoneGUID) { var zone = server.getCampaign().getZone(zoneGUID); var msg = PutZoneMsg.newBuilder().setZone(zone.toDto()); - server.getConnection().sendMessage(id, Message.newBuilder().setPutZoneMsg(msg).build()); + server.sendMessage(id, Message.newBuilder().setPutZoneMsg(msg).build()); } private void putToken(String clientId, GUID zoneGUID, Token token) { @@ -793,9 +791,7 @@ private void putToken(String clientId, GUID zoneGUID, Token token) { .setTokenGuid(token.getId().toString()) .setProperty(TokenUpdateDto.valueOf(Token.Update.setZOrder.name())) .addValues(0, TokenPropertyValueDto.newBuilder().setIntValue(zOrder)); - server - .getConnection() - .sendMessage(clientId, Message.newBuilder().setUpdateTokenPropertyMsg(msg).build()); + server.sendMessage(clientId, Message.newBuilder().setUpdateTokenPropertyMsg(msg).build()); } } diff --git a/src/main/java/net/rptools/maptool/server/ServerPolicy.java b/src/main/java/net/rptools/maptool/server/ServerPolicy.java index a46080d5b5..b4cdb80530 100644 --- a/src/main/java/net/rptools/maptool/server/ServerPolicy.java +++ b/src/main/java/net/rptools/maptool/server/ServerPolicy.java @@ -53,6 +53,26 @@ public ServerPolicy() { movementMetric = AppPreferences.getMovementMetric(); } + public ServerPolicy(ServerPolicy other) { + this.strictTokenMovement = other.strictTokenMovement; + this.isMovementLocked = other.isMovementLocked; + this.isTokenEditorLocked = other.isTokenEditorLocked; + this.playersCanRevealVision = other.playersCanRevealVision; + this.gmRevealsVisionForUnownedTokens = other.gmRevealsVisionForUnownedTokens; + this.useIndividualViews = other.useIndividualViews; + this.restrictedImpersonation = other.restrictedImpersonation; + this.playersReceiveCampaignMacros = other.playersReceiveCampaignMacros; + this.useToolTipsForDefaultRollFormat = other.useToolTipsForDefaultRollFormat; + this.useIndividualFOW = other.useIndividualFOW; + this.isAutoRevealOnMovement = other.isAutoRevealOnMovement; + this.includeOwnedNPCs = other.includeOwnedNPCs; + this.movementMetric = other.movementMetric; + this.hidemapselectui = other.hidemapselectui; + this.disablePlayerAssetPanel = other.disablePlayerAssetPanel; + this.useAstarPathfinding = other.useAstarPathfinding; + this.vblBlocksMove = other.vblBlocksMove; + } + /** * Whether token management can be done by everyone or only the GM and assigned tokens * @@ -164,7 +184,7 @@ public boolean getUseToolTipsForDefaultRollFormat() { * @return the current server time as the difference, measured in milliseconds, between the now * and midnight, January 1, 1970 UTC */ - public long getSystemTime() { + private long getSystemTime() { return System.currentTimeMillis(); } @@ -174,7 +194,7 @@ private String getLocalTimeDate() { return sdf.format(cal.getTime()); } - public String getTimeDate() { + private String getTimeDate() { return getLocalTimeDate(); } diff --git a/src/main/java/net/rptools/maptool/util/PersistenceUtil.java b/src/main/java/net/rptools/maptool/util/PersistenceUtil.java index 21254eb2b4..d570cecd72 100644 --- a/src/main/java/net/rptools/maptool/util/PersistenceUtil.java +++ b/src/main/java/net/rptools/maptool/util/PersistenceUtil.java @@ -320,6 +320,9 @@ public static PersistedMap loadMap(File mapFile) throws IOException { z.setName(n); z.imported(); // Resets creation timestamp and init panel, among other things z.optimize(); // Collapses overlaid or redundant drawables + + // Make sure the imported zone is as fresh as possible (new IDs all the way down). + persistedMap.zone = new Zone(z, false); } else { // TODO: Not a map but it is something with a property.xml file in it. // Should we have a filetype property in there? diff --git a/src/main/java/net/rptools/maptool/util/cipher/CipherUtil.java b/src/main/java/net/rptools/maptool/util/cipher/CipherUtil.java index 72854b647e..af604f448f 100644 --- a/src/main/java/net/rptools/maptool/util/cipher/CipherUtil.java +++ b/src/main/java/net/rptools/maptool/util/cipher/CipherUtil.java @@ -36,7 +36,6 @@ import javax.crypto.spec.SecretKeySpec; import net.rptools.lib.MD5Key; import net.rptools.maptool.language.I18N; -import net.rptools.maptool.server.MapToolServerConnection; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -56,7 +55,7 @@ public class CipherUtil { private static final String MESSAGE_DIGEST_ALGORITHM = "SHA3-256"; /** Logger used for log messages. */ - private static final Logger log = LogManager.getLogger(MapToolServerConnection.class); + private static final Logger log = LogManager.getLogger(CipherUtil.class); /** The length of the generated key. */ public static final int DEFAULT_GENERATED_KEY_LEN = 128; diff --git a/src/main/java/net/rptools/maptool/webapi/MTWebAppServer.java b/src/main/java/net/rptools/maptool/webapi/MTWebAppServer.java deleted file mode 100644 index 69166d6cb7..0000000000 --- a/src/main/java/net/rptools/maptool/webapi/MTWebAppServer.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.webapi; - -import com.google.gson.JsonObject; -import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import net.rptools.maptool.client.MapTool; -import net.rptools.maptool.language.I18N; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.server.handler.HandlerList; -import org.eclipse.jetty.server.handler.ResourceHandler; -import org.eclipse.jetty.util.log.StdErrLog; -import org.eclipse.jetty.webapp.WebAppContext; -import org.eclipse.jetty.websocket.server.WebSocketHandler; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; - -public class MTWebAppServer { - public static final String WEBAPP_CONTEXT_PATH = "webapi"; - - /** The port to listen on. */ - private int port = 8000; - - /** The embedded Jetty server we will use. */ - private Server server = null; - - /** Has the server been started. */ - private boolean started = false; - - /** Extra resource directories. */ - private Map resourceDirs = new HashMap<>(); - - public synchronized void addResourceDir(String contextPath, String dirname) { - if (started) { - System.err.println("Can not add a resource directory to a running Webapp server."); - throw new IllegalArgumentException( - "Can not add a resource directory to a running Webapp server."); - } - resourceDirs.put(contextPath, dirname); - } - - /** - * Sets the port - * - * @param port the port for the server - */ - public synchronized void setPort(int port) { - if (started) { - System.err.println("Can not change port of running Webapp server."); - throw new IllegalArgumentException("Can not change port of running Webapp server."); - } - this.port = port; - } - - /** Starts the server at the specified port. */ - public synchronized void startServer() { - if (started) { - return; // Do nothing as its already running. - } - - System.out.println("DEBUG: Starting server on port " + this.port + "..."); - server = new org.eclipse.jetty.server.Server(this.port); - - // set up the web socket handler - ContextHandler contextHandler = new ContextHandler(); - contextHandler.setContextPath("/ws"); - contextHandler.setHandler( - new WebSocketHandler() { - - @Override - public void configure(WebSocketServletFactory factory) { - factory.setCreator( - (req, resp) -> { - String query = req.getRequestURI().toString(); - if ((query == null) || (query.length() <= 0)) { - try { - resp.sendForbidden("DEBUG: Unspecified query"); - } catch (IOException e) { - - } - - return null; - } - - return new MTWebSocket(); - }); - } - }); - - WebAppContext webAppContext = new WebAppContext(); - webAppContext.setResourceBase( - this.getClass().getResource("/net/rptools/maptool/webapp").toExternalForm()); - webAppContext.setContextPath("/" + WEBAPP_CONTEXT_PATH); - webAppContext.setLogger(new StdErrLog()); - - HandlerList handlers = new HandlerList(); - - handlers.addHandler(contextHandler); - handlers.addHandler(webAppContext); - - for (Map.Entry res : resourceDirs.entrySet()) { - ContextHandler context = new ContextHandler(); - context.setWelcomeFiles(new String[] {"index.html"}); - context.setContextPath(res.getKey()); - - ResourceHandler resourceHandler = new ResourceHandler(); - resourceHandler.setDirectoriesListed(true); - resourceHandler.setWelcomeFiles(new String[] {"index.html"}); - resourceHandler.setResourceBase(res.getValue()); - - context.setHandler(resourceHandler); - handlers.addHandler(context); - - System.err.println("DEBUG: name=" + res.getKey() + ": value=" + res.getValue()); - } - - ContextHandler tokenImageContext = new ContextHandler(); - tokenImageContext.setContextPath("/token"); - tokenImageContext.setLogger(new StdErrLog()); - tokenImageContext.setHandler(new TokenImageHandler()); - - handlers.addHandler(tokenImageContext); - - server.setHandler(handlers); - - try { - server.start(); - } catch (Exception e) { - - } - System.out.println("DEBUG: Started."); - started = true; - - // Set up the heartbeat - ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor(); - - ses.scheduleAtFixedRate( - () -> { - JsonObject data = new JsonObject(); - MTWebClientManager.getInstance().sendToAllSessions("keepalive", data); - }, - 1, - 1, - TimeUnit.MINUTES); - - try { - String address = InetAddress.getLocalHost().getHostAddress(); - String portString = Integer.toString(port); - MapTool.addLocalMessage( - I18N.getText("webapp.serverStarted", address, portString, WEBAPP_CONTEXT_PATH)); - } catch (UnknownHostException e) { - e.printStackTrace(); - // FIXME: log this error - } - } - - public boolean hasStarted() { - return started; - } -} diff --git a/src/main/java/net/rptools/maptool/webapi/MTWebClientManager.java b/src/main/java/net/rptools/maptool/webapi/MTWebClientManager.java deleted file mode 100644 index 79d1fb911c..0000000000 --- a/src/main/java/net/rptools/maptool/webapi/MTWebClientManager.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.webapi; - -import com.google.gson.JsonObject; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -public class MTWebClientManager { - - /** Create the singleton instance. */ - private static final MTWebClientManager instance = new MTWebClientManager(); - - /** The connected clients. */ - private Set clientSockets = Collections.synchronizedSet(new HashSet()); - - /** Create a new MTWebClientManager. */ - private MTWebClientManager() {} - - /** - * Returns the singleton instance of MTWebClientManager. - * - * @return the instance of MTWebClientManager. - */ - public static MTWebClientManager getInstance() { - return instance; - } - - /** - * Return the sessions for the clients that are connected. - * - * @return the sessions for the clients that are connected. - */ - Collection getClientSessions() { - return Collections.unmodifiableCollection(clientSockets); - } - - /** - * Sends a message to all sessions. - * - * @param messageType the type of the message. - * @param data the data to send - */ - public void sendToAllSessions(String messageType, JsonObject data) { - for (MTWebSocket ws : clientSockets) { - ws.sendMessage(messageType, data); - } - } - - /** - * Adds a client to the list of clients being managed. - * - * @param wcs The web socket of the client. - */ - void addClient(MTWebSocket wcs) { - clientSockets.add(wcs); - sendInitialInfo(wcs); - } - - /** - * Removes a client from the list of clients being managed. - * - * @param wcs The web socket of the client. - */ - void removeClient(MTWebSocket wcs) { - clientSockets.remove(wcs); - } - - /** - * Send the initiative information to the specified client. - * - * @param wcs the web socket of the client. - */ - void sendInitialInfo(MTWebSocket wcs) { - WebAppInitiative.getInstance().sendInitiative(wcs); - } -} diff --git a/src/main/java/net/rptools/maptool/webapi/MTWebSocket.java b/src/main/java/net/rptools/maptool/webapi/MTWebSocket.java deleted file mode 100644 index da276aee7e..0000000000 --- a/src/main/java/net/rptools/maptool/webapi/MTWebSocket.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.webapi; - -import com.google.gson.JsonObject; -import java.io.IOException; -import net.rptools.maptool.client.functions.json.JSONMacroFunctions; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.WebSocketAdapter; - -public class MTWebSocket extends WebSocketAdapter { - - /** The Session of this socket. */ - private Session session; - - @Override - /** - * Adds the session to the chatroom participants list, and sends back to the user the last three - * messages in the conversation. - */ - public void onWebSocketConnect(Session session) { - System.out.println("DEBUG: Websocket Connect from " + session.getRemoteAddress().getAddress()); - this.session = session; - System.out.println("DEBUG: Connected"); - MTWebClientManager.getInstance().addClient(this); - } - - @Override - public void onWebSocketBinary(byte[] bytes, int x, int y) { - // not used - } - - @Override - public void onWebSocketText(String message) { - System.out.println("DEBUG: Got Message" + message); - // FIXME: need to test this is valid - try { - JsonObject json = JSONMacroFunctions.getInstance().asJsonElement(message).getAsJsonObject(); - String messageType = json.get("messageType").getAsString(); - String messageId = json.get("messageId").getAsString(); - JsonObject data = json.get("data").getAsJsonObject(); - - if ("initiative".equals(messageType)) { - System.out.println("DEBUG: Got an initiative message"); - WebAppInitiative.getInstance().processInitiativeMessage(data); - } else if ("tokenInfo".equals(messageType) || "tokenProperties".equals(messageType)) { - WebTokenInfo.getInstance().sendTokenInfo(this, messageId, data); - } else if ("macro".equals(messageType)) { - WebTokenInfo.getInstance().processMacro(data); - } else if ("setProperties".equals(messageType)) { - WebTokenInfo.getInstance().processSetProperties(data); - } - } catch (Exception e) { - e.printStackTrace(); // FIXME: fix this to deal with error properly. - } - } - - @Override - public void onWebSocketError(Throwable cause) { - System.out.println("DEBUG: Websocket Error " + cause.getMessage()); - System.out.println( - "DEBUG: number connections = " - + MTWebClientManager.getInstance().getClientSessions().size()); - MTWebClientManager.getInstance().removeClient(this); - } - - @Override - public void onWebSocketClose(int statusCode, String reason) { - System.out.println("DEBUG: Websocket Close from " + session.getRemoteAddress().getAddress()); - MTWebClientManager.getInstance().removeClient(this); - } - - void sendMessage(String messageType, JsonObject data) { - sendMessage(messageType, null, data); - } - - /** - * Sends a message to the client. - * - * @param messageType The type of the message. - * @param inResponseTo The message this is a response to. - * @param data The data in the message. - */ - void sendMessage(String messageType, String inResponseTo, JsonObject data) { - JsonObject message = new JsonObject(); - message.addProperty("messageType", messageType); - message.add("data", data); - if (inResponseTo != null) { - message.addProperty("inResponseTo", inResponseTo); - } - - try { - session.getRemote().sendString(message.toString()); - System.out.println("DEBUG: Wrote: " + message.toString()); - } catch (IOException ioe) { - System.out.println("DEBUG: websocket write error."); - System.out.println( - "DEBUG: number connections = " - + MTWebClientManager.getInstance().getClientSessions().size()); - MTWebClientManager.getInstance().removeClient(this); - } - } -} diff --git a/src/main/java/net/rptools/maptool/webapi/TokenImageHandler.java b/src/main/java/net/rptools/maptool/webapi/TokenImageHandler.java deleted file mode 100644 index 6fe6feb51c..0000000000 --- a/src/main/java/net/rptools/maptool/webapi/TokenImageHandler.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.webapi; - -import java.io.IOException; -import javax.imageio.ImageIO; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import net.rptools.maptool.model.Asset; -import net.rptools.maptool.model.AssetManager; -import net.rptools.maptool.model.Token; -import net.rptools.maptool.util.ImageManager; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.handler.AbstractHandler; - -public class TokenImageHandler extends AbstractHandler { - @Override - public void handle( - String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) - throws IOException { - - String args[] = target.replaceAll("^/", "").split("/"); - - for (String s : args) { - System.out.println("DEBUG: ARGS: " + s); - } - - if ("portrait".equalsIgnoreCase(args[0])) { - baseRequest.setHandled(sendPortrait(response, args[1])); - } else if ("image".equalsIgnoreCase(args[0])) { - baseRequest.setHandled(sendImage(response, args[1])); - } else if ("portraitOrImage".equalsIgnoreCase(args[0])) { - baseRequest.setHandled(sendPortraitOrImage(response, args[1])); - } - - /* - * response.setContentType("text/html; charset=utf-8"); response.setStatus(HttpServletResponse.SC_OK); - * - * PrintWriter out = response.getWriter(); - * - * baseRequest.setHandled(true); - */ - } - - private boolean sendImage(HttpServletResponse response, String tokenId) throws IOException { - System.out.println("DEBUG: Here (> 0) as well"); - Token token = WebTokenInfo.getInstance().findTokenFromId(tokenId); - if (token == null) { - return false; - // FIXME: log this error - } - - if (token.getImageAssetId() == null) { - response.setContentType("image/png"); - ImageIO.write(ImageManager.BROKEN_IMAGE, "png", response.getOutputStream()); - response.setStatus(HttpServletResponse.SC_OK); - } else { - Asset asset = AssetManager.getAsset(token.getImageAssetId()); - byte[] image = asset.getData(); - response.setContentType("image/" + asset.getExtension()); - response.setStatus(HttpServletResponse.SC_OK); - - response.setContentLength(image.length); - response.getOutputStream().write(image); - } - - return true; - } - - private boolean sendPortrait(HttpServletResponse response, String tokenId) throws IOException { - - System.out.println("DEBUG: Here (> 0) as well"); - Token token = WebTokenInfo.getInstance().findTokenFromId(tokenId); - if (token == null) { - return false; - // FIXME: log this error - } - - if (token.getPortraitImage() == null) { - response.setContentType("image/png"); - ImageIO.write(ImageManager.BROKEN_IMAGE, "png", response.getOutputStream()); - response.setStatus(HttpServletResponse.SC_OK); - } else { - Asset asset = AssetManager.getAsset(token.getPortraitImage()); - byte[] image = asset.getData(); - response.setContentType("image/" + asset.getExtension()); - response.setStatus(HttpServletResponse.SC_OK); - - response.setContentLength(image.length); - response.getOutputStream().write(image); - } - - return true; - } - - private boolean sendPortraitOrImage(HttpServletResponse response, String tokenId) - throws IOException { - - System.out.println("DEBUG: Here (> 0) as well"); - Token token = WebTokenInfo.getInstance().findTokenFromId(tokenId); - if (token == null) { - return false; - // FIXME: log this error - } - - Asset asset = null; - if (token.getPortraitImage() != null) { - asset = AssetManager.getAsset(token.getPortraitImage()); - } else if (token.getImageAssetId() != null) { - asset = AssetManager.getAsset(token.getImageAssetId()); - } - - if (asset != null) { - byte[] image = asset.getData(); - response.setContentType("image/" + asset.getExtension()); - response.setStatus(HttpServletResponse.SC_OK); - - response.setContentLength(image.length); - response.getOutputStream().write(image); - } else { - response.setContentType("image/png"); - ImageIO.write(ImageManager.BROKEN_IMAGE, "png", response.getOutputStream()); - response.setStatus(HttpServletResponse.SC_OK); - } - - return true; - } -} diff --git a/src/main/java/net/rptools/maptool/webapi/WebAppInitiative.java b/src/main/java/net/rptools/maptool/webapi/WebAppInitiative.java deleted file mode 100644 index e8dbeccf0e..0000000000 --- a/src/main/java/net/rptools/maptool/webapi/WebAppInitiative.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.webapi; - -import com.google.common.eventbus.Subscribe; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.util.List; -import javax.swing.*; -import net.rptools.maptool.client.AppUtil; -import net.rptools.maptool.client.MapTool; -import net.rptools.maptool.client.events.ZoneActivated; -import net.rptools.maptool.client.ui.tokenpanel.InitiativePanel; -import net.rptools.maptool.events.MapToolEventBus; -import net.rptools.maptool.model.*; -import net.rptools.maptool.model.zones.InitiativeListChanged; - -public class WebAppInitiative { - - private static final WebAppInitiative instance = new WebAppInitiative(); - - private class InitiativeListener implements PropertyChangeListener { - - private InitiativeList initiativeList; - private Zone zone; - - private void setList(InitiativeList ilist) { - if (initiativeList != null) { - initiativeList.removePropertyChangeListener(this); - } - initiativeList = ilist; - initiativeList.addPropertyChangeListener(this); - } - - private void setZone(Zone z) { - setList(z.getInitiativeList()); - zone = z; - } - - @Override - public void propertyChange(PropertyChangeEvent evt) { - // This should really be run in a separate thread, but to do that I need to work out - // concurrency issues. - sendInitiative(); - System.out.println("Here!"); - } - - @Subscribe - void onInitiativeListChanged(InitiativeListChanged event) { - final var list = event.initiativeList(); - if (list.getZone() != zone) { - return; - } - - setList(list); - sendInitiative(); - } - - @Subscribe - void onZoneActivated(ZoneActivated event) { - setZone(event.zone()); - sendInitiative(); - } - - public void updateListeners() { - setZone(MapTool.getFrame().getCurrentZoneRenderer().getZone()); - } - } - - private InitiativeListener initiativeListener; - - public static WebAppInitiative getInstance() { - return instance; - } - - private WebAppInitiative() { - initiativeListener = new InitiativeListener(); - SwingUtilities.invokeLater( - () -> { - new MapToolEventBus().getMainEventBus().register(this); - initiativeListener.updateListeners(); - System.out.println("Here..."); - }); - } - - private JsonObject getInitiativeDetails() { - JsonObject json = new JsonObject(); - InitiativeList initiativeList = - MapTool.getFrame().getCurrentZoneRenderer().getZone().getInitiativeList(); - - List tokenInitList = - initiativeList.getTokens(); - - JsonArray tokArray = new JsonArray(); - int index = 0; - for (InitiativeList.TokenInitiative token : tokenInitList) { - if (InitiativeListModel.isTokenVisible(token.getToken(), initiativeList.isHideNPC())) { - JsonObject tokJSon = new JsonObject(); - tokJSon.addProperty("id", token.getToken().getId().toString()); - tokJSon.addProperty("name", token.getToken().getName()); - tokJSon.addProperty("holding", token.isHolding()); - if (token.getState() != null) { - tokJSon.addProperty("initiative", token.getState()); - } - tokJSon.addProperty("tokenIndex", index); - /* - * if (AppUtil.playerOwns(token.getToken())) { tokJSon.put("playerOwns", "true"); } else { tokJSon.put("playerOwns", "false"); } - */ - tokJSon.addProperty("playerOwns", AppUtil.playerOwns(token.getToken())); - tokArray.add(tokJSon); - } - index++; - } - - json.add("initiative", tokArray); - json.addProperty("current", initiativeList.getCurrent()); - json.addProperty("round", initiativeList.getRound()); - json.addProperty("canAdvance", canAdvanceInitiative()); - - return json; - } - - void sendInitiative(MTWebSocket mtws) { - mtws.sendMessage("initiative", getInitiativeDetails()); - } - - void sendInitiative(MTWebSocket mtws, String inReponseTo) { - mtws.sendMessage("initiative", inReponseTo, getInitiativeDetails()); - } - - void sendInitiative() { - JsonObject init = getInitiativeDetails(); - MTWebClientManager.getInstance().sendToAllSessions("initiative", init); - } - - void processInitiativeMessage(JsonObject json) { - InitiativeList ilist = initiativeListener.initiativeList; - String currentInit = Integer.toString(ilist.getCurrent()); - String currentRound = Integer.toString(ilist.getRound()); - - // If there is a mismatch between client and us don't perform the command. This can happen - // because multiple - // people can be updating the initiative at the same time and its possible that two people might - // update the - // initiative at the same time. - if (!currentInit.equals(json.get("currentInitiative").getAsString()) - || !currentRound.equals(json.get("currentRound").getAsString())) { - return; // FIXME: This needs to send a "can not do" message back to client. - } - - String command = json.get("command").getAsString(); - if ("nextInitiative".equals(command)) { - System.out.println("DEBUG: got next initiative call" + json.toString()); - - if (canAdvanceInitiative()) { // Trust a web client? You gotta be joking :) - ilist.nextInitiative(); - } - } else if ("previousInitiative".equals(command)) { - if (canAdvanceInitiative()) { // Trust a web client? You gotta be joking :) - ilist.prevInitiative(); - } - } else if ("sortInitiative".equals(command)) { - ilist.sort(); - } else if ("toggleHoldInitiative".equals(command)) { - InitiativeList.TokenInitiative tokenInit = null; - GUID id = new GUID(json.get("token").getAsString()); - int tokenIndex = json.get("tokenIndex").getAsInt(); - int index = 0; - for (InitiativeList.TokenInitiative ti : ilist.getTokens()) { - if (ti.getId().equals(id) && tokenIndex == index) { - System.out.println("DEBUG: Here, index = " + index + ", id = " + ti.getId()); - tokenInit = ti; - break; - } - index++; - } - - if (tokenInit == null) { - // FIXME: need to log this. - } else { - System.out.println("DEBUG: Here, changing holding on " + tokenInit.getToken().getName()); - tokenInit.setHolding(!tokenInit.isHolding()); - } - - } else { - // FIXME: need to log this. - } - } - - // TODO: When the whole of initiative functionality is fixed up then this needs to be rolled into - // that. - private boolean canAdvanceInitiative() { - InitiativePanel ipanel = MapTool.getFrame().getInitiativePanel(); - if (ipanel.hasGMPermission()) { - return true; - } - - InitiativeList ilist = initiativeListener.initiativeList; - - return ipanel.hasOwnerPermission(ilist.getTokenInitiative(ilist.getCurrent()).getToken()); - } -} diff --git a/src/main/java/net/rptools/maptool/webapi/WebTokenInfo.java b/src/main/java/net/rptools/maptool/webapi/WebTokenInfo.java deleted file mode 100644 index 8fc98a184a..0000000000 --- a/src/main/java/net/rptools/maptool/webapi/WebTokenInfo.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * This software Copyright by the RPTools.net development team, and - * licensed under the Affero GPL Version 3 or, at your option, any later - * version. - * - * MapTool Source Code 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. - * - * You should have received a copy of the GNU Affero General Public - * License * along with this source Code. If not, please visit - * and specifically the Affero license - * text at . - */ -package net.rptools.maptool.webapi; - -import com.google.common.eventbus.Subscribe; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import java.awt.*; -import java.util.*; -import java.util.List; -import net.rptools.maptool.client.MapTool; -import net.rptools.maptool.client.functions.json.JSONMacroFunctions; -import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer; -import net.rptools.maptool.events.MapToolEventBus; -import net.rptools.maptool.model.*; -import net.rptools.maptool.model.zones.TokensAdded; -import net.rptools.maptool.model.zones.TokensChanged; -import net.rptools.maptool.model.zones.TokensRemoved; - -public class WebTokenInfo { - - private static final WebTokenInfo instance = new WebTokenInfo(); - - private WebTokenInfo() { - // Add listener for new zones. - new MapToolEventBus().getMainEventBus().register(this); - } - - @Subscribe - private void onTokensAdded(TokensAdded event) { - for (final var token : event.tokens()) { - tokenAdded(token); - } - } - - @Subscribe - private void onTokensRemoved(TokensRemoved event) { - for (final var token : event.tokens()) { - tokenRemoved(token); - } - } - - @Subscribe - private void onTokensChanged(TokensChanged event) { - for (final var token : event.tokens()) { - tokenChanged(token); - } - } - - private void tokenChanged(Token token) { - JsonObject jobj = new JsonObject(); - JsonArray tokenArray = new JsonArray(); - tokenArray.add(token.getId().toString()); - jobj.add("tokensChanged", tokenArray); - - MTWebClientManager.getInstance().sendToAllSessions("token-update", jobj); - } - - private void tokenAdded(Token token) { - JsonObject jobj = new JsonObject(); - JsonArray tokenArray = new JsonArray(); - tokenArray.add(token.getId().toString()); - jobj.add("tokensAdded", tokenArray); - - MTWebClientManager.getInstance().sendToAllSessions("token-update", jobj); - } - - private void tokenRemoved(Token token) { - JsonObject jobj = new JsonObject(); - JsonArray tokenArray = new JsonArray(); - tokenArray.add(token.getId().toString()); - jobj.add("tokensRemoved", tokenArray); - - MTWebClientManager.getInstance().sendToAllSessions("token-update", jobj); - } - - public static WebTokenInfo getInstance() { - return instance; - } - - public Token findTokenFromId(String tokenId) { - System.out.println("DEBUG: tokenId = " + tokenId); - final GUID id = new GUID(tokenId); - - final List tokenList = new ArrayList<>(); - - List zrenderers = MapTool.getFrame().getZoneRenderers(); - for (ZoneRenderer zr : zrenderers) { - tokenList.addAll(zr.getZone().getTokensFiltered(t -> t.getId().equals(id))); - - if (tokenList.size() > 0) { - break; - } - } - - if (tokenList.size() > 0) { - return tokenList.get(0); - } else { - return null; - } - } - - private Zone findZoneTokenIsOn(Token token) { - List zrenderers = MapTool.getFrame().getZoneRenderers(); - for (ZoneRenderer zr : zrenderers) { - if (zr.getZone().getAllTokens().contains(token)) { - return zr.getZone(); - } - } - - return null; - } - - void sendTokenInfo(MTWebSocket mtws, String inResponseTo, JsonObject data) { - - if (data.has("propertyNames")) { - sendTokenProperties(mtws, inResponseTo, data); - } else { - sendTokenRegisterdProperties(mtws, inResponseTo, data); - } - } - - String getTokenValue(Token token, String name) { - if (":name".equalsIgnoreCase(name)) { - return token.getName(); - } else if (":notes".equalsIgnoreCase(name)) { - return token.getNotes(); - } else if (":label".equalsIgnoreCase(name)) { - return token.getLabel(); - } - - return ""; - } - - void sendTokenProperties(MTWebSocket mtws, String inResponseTo, JsonObject data) { - String tokenId = data.get("tokenId").getAsString(); - Token token = findTokenFromId(tokenId); - - if (token == null) { - System.out.println("DEBUG: sendTokenInfo(): Unable to find token " + tokenId); - return; - // FIXME: log this error - } - - JsonObject jobj = new JsonObject(); - jobj.addProperty("tokenId", tokenId); - - JsonArray properties = new JsonArray(); - JsonObject propertiesMap = new JsonObject(); - - JsonArray propToFetch = data.get("propertyNames").getAsJsonArray(); - for (int i = 0; i < propToFetch.size(); i++) { - String pname = propToFetch.get(i).getAsString(); - String val; - if (pname.startsWith(":")) { - val = getTokenValue(token, pname); - } else { - - val = token.getProperty(pname) == null ? null : token.getProperty(pname).toString(); - } - JsonObject jprop = new JsonObject(); - jprop.addProperty("name", pname); - jprop.addProperty("value", val); - properties.add(jprop); - propertiesMap.addProperty(pname, val); - } - - jobj.add("properties", properties); - jobj.add("propertiesMap", propertiesMap); - - mtws.sendMessage("tokenProperties", inResponseTo, jobj); - } - - void sendTokenRegisterdProperties(MTWebSocket mtws, String inResponseTo, JsonObject data) { - String tokenId = data.get("tokenId").getAsString(); - Token token = findTokenFromId(tokenId); - - if (token == null) { - System.out.println("DEBUG: sendTokenInfo(): Unable to find token " + tokenId); - return; - // FIXME: log this error - } - - JsonObject jobj = new JsonObject(); - jobj.addProperty("tokenId", tokenId); - jobj.addProperty("name", token.getName()); - if (token.getLabel() != null) { - jobj.addProperty("label", token.getLabel()); - } - if (token.getNotes() != null) { - jobj.addProperty("notes", token.getNotes()); - } - - JsonObject jprop = new JsonObject(); - - for (TokenProperty tp : MapTool.getCampaign().getTokenPropertyList(token.getPropertyType())) { - JsonObject jp = new JsonObject(); - jp.addProperty("name", tp.getName()); - if (tp.getShortName() != null) { - jp.addProperty("shortName", tp.getShortName()); - } - if (tp.getDefaultValue() != null) { - jp.addProperty("defaultValue", tp.getDefaultValue()); - } - Object property = token.getProperty(tp.getName()); - if (property != null) { - jp.add("value", JSONMacroFunctions.getInstance().asJsonElement(property)); - } - jp.addProperty("showOnStatSheet", tp.isShowOnStatSheet()); - - jprop.add(tp.getName(), jp); - } - - jobj.add("properties", jprop); - - JsonArray jmacros = new JsonArray(); - - for (MacroButtonProperties macro : token.getMacroList(false)) { - JsonObject jmb = new JsonObject(); - jmb.addProperty("label", macro.getLabel()); - jmb.addProperty("tooltip", macro.getEvaluatedToolTip()); - jmb.addProperty("index", macro.getIndex()); - jmb.addProperty("fontColor", macro.getFontColorAsHtml()); - jmb.addProperty("displayGroup", macro.getGroupForDisplay()); - jmb.addProperty("group", macro.getGroup()); - jmb.addProperty("autoExecute", macro.getAutoExecute()); - jmb.addProperty("maxWidth", macro.getMaxWidth()); - jmb.addProperty("minWidth", macro.getMinWidth()); - jmb.addProperty("applyToTokens", macro.getApplyToTokens()); - - jmacros.add(jmb); - } - - jobj.add("macros", jmacros); - - mtws.sendMessage("tokenInfo", inResponseTo, jobj); - } - - void processMacro(JsonObject data) { - // FIXME: need to check parameters. - // FIXME: need to check permissions. - if ("callMacro".equalsIgnoreCase(data.get("command").getAsString())) { - Token token = findTokenFromId(data.get("tokenId").getAsString()); - - MacroButtonProperties macro = token.getMacro(data.get("macroIndex").getAsInt(), false); - macro.executeMacro(token.getId()); - } - } - - void processSetProperties(JsonObject data) { - final String tokenId = data.get("tokenId").getAsString(); - final Token token = findTokenFromId(tokenId); - final Zone zone = findZoneTokenIsOn(token); - - if (token == null) { - System.out.println("DEBUG: sendTokenInfo(): Unable to find token " + tokenId); - return; - // FIXME: log this error - } - - final JsonObject props = data.get("properties").getAsJsonObject(); - EventQueue.invokeLater( - () -> { - Set pnames = props.keySet(); - for (String pname : pnames) { - String val = props.get(pname).getAsString(); - token.setProperty(pname, val); - } - - zone.putToken(token); - }); - } -} diff --git a/src/main/proto/drawing_dto.proto b/src/main/proto/drawing_dto.proto index 98e45786bd..c14353a6a5 100644 --- a/src/main/proto/drawing_dto.proto +++ b/src/main/proto/drawing_dto.proto @@ -57,66 +57,60 @@ message WallTemplateDto { string id = 1; string layer = 2; google.protobuf.StringValue name = 3; - string zoneId = 4; - IntPointDto vertex = 5; - int32 radius = 6; - bool mouse_slope_greater = 7; - IntPointDto path_vertex = 8; - bool double_wide = 9; - repeated IntPointDto points = 10; + IntPointDto vertex = 4; + int32 radius = 5; + bool mouse_slope_greater = 6; + IntPointDto path_vertex = 7; + bool double_wide = 8; + repeated IntPointDto points = 9; } message LineTemplateDto { string id = 1; string layer = 2; google.protobuf.StringValue name = 3; - string zoneId = 4; - IntPointDto vertex = 5; - int32 radius = 6; - bool mouse_slope_greater = 7; - string quadrant = 8; - IntPointDto path_vertex = 9; - bool double_wide = 10; + IntPointDto vertex = 4; + int32 radius = 5; + bool mouse_slope_greater = 6; + string quadrant = 7; + IntPointDto path_vertex = 8; + bool double_wide = 9; } message BlastTemplateDto { string id = 1; string layer = 2; google.protobuf.StringValue name = 3; - string zoneId = 4; - IntPointDto vertex = 5; - int32 radius = 6; - string direction = 7; - int32 offset_x = 8; - int32 offset_y = 9; + IntPointDto vertex = 4; + int32 radius = 5; + string direction = 6; + int32 offset_x = 7; + int32 offset_y = 8; } message ConeTemplateDto { string id = 1; string layer = 2; google.protobuf.StringValue name = 3; - string zoneId = 4; - IntPointDto vertex = 5; - int32 radius = 6; - string direction = 7; + IntPointDto vertex = 4; + int32 radius = 5; + string direction = 6; } message BurstTemplateDto { string id = 1; string layer = 2; google.protobuf.StringValue name = 3; - string zoneId = 4; - IntPointDto vertex = 5; - int32 radius = 6; + IntPointDto vertex = 4; + int32 radius = 5; } message RadiusTemplateDto { string id = 1; string layer = 2; google.protobuf.StringValue name = 3; - string zoneId = 4; - IntPointDto vertex = 5; - int32 radius = 6; + IntPointDto vertex = 4; + int32 radius = 5; } message LineCellTemplateDto { @@ -126,18 +120,15 @@ message LineCellTemplateDto { string zoneId = 4; IntPointDto vertex = 5; int32 radius = 6; - bool mouse_slope_greater = 7; - string quadrant = 8; - IntPointDto path_vertex = 9; + IntPointDto path_vertex = 7; } message RadiusCellTemplateDto { string id = 1; string layer = 2; google.protobuf.StringValue name = 3; - string zoneId = 4; - IntPointDto vertex = 5; - int32 radius = 6; + IntPointDto vertex = 4; + int32 radius = 5; } message DrawablesGroupDto { diff --git a/src/main/resources/net/rptools/maptool/client/ui/themes/AarkLaF.properties b/src/main/resources/net/rptools/maptool/client/ui/themes/AarkLaF.properties new file mode 100644 index 0000000000..a31cd4404f --- /dev/null +++ b/src/main/resources/net/rptools/maptool/client/ui/themes/AarkLaF.properties @@ -0,0 +1,318 @@ +@baseColour = #edead9 +@col0 = shade(@baseColour,90%) +@col5 = changeLightness(@col50, 11%) +@col10 = changeLightness(@col50, 13%) +@col20 = changeLightness(@col50, 15%) +@col30 = changeLightness(@col50, 17%) +@col40 = changeLightness(@col50, 20%) +@col50 = changeLightness(saturate(mix(@col0, @col100, 90%), 20%, derived), 23%) +@col60 = changeLightness(@col50, 38%) +@col70 = changeLightness(@col50, 46%) +@col80 = changeLightness(@col50, 58%) +@col90 = changeLightness(@col50, 85%) +@col100= tint(@baseColour, 86%) + +@blue = #2675bf +@red = #c42b1c +@yellow= #fcf2a7 +@dkBlue= shade(@blue, 30%) +@dkRed= shade(@red, 30%) +@dkYellow= shade(@yellow, 40%) + +@gradientEnd = mix(@col100,@col0,3%) +@gradientStart = mix(@col100,@col0,25%) + +@gradientBorderEnd = lighten(@gradientStart, 40%) +@gradientBorderStart = lighten(@gradientEnd, 30%) + +defaultFont = +1 + +*.foreground = @col90 +*.background = @col10 +*.disabledBackground = @col40 +@componentBackground = @col10 +*.inactiveBackground = @col50 +Component.arrowType = "triangle" +TabbedPane.arrowType= "triangle" + +Button.default.startBackground = @gradientStart +Button.default.endBackground = @gradientEnd +Button.default.startBorderColor = @gradientBorderStart +Button.default.endBorderColor = @gradientBorderEnd +Button.default.borderWidth = 1 +Button.default.focusedBackground = @col20 +Button.default.focusedForeground = @col90 +Button.default.foreground = @col90 +Button.default.hoverBackground = @col50 +Button.default.hoverBorderColor = @yellow +Button.default.hoverForeground = @col90 +Button.default.pressedBackground = @col10 +Button.default.pressedForeground = @col90 +Button.default.shadowColor = @col50 +Button.default.disabledBackground= @col40 + +Button.background +Button.foreground = if($Button.background, contrast($Button.background, @col90, @col5), $Button.default.foreground) +Button.startBackground = if($Button.background, lighten($Button.background, 10%), $Button.default.startBackground) +Button.endBackground = if($Button.background, darken($Button.background, 5%), $Button.default.endBackground) +Button.borderWidth = $Button.default.borderWidth +Button.startBorderColor = $Button.default.startBorderColor +Button.endBorderColor = $Button.default.endBorderColor +Button.disabledBackground = if($Button.background, changeLightness(desaturate($Button.background,70%,relative),80%,autoInverse), $Button.default.pressedBackground) +Button.disabledForeground = if($Button.background, fadein(contrast($Button.disabledSelectedBackground, @col90, @col5),40%,derived), $Button.default.pressedForeground) +Button.focusedBackground = if($Button.background, lighten($Button.background, 8%), $Button.default.focusedBackground) +Button.focusedForeground = if($Button.background, contrast($Button.focusedBackground, @col90, @col5), $Button.default.focusedForeground) +Button.hoverBorderColor = $Button.default.hoverBorderColor +Button.hoverBackground = if($Button.background, darken($Button.background, 4%), $Button.default.hoverBackground) +Button.hoverForeground = if($Button.background, contrast($Button.hoverBackground, @col90, @col5), $Button.default.hoverForeground) +Button.pressedBackground = if($Button.background, darken($Button.background, 12%), $Button.default.pressedBackground) +Button.pressedForeground = if($Button.background, contrast($Button.pressedBackground, @col90, @col5), $Button.default.pressedForeground) +Button.selectedBackground = if($Button.background, lighten($Button.background, 4%), lighten($Button.default.pressedBackground, 14%)) +Button.selectedForeground = if($Button.background, contrast($Button.selectedBackground, @col90, @col5), $Button.default.pressedForeground) +Button.disabledSelectedBackground = if($Button.background, lighten($Button.background, 4%), $Button.default.pressedBackground) +Button.disabledSelectedForeground = if($Button.background, contrast($Button.disabledSelectedBackground, @col90, @col5), $Button.default.pressedForeground) +Button.shadowColor = $Button.default.shadowColor +Button.minimumWidth = 30 +Button.paintShadow = true +Button.shadowWidth = 2 + +ToggleButton.foreground = $Button.foreground +ToggleButton.startBackground = $Button.startBackground +ToggleButton.endBackground = $Button.endBackground +ToggleButton.borderWidth = $Button.borderWidth +ToggleButton.startBorderColor = $Button.startBorderColor +ToggleButton.endBorderColor = $Button.endBorderColor +ToggleButton.disabledBackground = $Button.disabledBackground +ToggleButton.disabledForeground = $Button.disabledForeground +ToggleButton.focusedBackground = $Button.focusedBackground +ToggleButton.focusedForeground = $Button.focusedForeground +ToggleButton.hoverBorderColor = $Button.hoverBorderColor +ToggleButton.hoverBackground = $Button.hoverBackground +ToggleButton.hoverForeground = $Button.hoverForeground +ToggleButton.pressedBackground = $Button.pressedBackground +ToggleButton.pressedForeground = $Button.pressedForeground +ToggleButton.selectedBackground = $Button.selectedBackground +ToggleButton.selectedForeground = $Button.selectedForeground +ToggleButton.disabledSelectedBackground = $Button.disabledSelectedBackground +ToggleButton.disabledSelectedForeground = $Button.disabledSelectedForeground + + +Button.toolbar.hoverForeground = $Button.hoverForeground +Button.toolbar.hoverBackground = $Button.hoverBackground +Button.toolbar.pressedForeground = $Button.pressedForeground +Button.toolbar.pressedBackground = $Button.pressedBackground +Button.toolbar.selectedForeground = $Button.selectedForeground +Button.toolbar.selectedBackground = $Button.selectedBackground +Button.toolbar.disabledSelectedForeground = $Button.disabledSelectedForeground +Button.toolbar.disabledSelectedBackground = $Button.disabledSelectedBackground +ToggleButton.toolbar.hoverForeground = $Button.hoverForeground +ToggleButton.toolbar.hoverBackground = $Button.hoverBackground +ToggleButton.toolbar.pressedForeground = $Button.pressedForeground +ToggleButton.toolbar.pressedBackground = $Button.pressedBackground +ToggleButton.toolbar.selectedForeground = $Button.selectedForeground +ToggleButton.toolbar.selectedBackground = $Button.selectedBackground +ToggleButton.toolbar.disabledSelectedForeground = $Button.disabledSelectedForeground +ToggleButton.toolbar.disabledSelectedBackground = $Button.disabledSelectedBackground + +Button.toolbar.margin = 3,3,3,3 +Button.toolbar.spacingInsets = 1,2,1,0 + +CheckBox.icon.background = @col20 +CheckBox.icon.checkmarkColor = @col100 +CheckBox.icon.focusedBackground = @col40 +CheckBox.icon.focusedBorderColor = @yellow +CheckBox.icon.focusColor = @yellow +CheckBox.icon.selectedBackground = @col0 +CheckBox.icon.hoverBorderColor = @col90 +CheckBox.icon.pressedBorderColor = @col90 +CheckBox.icon.disabledBackground = @col40 +CheckBox.icon.focusedSelectedBackground = @col20 +CheckBox.icon.focusedSelectedBorderColor = @blue + +CheckBox.icon.borderWidth = 0.6 +CheckBox.icon.borderColor = @col80 +RadioButton.icon.centerDiameter = 8 + +ColorChooser.swatchesSwatchSize = 18,18 + +Component.arc = 9 +Component.borderColor = mix(@gradientBorderStart, @gradientBorderEnd, 70%) +Component.borderWidth = 1 +Component.focusWidth = 1 +Component.focusedBorderColor = mix(@gradientBorderStart, @gradientBorderEnd, 80%) +Component.innerFocusWidth = 0 +Component.titleBarCaption = true +Component.innerOutlineWidth = 4.7 + +HelpButton.innerFocusWidth = 1 +HelpButton.questionMarkColor = @col100 +HelpButton.borderColor = $HelpButton.questionMarkColor +HelpButton.borderWidth = 3.8 +HelpButton.background = @blue +HelpButton.focusedBorderColor = $HelpButton.hoverBackground +HelpButton.focusedBackground = $HelpButton.background +HelpButton.disabledQuestionMarkColor = @col60 +HelpButton.disabledBackground = @col10 +HelpButton.hoverBackground = darken(@blue, 15%) +HelpButton.hoverBorderColor = @col80 +HelpButton.pressedBackground = @col30 + +InternalFrame.activeTitleBackground = @col60 +InternalFrame.activeTitleForeground = @col10 +InternalFrame.inactiveTitleBackground = @col30 +InternalFrame.inactiveTitleForeground = @col80 +InternalFrame.inactiveBorderColor = $InternalFrame.inactiveTitleBackground +InternalFrame.buttonHoverBackground = lighten($InternalFrame.activeTitleBackground,10%,derived) +InternalFrame.buttonPressedBackground = lighten($InternalFrame.activeTitleForeground,40%,derived) +InternalFrame.closeHoverBackground = @red +InternalFrame.closeHoverForeground = @col10 +InternalFrame.activeBorderColor = $InternalFrame.activeTitleBackground + +Menu.acceleratorForeground = @yellow +Menu.acceleratorSelectionForeground = @col100 +Menu.background = @col10 +#Menu.border = 1,2,1,4, @yellow, 0.5, 4 +Menu.borderPainted = false +Menu.disabledForeground = @col80 +Menu.foreground = @col90 +Menu.icon.arrowColor = @yellow +Menu.icon.disabledArrowColor = fadein($Menu.icon.arrowColor,20%,derived) +Menu.selectionBackground = @blue +Menu.selectionForeground = @col90 +Menu.opaque = true +Menu.margin = 2,4,2,2 + +MenuItem.foreground = $Menu.foreground +MenuItem.disabledForeground = $Menu.disabledForeground +MenuItem.background = $Menu.background +MenuItem.selectionForeground = $Menu.selectionForeground +MenuItem.selectionBackground = $Menu.selectionBackground +MenuItem.acceleratorForeground = $Menu.acceleratorForeground +MenuItem.acceleratorSelectionForeground = $Menu.acceleratorSelectionForeground +MenuItem.checkBackground = @red +MenuItem.underlineSelectionColor = @red +MenuItem.underlineSelectionBackground = @dkBlue +MenuItem.underlineSelectionCheckBackground = @yellow +MenuItem.underlineSelectionHeight = 4 +MenuItem.minimumIconSize = 14,14 +MenuItem.checkMargins = 1,2,1,3 +#MenuItem.selectionType = underline +MenuItem.opaque = $Menu.opaque +#MenuItem.border = 1,2,1,2, @col80, 0.4, 2 + +CheckBoxMenuItem.foreground = $Menu.foreground +CheckBoxMenuItem.disabledForeground = $Menu.disabledForeground +CheckBoxMenuItem.background = $Menu.background +CheckBoxMenuItem.selectionForeground = $Menu.selectionForeground +CheckBoxMenuItem.selectionBackground = $Menu.selectionBackground +CheckBoxMenuItem.acceleratorForeground = $Menu.acceleratorForeground +CheckBoxMenuItem.acceleratorSelectionForeground = $Menu.acceleratorSelectionForeground +CheckBoxMenuItem.icon.checkmarkColor = $Menu.icon.arrowColor +CheckBoxMenuItem.icon.disabledCheckmarkColor = $Menu.icon.disabledArrowColor +CheckBoxMenuItem.opaque = $Menu.opaque + +RadioButtonMenuItem.foreground = $Menu.foreground +RadioButtonMenuItem.disabledForeground = $Menu.disabledForeground +RadioButtonMenuItem.background = $Menu.background +RadioButtonMenuItem.selectionForeground = $Menu.selectionForeground +RadioButtonMenuItem.selectionBackground = $Menu.selectionBackground +RadioButtonMenuItem.acceleratorForeground = $Menu.acceleratorForeground +RadioButtonMenuItem.acceleratorSelectionForeground = $Menu.acceleratorSelectionForeground +RadioButtonMenuItem.opaque = $Menu.opaque + +MenuBar.selectionArc = 9 +MenuBar.background = $Menu.background +MenuBar.foreground = $Menu.foreground +#MenuBar.border = $Menu.border +#MenuBar.borderColor = @yellow +MenuBar.underlineSelectionColor = @col0 +MenuBar.underlineSelectionHeight = 1 + +PopupMenuSeparator.height = 7 +PopupMenu.background = $Menu.background +PopupMenuSeparator.stripeWidth = 1 + +Panel.background = @col30 + +ProgressBar.arc = 3 +ProgressBar.background = @dkYellow +ProgressBar.foreground = @dkBlue +ProgressBar.selectionForeground = @yellow +ProgressBar.selectionBackground = @col0 +ProgressBar.border = 1,1,1,1,@col0,1,12 + +ScrollBar.background = @col0 +ScrollBar.showButtons = true +ScrollBar.thumb = @col60 +ScrollBar.thumbArc = 7 +ScrollBar.thumbInsets = 2,2,1,1 +ScrollBar.track = @col40 +ScrollBar.trackArc = 999 +ScrollBar.trackInsets = 2, 6, 2, 6 +ScrollBar.width = 15 +ScrollBar.buttonArrowColor = @col60 +ScrollBar.buttonDisabledArrowColor = fadein(@col90,40%,derived) +ScrollBar.hoverButtonBackground = @col90 +ScrollBar.minimumButtonSize = 10, 10 + +Slider.focusWidth = 1 +Slider.focusedColor = @col40 +Slider.focusedThumbBorderColor = @col0 +Slider.thumbBorderWidth = 2 +Slider.thumbBorderColor = @col80 +Slider.thumbColor = @col60 +Slider.thumbArc = 5 +Slider.thumbInsets = 2, 2, 2, 2 +Slider.thumbSize = 14,14 +Slider.tickColor = @col90 +Slider.trackArc = 99 +Slider.trackColor = @col30 +Slider.trackInsets = 2, 4, 2, 4 +Slider.trackValueColor = $Slider.thumbColor +Slider.trackWidth = 5 + +Spinner.buttonSeparatorWidth = 1.2 + +SplitPane.dividerSize = 5 +SplitPaneDivider.hoverColor = @red +SplitPaneDivider.gripDotCount = 8 +SplitPaneDivider.gripColor = @col80 +SplitPane.shadow = @blue +SplitPane.background = @col40 + +TabbedPane.background = @col20 +TabbedPane.foreground = @col80 +TabbedPane.inactiveUnderlineColor = @col30 +TabbedPane.selectedBackground = @col5 +TabbedPane.selectedForeground = @col90 +TabbedPane.showTabSeparators = true +TabbedPane.tabSeparatorColor = @col50 +TabbedPane.tabSeparatorsFullHeight= true +TabbedPane.underlineColor = @col50 +TabbedPane.hoverColor = @col0 + +TextComponent.arc = 8 + +TitlePane.buttonSize = 16,14 +TitlePane.iconSize = 15,15 +TitlePane.iconMargins = 1,1,1,1 +TitlePane.titleMargins = 1,4,1,2 +TitlePane.menuBarTitleGap = 8 + +ToolBar.background = @col5 +ToolBar.foreground = @col90 +ToolBar.floatable = true +ToolBar.focusableButtons = true +ToolBar.gripColor = @col90 +ToolBar.separatorWidth = 8 +ToolBar.separatorColor = @col90 + +ToolTip.background = @col50 +ToolTip.foreground = @col90 +ToolTip.border = 3,3,5,3, @gradientBorderEnd, 1, 6 + +Tree.background = @col20 +Tree.textBackground = @col10 +Tree.textForeground = @col90 + +Window.style = small \ No newline at end of file diff --git a/src/main/resources/net/rptools/maptool/language/i18n.properties b/src/main/resources/net/rptools/maptool/language/i18n.properties index bacb3cd884..85c0802e53 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n.properties @@ -406,7 +406,7 @@ GridlessGrid.error.notLoaded = Could not load gridless grid footprints. Handshake.msg.duplicateName = Someone is already using that player name. Please choose another name and try again. Handshake.msg.wrongPassword = You entered the wrong password. Please try again. Handshake.msg.unknownPlayer = Player {0} is unknown. -Handshake.msg.wrongVersion = The versions used do not match. You are trying to connect a client running version {0} to a server running version {1}.
Please update your client to the correct version. +Handshake.msg.wrongVersion = The versions used do not match. You are trying to connect a client to a server running a different version.
Please update your client to the correct version. Handshake.msg.failedLogin = Failed login attempt from {0}, errors decoding handshake follow. Handshake.msg.failedLoginDecode = Failed decoding handshake with password. Handshake.msg.encodeInitFail = Failed to initialize encoding for handshake. @@ -420,6 +420,8 @@ Handshake.msg.incorrectPublicKey = Incorrect public key used during handshake. Handshake.msg.deniedEasyConnect = Easy connect request denied. Handshake.msg.gmDeniedRequest = GM has denied your request to connect. Handshake.msg.playerAlreadyConnected = That player name is already connected. +Handshake.msg.failedToGetPublicKey = Failed to get the player's public key +Handshake.msg.unexpectedError = Unexpected error encountered during handshake. Update.title = Update Available Update.msg1 = A new version of MapTool is available! @@ -1246,6 +1248,7 @@ action.commandPanel = Command Panel # In order to prevent I18N from warning that they don't exist these # lines have been added. action.commitCommand = Not used (action.commitCommand) +action.newlineCommand = Not used (action.newlineCommand) action.copyTokens = Copy action.copyTokens.accel = C action.copyTokens.description = Copy selected tokens to an internal clipboard. @@ -1418,6 +1421,7 @@ action.saveMapAs = &Export Map... action.saveMapAs.description = Save a map to an external file. action.saveMessageHistory = Save Chat Log &History... action.saveMessageHistory.description = Save the contents of your chat log, including whispers. +action.nextToken = Select next token # Tool action.sendChat = C&hat action.sendChat.accel = ENTER @@ -2160,6 +2164,7 @@ msg.confirm.restoreFoW = Map contains exposed areas msg.confirm.saveCampaign = Would you like to save your campaign before you exit? msg.error.alreadyRegistered = That ID is already in use -- server not registered. msg.error.alreadyRunningServer = You are already running a server. +msg.error.stillRunningServer = You are still running a server. msg.error.browser.cannotStart = Browser could not be started.
{0} msg.error.browser.notFound = System browser could not be identified.
Please specify your system browser in the {0} environment variable. msg.error.cantAdjustGridInfMaps = Cannot adjust grid on infinite maps. @@ -2729,7 +2734,7 @@ tools.token.fow.all.tooltip = Show FoW for All Tokens you explicitly own or are tools.token.fow.gm.tooltip = Show FoW for Tokens you explicitly own.
IF you are a GM, you will also see any tokens that have no ownership set. tools.token.fow.npc.tooltip = Show FoW for NPC Tokens you explicitly own or are owned by all. tools.token.fow.pc.tooltip = Show FoW for PC Tokens you explicitly own or are owned by all. -tools.topo.tooltip = Vision Blocking Layer (VBL) Tools +tools.topo.tooltip = Topology Tools (Vision and Movement Blocking Layer) tools.zoneselector.tooltip = Select Map tools.initiative.tooltip = Initiative Tools @@ -2813,15 +2818,6 @@ mapSortType.GMNAME = True Name mapSortType.DISPLAYNAME = Display Name -webapp.serverAlreadyRunning = The webapp server is already running. -webapp.serverStarted = Webapp server has been started
Connect to \ - http://{0}:{1}/{2} on your browser/phone/tablet.

\ - The experimental Server App is deprecated and will be removed in an \ - upcoming version.

If you currently make use of this feature please \ - visit discord channel
to let us know how you are using it so we \ - can include this in our plans for its replacement

Discord: \ - https://discord.gg/hKChMAmn32

- whisper.command = Whisper whisper.description = Send a message to a specific player. whisper.enterText = enter text here diff --git a/src/main/resources/net/rptools/maptool/language/i18n_ach.properties b/src/main/resources/net/rptools/maptool/language/i18n_ach.properties index a74557fd02..2eecbf2fde 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_ach.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_ach.properties @@ -1477,9 +1477,6 @@ visionType.DAY = crwdns2040\:0crwdne2040\:0 visionType.NIGHT = crwdns2042\:0crwdne2042\:0 visionType.OFF = crwdns2044\:0crwdne2044\:0 -webapp.serverAlreadyRunning = crwdns2046\:0crwdne2046\:0 -webapp.serverStarted = crwdns2048\:0{0}crwdnd2048\:0{1}crwdnd2048\:0{2}crwdne2048\:0 - whisper.command = crwdns2050\:0crwdne2050\:0 whisper.description = crwdns2052\:0crwdne2052\:0 whisper.enterText = crwdns2054\:0crwdne2054\:0 diff --git a/src/main/resources/net/rptools/maptool/language/i18n_cs.properties b/src/main/resources/net/rptools/maptool/language/i18n_cs.properties index 883c4c656b..0b4fb34f9f 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_cs.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_cs.properties @@ -2829,9 +2829,6 @@ mapSortType.GMNAME = True Name mapSortType.DISPLAYNAME = Display Name -webapp.serverAlreadyRunning = The webapp server is already running. -webapp.serverStarted = Webapp server has been started
Connect to http\://{0}\:{1}/{2} on your browser/phone/tablet.

The experimental Server App is deprecated and will be removed in an upcoming version.

If you currently make use of this feature please visit discord channel
to let us know how you are using it so we can include this in our plans for its replacement

Discord\: https\://discord.gg/hKChMAmn32

- whisper.command = Whisper whisper.description = Send a message to a specific player. whisper.enterText = enter text here diff --git a/src/main/resources/net/rptools/maptool/language/i18n_da.properties b/src/main/resources/net/rptools/maptool/language/i18n_da.properties index cfc965ad1a..fac9a1edfa 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_da.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_da.properties @@ -2829,9 +2829,6 @@ mapSortType.GMNAME = Sandt Navn mapSortType.DISPLAYNAME = Vist navn -webapp.serverAlreadyRunning = The webapp server is already running. -webapp.serverStarted = Webapp server has been started
Connect to http\://{0}\:{1}/{2} on your browser/phone/tablet.

The experimental Server App is deprecated and will be removed in an upcoming version.

If you currently make use of this feature please visit discord channel
to let us know how you are using it so we can include this in our plans for its replacement

Discord\: https\://discord.gg/hKChMAmn32

- whisper.command = Hvisk whisper.description = Send a message to a specific player. whisper.enterText = skriv tekst her diff --git a/src/main/resources/net/rptools/maptool/language/i18n_de.properties b/src/main/resources/net/rptools/maptool/language/i18n_de.properties index 3a91769b71..765a2902dd 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_de.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_de.properties @@ -2829,9 +2829,6 @@ mapSortType.GMNAME = Technischer Name mapSortType.DISPLAYNAME = Anzeigename -webapp.serverAlreadyRunning = Der Webapp-Server läuft bereits. -webapp.serverStarted = Webapp-Server wurde gestartet
Verbinden Sie sich mit http\://{0}\:{1}/{2} auf Ihrem Browser/Handy/Tablet.

Die experimentelle Server-App ist veraltet und wird in einer kommenden Version entfernt.

Wenn du diese Funktion derzeit nutzt, besuche bitte den Discord Channel,
um uns wissen zu lassen, wie du sie nutzt, damit wir dies berücksichtigen können.

Discord\: https\://discord. g/hKChMAmn32

- whisper.command = Flüstern whisper.description = Sende eine Nachricht an einen bestimmten Spieler. whisper.enterText = hier Text eingeben diff --git a/src/main/resources/net/rptools/maptool/language/i18n_en.properties b/src/main/resources/net/rptools/maptool/language/i18n_en.properties index d91a1ca8c2..1e88824356 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_en.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_en.properties @@ -412,7 +412,7 @@ GridlessGrid.error.notLoaded = Could not load gridless grid footprints. Handshake.msg.duplicateName = Someone is already using that player name. Please choose another name and try again. Handshake.msg.wrongPassword = You entered the wrong password. Please try again. Handshake.msg.unknownPlayer = Player {0} is unknown. -Handshake.msg.wrongVersion = The versions used do not match. You are trying to connect a client running version {0} to a server running version {1}.
Please update your client to the correct version. +Handshake.msg.wrongVersion = The versions used do not match. You are trying to connect a client to a server running a different version.
Please update your client to the correct version. Handshake.msg.failedLogin = Failed login attempt from {0}, errors decoding handshake follow. Handshake.msg.failedLoginDecode = Failed decoding handshake with password. Handshake.msg.encodeInitFail = Failed to initialize encoding for handshake. diff --git a/src/main/resources/net/rptools/maptool/language/i18n_en_au.properties b/src/main/resources/net/rptools/maptool/language/i18n_en_au.properties index bc0508f589..d7306d235c 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_en_au.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_en_au.properties @@ -407,7 +407,7 @@ GridlessGrid.error.notLoaded = Could not load gridless grid footprints. Handshake.msg.duplicateName = Someone is already using that player name. Please choose another name and try again. Handshake.msg.wrongPassword = You entered the wrong password. Please try again. Handshake.msg.unknownPlayer = Player {0} is unknown. -Handshake.msg.wrongVersion = The versions used do not match. You are trying to connect a client running version {0} to a server running version {1}.
Please update your client to the correct version. +Handshake.msg.wrongVersion = The versions used do not match. You are trying to connect a client to a server running a different version.
Please update your client to the correct version. Handshake.msg.failedLogin = Failed login attempt from {0}, errors decoding handshake follow. Handshake.msg.failedLoginDecode = Failed decoding handshake with password. Handshake.msg.encodeInitFail = Failed to initialize encoding for handshake. diff --git a/src/main/resources/net/rptools/maptool/language/i18n_en_gb.properties b/src/main/resources/net/rptools/maptool/language/i18n_en_gb.properties index bc0508f589..d7306d235c 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_en_gb.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_en_gb.properties @@ -407,7 +407,7 @@ GridlessGrid.error.notLoaded = Could not load gridless grid footprints. Handshake.msg.duplicateName = Someone is already using that player name. Please choose another name and try again. Handshake.msg.wrongPassword = You entered the wrong password. Please try again. Handshake.msg.unknownPlayer = Player {0} is unknown. -Handshake.msg.wrongVersion = The versions used do not match. You are trying to connect a client running version {0} to a server running version {1}.
Please update your client to the correct version. +Handshake.msg.wrongVersion = The versions used do not match. You are trying to connect a client to a server running a different version.
Please update your client to the correct version. Handshake.msg.failedLogin = Failed login attempt from {0}, errors decoding handshake follow. Handshake.msg.failedLoginDecode = Failed decoding handshake with password. Handshake.msg.encodeInitFail = Failed to initialize encoding for handshake. diff --git a/src/main/resources/net/rptools/maptool/language/i18n_es.properties b/src/main/resources/net/rptools/maptool/language/i18n_es.properties index 019a1199a0..25f7e0b735 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_es.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_es.properties @@ -2829,9 +2829,6 @@ mapSortType.GMNAME = True Name mapSortType.DISPLAYNAME = Display Name -webapp.serverAlreadyRunning = The webapp server is already running. -webapp.serverStarted = Webapp server has been started
Connect to http\://{0}\:{1}/{2} on your browser/phone/tablet.

The experimental Server App is deprecated and will be removed in an upcoming version.

If you currently make use of this feature please visit discord channel
to let us know how you are using it so we can include this in our plans for its replacement

Discord\: https\://discord.gg/hKChMAmn32

- whisper.command = Susurro whisper.description = Envía un mensaje a un jugador específico. whisper.enterText = introducir texto aquí diff --git a/src/main/resources/net/rptools/maptool/language/i18n_fr.properties b/src/main/resources/net/rptools/maptool/language/i18n_fr.properties index d9d7473baf..74f2389b25 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_fr.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_fr.properties @@ -2829,9 +2829,6 @@ mapSortType.GMNAME = True Name mapSortType.DISPLAYNAME = Display Name -webapp.serverAlreadyRunning = Le serveur webapp est déjà en cours d'exécution. -webapp.serverStarted = Webapp server has been started
Connect to http\://{0}\:{1}/{2} on your browser/phone/tablet.

The experimental Server App is deprecated and will be removed in an upcoming version.

If you currently make use of this feature please visit discord channel
to let us know how you are using it so we can include this in our plans for its replacement

Discord\: https\://discord.gg/hKChMAmn32

- whisper.command = Chuchotement whisper.description = Envoyer un message à un joueur spécifique. whisper.enterText = Entrez le texte ici diff --git a/src/main/resources/net/rptools/maptool/language/i18n_it.properties b/src/main/resources/net/rptools/maptool/language/i18n_it.properties index 6668a43078..1d28c107e2 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_it.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_it.properties @@ -2829,9 +2829,6 @@ mapSortType.GMNAME = Vero Nome mapSortType.DISPLAYNAME = Nome Visualizzato -webapp.serverAlreadyRunning = Il server webapp è già in esecuzione. -webapp.serverStarted = Il server Webapp è stato avviato
Connettiti a http\://{0}\:{1}/{2} sul tuo browser/telefono/tablet.

L'App Server sperimentale è deprecata e verrà rimossa in una prossima versione.

Se attualmente utilizzi questa funzionalità, visita il canale discord
per farci sapere come la stai usando, così possiamo includerlo nei nostri piani per la sua sostituzione

Discord\: https\://discord. g/hKChMAmn32

- whisper.command = Sussurra whisper.description = Invia un messaggio a un giocatore specifico. whisper.enterText = Inserisci il testo qui diff --git a/src/main/resources/net/rptools/maptool/language/i18n_ja.properties b/src/main/resources/net/rptools/maptool/language/i18n_ja.properties index 0d99b72099..77250e6325 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_ja.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_ja.properties @@ -2829,9 +2829,6 @@ mapSortType.GMNAME = 本来の名称 mapSortType.DISPLAYNAME = 表示名称 -webapp.serverAlreadyRunning = このWEBアプリケーションサーバーは既に実行中です。 -webapp.serverStarted = ウェブアプリ・サーバーが起動しています
ブラウザ/スマホ/タブレット等からhttp\://{0}\:{1}/{2}に接続してください。

実験的なサーバー・アプリは廃止予定で、今後のバージョンでは削除される予定です。

現在この機能を使用している場合は、Discord チャンネルにアクセスし、
どのように使用しているか教えてください。開発計画にこの機能の代替機能を含めることができます

Discord:https\://discord.gg/hKChMAmn32

- whisper.command = 耳打ち whisper.description = 特定のプレイヤーにメッセージを送る。 whisper.enterText = ここにテキストを入力してください diff --git a/src/main/resources/net/rptools/maptool/language/i18n_nl.properties b/src/main/resources/net/rptools/maptool/language/i18n_nl.properties index 1b5c8032e5..af52f11ea7 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_nl.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_nl.properties @@ -2829,9 +2829,6 @@ mapSortType.GMNAME = True Name mapSortType.DISPLAYNAME = Display Name -webapp.serverAlreadyRunning = De webapp server is al actief. -webapp.serverStarted = Webapp server has been started
Connect to http\://{0}\:{1}/{2} on your browser/phone/tablet.

The experimental Server App is deprecated and will be removed in an upcoming version.

If you currently make use of this feature please visit discord channel
to let us know how you are using it so we can include this in our plans for its replacement

Discord\: https\://discord.gg/hKChMAmn32

- whisper.command = Fluisteren whisper.description = Stuur een bericht naar een specifieke speler. whisper.enterText = voer hier uw tekst in diff --git a/src/main/resources/net/rptools/maptool/language/i18n_pl.properties b/src/main/resources/net/rptools/maptool/language/i18n_pl.properties index 5a2d34e1ee..f3238cd519 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_pl.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_pl.properties @@ -2829,9 +2829,6 @@ mapSortType.GMNAME = Nazwa mapSortType.DISPLAYNAME = Nazwa Wyświetlana -webapp.serverAlreadyRunning = WebApp serwer jest już uruchomiony. -webapp.serverStarted = Webapp server został uruchomiony
Połącz się z http\://{0}\:{1}/{2} w przeglądarce/telefonie/tablet.

Eksperymentalna aplikacja serwera jest przestarzała i zostanie usunięta w nadchodzącej wersji.

Jeśli obecnie korzystasz z tej funkcji, odwiedź kanał Discorda
, aby poinformować nas, jak ją używasz, abyśmy mogli uwzględnić to w naszych planach

Discord\: https\://discord. g/hKChMAmn32

- whisper.command = Szept whisper.description = Wyślij wiadomość do konkretnego gracza. whisper.enterText = tu wprowadź tekst diff --git a/src/main/resources/net/rptools/maptool/language/i18n_pt.properties b/src/main/resources/net/rptools/maptool/language/i18n_pt.properties index 9a49624253..911a66b410 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_pt.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_pt.properties @@ -2829,9 +2829,6 @@ mapSortType.GMNAME = Nome verdadeiro mapSortType.DISPLAYNAME = Nome de Exibição -webapp.serverAlreadyRunning = O app de rede já esta em execução. -webapp.serverStarted = Webapp server has been started
Connect to http\://{0}\:{1}/{2} on your browser/phone/tablet.

The experimental Server App is deprecated and will be removed in an upcoming version.

If you currently make use of this feature please visit discord channel
to let us know how you are using it so we can include this in our plans for its replacement

Discord\: https\://discord.gg/hKChMAmn32

- whisper.command = Sussurrar whisper.description = Enviar mensagem a um jogador específico. whisper.enterText = insira o texto aqui diff --git a/src/main/resources/net/rptools/maptool/language/i18n_ru.properties b/src/main/resources/net/rptools/maptool/language/i18n_ru.properties index 937a47ac0f..a9053285c6 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_ru.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_ru.properties @@ -2829,9 +2829,6 @@ mapSortType.GMNAME = True Name mapSortType.DISPLAYNAME = Display Name -webapp.serverAlreadyRunning = Webapp сервер уже запущен. -webapp.serverStarted = Webapp server has been started
Connect to http\://{0}\:{1}/{2} on your browser/phone/tablet.

The experimental Server App is deprecated and will be removed in an upcoming version.

If you currently make use of this feature please visit discord channel
to let us know how you are using it so we can include this in our plans for its replacement

Discord\: https\://discord.gg/hKChMAmn32

- whisper.command = Шепнуть whisper.description = Отправить сообщение конкретному игроку. whisper.enterText = введите текст здесь diff --git a/src/main/resources/net/rptools/maptool/language/i18n_sv.properties b/src/main/resources/net/rptools/maptool/language/i18n_sv.properties index 88e18f2d0d..f75b43ebb8 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_sv.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_sv.properties @@ -2829,9 +2829,6 @@ mapSortType.GMNAME = True Name mapSortType.DISPLAYNAME = Display Name -webapp.serverAlreadyRunning = Webbapp-servern körs redan. -webapp.serverStarted = Webapp server has been started
Connect to http\://{0}\:{1}/{2} on your browser/phone/tablet.

The experimental Server App is deprecated and will be removed in an upcoming version.

If you currently make use of this feature please visit discord channel
to let us know how you are using it so we can include this in our plans for its replacement

Discord\: https\://discord.gg/hKChMAmn32

- whisper.command = Viska whisper.description = Skicka ett meddelande till en specifik spelare. whisper.enterText = ange text här diff --git a/src/main/resources/net/rptools/maptool/language/i18n_uk.properties b/src/main/resources/net/rptools/maptool/language/i18n_uk.properties index 7b561f4d61..a28153ceb6 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_uk.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_uk.properties @@ -2829,9 +2829,6 @@ mapSortType.GMNAME = True Name mapSortType.DISPLAYNAME = Display Name -webapp.serverAlreadyRunning = The webapp server is already running. -webapp.serverStarted = Webapp server has been started
Connect to http\://{0}\:{1}/{2} on your browser/phone/tablet.

The experimental Server App is deprecated and will be removed in an upcoming version.

If you currently make use of this feature please visit discord channel
to let us know how you are using it so we can include this in our plans for its replacement

Discord\: https\://discord.gg/hKChMAmn32

- whisper.command = Whisper whisper.description = Send a message to a specific player. whisper.enterText = enter text here diff --git a/src/main/resources/net/rptools/maptool/language/i18n_zh.properties b/src/main/resources/net/rptools/maptool/language/i18n_zh.properties index f1fc8c8461..535c676f0c 100644 --- a/src/main/resources/net/rptools/maptool/language/i18n_zh.properties +++ b/src/main/resources/net/rptools/maptool/language/i18n_zh.properties @@ -2829,9 +2829,6 @@ mapSortType.GMNAME = 真名 mapSortType.DISPLAYNAME = 显示名 -webapp.serverAlreadyRunning = webapp服务器正在运行。 -webapp.serverStarted = Webapp server has been started
Connect to http\://{0}\:{1}/{2} on your browser/phone/tablet.

The experimental Server App is deprecated and will be removed in an upcoming version.

If you currently make use of this feature please visit discord channel
to let us know how you are using it so we can include this in our plans for its replacement

Discord\: https\://discord.gg/hKChMAmn32

- whisper.command = 耳語 whisper.description = 发送消息给指定玩家。 whisper.enterText = 在這裡輸入測試 diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/.bower.json b/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/.bower.json deleted file mode 100644 index 7a67a868c1..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/.bower.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "handlebars", - "version": "2.0.0", - "main": "handlebars.js", - "dependencies": {}, - "homepage": "https://github.com/components/handlebars.js", - "_release": "2.0.0", - "_resolution": { - "type": "version", - "tag": "v2.0.0", - "commit": "a0f211c3dd9a5e464b4f16020ff85783fd36ce1c" - }, - "_source": "git://github.com/components/handlebars.js.git", - "_target": "~2.0.0", - "_originalSource": "handlebars", - "_direct": true -} \ No newline at end of file diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/.gitignore b/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/.gitignore deleted file mode 100644 index 7579f74311..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -vendor -composer.lock diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/README.md b/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/README.md deleted file mode 100644 index 751b1220af..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/README.md +++ /dev/null @@ -1,11 +0,0 @@ -Handlebars.js -============= - -Shim repository for [Handlebars.js](http://handlebarsjs.com). - -Package Managers ----------------- - -* [Bower](http://twitter.github.com/bower/): `handlebars` -* [Component](http://github.com/component/component): `components/handlebars.js` -* [Composer](http://packagist.org/packages/components/handlebars.js): `components/handlebars.js` diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/bower.json b/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/bower.json deleted file mode 100644 index ec92803100..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/bower.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "handlebars", - "version": "2.0.0", - "main": "handlebars.js", - "dependencies": {} -} diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/component.json b/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/component.json deleted file mode 100644 index f74c83962a..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/component.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "handlebars", - "repo": "components/handlebars.js", - "version": "1.0.0", - "main": "handlebars.js", - "scripts": [ - "handlebars.js" - ] -} diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/composer.json b/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/composer.json deleted file mode 100644 index c5dc4271fc..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/composer.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "components/handlebars.js", - "description": "Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be.", - "homepage": "http://handlebarsjs.com", - "license": "MIT", - "type": "component", - "keywords": [ - "handlebars", - "mustache", - "html" - ], - "authors": [ - { - "name": "Chris Wanstrath", - "homepage": "http://chriswanstrath.com" - } - ], - "require": { - "robloach/component-installer": "*" - }, - "extra": { - "component": { - "name": "handlebars", - "scripts": [ - "handlebars.js" - ], - "files": [ - "handlebars.runtime.js" - ], - "shim": { - "exports": "Handlebars" - } - } - } -} diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars-source.gemspec b/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars-source.gemspec deleted file mode 100644 index f9e1a4bc43..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars-source.gemspec +++ /dev/null @@ -1,22 +0,0 @@ -# -*- encoding: utf-8 -*- -require 'json' - -package = JSON.parse(File.read('bower.json')) - -Gem::Specification.new do |gem| - gem.name = "handlebars-source" - gem.authors = ["Yehuda Katz"] - gem.email = ["wycats@gmail.com"] - gem.date = Time.now.strftime("%Y-%m-%d") - gem.description = %q{Handlebars.js source code wrapper for (pre)compilation gems.} - gem.summary = %q{Handlebars.js source code wrapper} - gem.homepage = "https://github.com/wycats/handlebars.js/" - gem.version = package["version"].sub! "-", "." - gem.license = "MIT" - - gem.files = [ - 'handlebars.js', - 'handlebars.runtime.js', - 'lib/handlebars/source.rb' - ] -end diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.amd.js b/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.amd.js deleted file mode 100644 index dddd408fff..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.amd.js +++ /dev/null @@ -1,3042 +0,0 @@ -/*! - - handlebars v2.0.0 - -Copyright (C) 2011-2014 by Yehuda Katz - -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. - -@license -*/ - -define( - 'handlebars/safe-string',["exports"], - function(__exports__) { - - // Build out our basic SafeString type - function SafeString(string) { - this.string = string; - } - - SafeString.prototype.toString = function() { - return "" + this.string; - }; - - __exports__["default"] = SafeString; - }); -define( - 'handlebars/utils',["./safe-string","exports"], - function(__dependency1__, __exports__) { - - /*jshint -W004 */ - var SafeString = __dependency1__["default"]; - - var escape = { - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" - }; - - var badChars = /[&<>"'`]/g; - var possible = /[&<>"'`]/; - - function escapeChar(chr) { - return escape[chr]; - } - - function extend(obj /* , ...source */) { - for (var i = 1; i < arguments.length; i++) { - for (var key in arguments[i]) { - if (Object.prototype.hasOwnProperty.call(arguments[i], key)) { - obj[key] = arguments[i][key]; - } - } - } - - return obj; - } - - __exports__.extend = extend;var toString = Object.prototype.toString; - __exports__.toString = toString; - // Sourced from lodash - // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt - var isFunction = function(value) { - return typeof value === 'function'; - }; - // fallback for older versions of Chrome and Safari - /* istanbul ignore next */ - if (isFunction(/x/)) { - isFunction = function(value) { - return typeof value === 'function' && toString.call(value) === '[object Function]'; - }; - } - var isFunction; - __exports__.isFunction = isFunction; - /* istanbul ignore next */ - var isArray = Array.isArray || function(value) { - return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; - }; - __exports__.isArray = isArray; - - function escapeExpression(string) { - // don't escape SafeStrings, since they're already safe - if (string instanceof SafeString) { - return string.toString(); - } else if (string == null) { - return ""; - } else if (!string) { - return string + ''; - } - - // Force a string conversion as this will be done by the append regardless and - // the regex test will do this transparently behind the scenes, causing issues if - // an object's to string has escaped characters in it. - string = "" + string; - - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); - } - - __exports__.escapeExpression = escapeExpression;function isEmpty(value) { - if (!value && value !== 0) { - return true; - } else if (isArray(value) && value.length === 0) { - return true; - } else { - return false; - } - } - - __exports__.isEmpty = isEmpty;function appendContextPath(contextPath, id) { - return (contextPath ? contextPath + '.' : '') + id; - } - - __exports__.appendContextPath = appendContextPath; - }); -define( - 'handlebars/exception',["exports"], - function(__exports__) { - - - var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; - - function Exception(message, node) { - var line; - if (node && node.firstLine) { - line = node.firstLine; - - message += ' - ' + line + ':' + node.firstColumn; - } - - var tmp = Error.prototype.constructor.call(this, message); - - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (var idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } - - if (line) { - this.lineNumber = line; - this.column = node.firstColumn; - } - } - - Exception.prototype = new Error(); - - __exports__["default"] = Exception; - }); -define( - 'handlebars/base',["./utils","./exception","exports"], - function(__dependency1__, __dependency2__, __exports__) { - - var Utils = __dependency1__; - var Exception = __dependency2__["default"]; - - var VERSION = "2.0.0"; - __exports__.VERSION = VERSION;var COMPILER_REVISION = 6; - __exports__.COMPILER_REVISION = COMPILER_REVISION; - var REVISION_CHANGES = { - 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it - 2: '== 1.0.0-rc.3', - 3: '== 1.0.0-rc.4', - 4: '== 1.x.x', - 5: '== 2.0.0-alpha.x', - 6: '>= 2.0.0-beta.1' - }; - __exports__.REVISION_CHANGES = REVISION_CHANGES; - var isArray = Utils.isArray, - isFunction = Utils.isFunction, - toString = Utils.toString, - objectType = '[object Object]'; - - function HandlebarsEnvironment(helpers, partials) { - this.helpers = helpers || {}; - this.partials = partials || {}; - - registerDefaultHelpers(this); - } - - __exports__.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = { - constructor: HandlebarsEnvironment, - - logger: logger, - log: log, - - registerHelper: function(name, fn) { - if (toString.call(name) === objectType) { - if (fn) { throw new Exception('Arg not supported with multiple helpers'); } - Utils.extend(this.helpers, name); - } else { - this.helpers[name] = fn; - } - }, - unregisterHelper: function(name) { - delete this.helpers[name]; - }, - - registerPartial: function(name, partial) { - if (toString.call(name) === objectType) { - Utils.extend(this.partials, name); - } else { - this.partials[name] = partial; - } - }, - unregisterPartial: function(name) { - delete this.partials[name]; - } - }; - - function registerDefaultHelpers(instance) { - instance.registerHelper('helperMissing', function(/* [args, ]options */) { - if(arguments.length === 1) { - // A missing field in a {{foo}} constuct. - return undefined; - } else { - // Someone is actually trying to call something, blow up. - throw new Exception("Missing helper: '" + arguments[arguments.length-1].name + "'"); - } - }); - - instance.registerHelper('blockHelperMissing', function(context, options) { - var inverse = options.inverse, - fn = options.fn; - - if(context === true) { - return fn(this); - } else if(context === false || context == null) { - return inverse(this); - } else if (isArray(context)) { - if(context.length > 0) { - if (options.ids) { - options.ids = [options.name]; - } - - return instance.helpers.each(context, options); - } else { - return inverse(this); - } - } else { - if (options.data && options.ids) { - var data = createFrame(options.data); - data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name); - options = {data: data}; - } - - return fn(context, options); - } - }); - - instance.registerHelper('each', function(context, options) { - if (!options) { - throw new Exception('Must pass iterator to #each'); - } - - var fn = options.fn, inverse = options.inverse; - var i = 0, ret = "", data; - - var contextPath; - if (options.data && options.ids) { - contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.'; - } - - if (isFunction(context)) { context = context.call(this); } - - if (options.data) { - data = createFrame(options.data); - } - - if(context && typeof context === 'object') { - if (isArray(context)) { - for(var j = context.length; i 0) { - throw new Exception("Invalid path: " + original, this); - } else if (part === "..") { - depth++; - depthString += '../'; - } else { - this.isScoped = true; - } - } else { - dig.push(part); - } - } - - this.original = original; - this.parts = dig; - this.string = dig.join('.'); - this.depth = depth; - this.idName = depthString + this.string; - - // an ID is simple if it only has one part, and that part is not - // `..` or `this`. - this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; - - this.stringModeValue = this.string; - }, - - PartialNameNode: function(name, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "PARTIAL_NAME"; - this.name = name.original; - }, - - DataNode: function(id, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "DATA"; - this.id = id; - this.stringModeValue = id.stringModeValue; - this.idName = '@' + id.stringModeValue; - }, - - StringNode: function(string, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "STRING"; - this.original = - this.string = - this.stringModeValue = string; - }, - - NumberNode: function(number, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "NUMBER"; - this.original = - this.number = number; - this.stringModeValue = Number(number); - }, - - BooleanNode: function(bool, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "BOOLEAN"; - this.bool = bool; - this.stringModeValue = bool === "true"; - }, - - CommentNode: function(comment, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "comment"; - this.comment = comment; - - this.strip = { - inlineStandalone: true - }; - } - }; - - - // Must be exported as an object rather than the root of the module as the jison lexer - // most modify the object to operate properly. - __exports__["default"] = AST; - }); -define( - 'handlebars/compiler/parser',["exports"], - function(__exports__) { - - /* jshint ignore:start */ - /* istanbul ignore next */ - /* Jison generated parser */ - var handlebars = (function(){ - var parser = {trace: function trace() { }, - yy: {}, - symbols_: {"error":2,"root":3,"program":4,"EOF":5,"program_repetition0":6,"statement":7,"mustache":8,"block":9,"rawBlock":10,"partial":11,"CONTENT":12,"COMMENT":13,"openRawBlock":14,"END_RAW_BLOCK":15,"OPEN_RAW_BLOCK":16,"sexpr":17,"CLOSE_RAW_BLOCK":18,"openBlock":19,"block_option0":20,"closeBlock":21,"openInverse":22,"block_option1":23,"OPEN_BLOCK":24,"CLOSE":25,"OPEN_INVERSE":26,"inverseAndProgram":27,"INVERSE":28,"OPEN_ENDBLOCK":29,"path":30,"OPEN":31,"OPEN_UNESCAPED":32,"CLOSE_UNESCAPED":33,"OPEN_PARTIAL":34,"partialName":35,"param":36,"partial_option0":37,"partial_option1":38,"sexpr_repetition0":39,"sexpr_option0":40,"dataName":41,"STRING":42,"NUMBER":43,"BOOLEAN":44,"OPEN_SEXPR":45,"CLOSE_SEXPR":46,"hash":47,"hash_repetition_plus0":48,"hashSegment":49,"ID":50,"EQUALS":51,"DATA":52,"pathSegments":53,"SEP":54,"$accept":0,"$end":1}, - terminals_: {2:"error",5:"EOF",12:"CONTENT",13:"COMMENT",15:"END_RAW_BLOCK",16:"OPEN_RAW_BLOCK",18:"CLOSE_RAW_BLOCK",24:"OPEN_BLOCK",25:"CLOSE",26:"OPEN_INVERSE",28:"INVERSE",29:"OPEN_ENDBLOCK",31:"OPEN",32:"OPEN_UNESCAPED",33:"CLOSE_UNESCAPED",34:"OPEN_PARTIAL",42:"STRING",43:"NUMBER",44:"BOOLEAN",45:"OPEN_SEXPR",46:"CLOSE_SEXPR",50:"ID",51:"EQUALS",52:"DATA",54:"SEP"}, - productions_: [0,[3,2],[4,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[10,3],[14,3],[9,4],[9,4],[19,3],[22,3],[27,2],[21,3],[8,3],[8,3],[11,5],[11,4],[17,3],[17,1],[36,1],[36,1],[36,1],[36,1],[36,1],[36,3],[47,1],[49,3],[35,1],[35,1],[35,1],[41,2],[30,1],[53,3],[53,1],[6,0],[6,2],[20,0],[20,1],[23,0],[23,1],[37,0],[37,1],[38,0],[38,1],[39,0],[39,2],[40,0],[40,1],[48,1],[48,2]], - performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) { - - var $0 = $$.length - 1; - switch (yystate) { - case 1: yy.prepareProgram($$[$0-1].statements, true); return $$[$0-1]; - break; - case 2:this.$ = new yy.ProgramNode(yy.prepareProgram($$[$0]), {}, this._$); - break; - case 3:this.$ = $$[$0]; - break; - case 4:this.$ = $$[$0]; - break; - case 5:this.$ = $$[$0]; - break; - case 6:this.$ = $$[$0]; - break; - case 7:this.$ = new yy.ContentNode($$[$0], this._$); - break; - case 8:this.$ = new yy.CommentNode($$[$0], this._$); - break; - case 9:this.$ = new yy.RawBlockNode($$[$0-2], $$[$0-1], $$[$0], this._$); - break; - case 10:this.$ = new yy.MustacheNode($$[$0-1], null, '', '', this._$); - break; - case 11:this.$ = yy.prepareBlock($$[$0-3], $$[$0-2], $$[$0-1], $$[$0], false, this._$); - break; - case 12:this.$ = yy.prepareBlock($$[$0-3], $$[$0-2], $$[$0-1], $$[$0], true, this._$); - break; - case 13:this.$ = new yy.MustacheNode($$[$0-1], null, $$[$0-2], yy.stripFlags($$[$0-2], $$[$0]), this._$); - break; - case 14:this.$ = new yy.MustacheNode($$[$0-1], null, $$[$0-2], yy.stripFlags($$[$0-2], $$[$0]), this._$); - break; - case 15:this.$ = { strip: yy.stripFlags($$[$0-1], $$[$0-1]), program: $$[$0] }; - break; - case 16:this.$ = {path: $$[$0-1], strip: yy.stripFlags($$[$0-2], $$[$0])}; - break; - case 17:this.$ = new yy.MustacheNode($$[$0-1], null, $$[$0-2], yy.stripFlags($$[$0-2], $$[$0]), this._$); - break; - case 18:this.$ = new yy.MustacheNode($$[$0-1], null, $$[$0-2], yy.stripFlags($$[$0-2], $$[$0]), this._$); - break; - case 19:this.$ = new yy.PartialNode($$[$0-3], $$[$0-2], $$[$0-1], yy.stripFlags($$[$0-4], $$[$0]), this._$); - break; - case 20:this.$ = new yy.PartialNode($$[$0-2], undefined, $$[$0-1], yy.stripFlags($$[$0-3], $$[$0]), this._$); - break; - case 21:this.$ = new yy.SexprNode([$$[$0-2]].concat($$[$0-1]), $$[$0], this._$); - break; - case 22:this.$ = new yy.SexprNode([$$[$0]], null, this._$); - break; - case 23:this.$ = $$[$0]; - break; - case 24:this.$ = new yy.StringNode($$[$0], this._$); - break; - case 25:this.$ = new yy.NumberNode($$[$0], this._$); - break; - case 26:this.$ = new yy.BooleanNode($$[$0], this._$); - break; - case 27:this.$ = $$[$0]; - break; - case 28:$$[$0-1].isHelper = true; this.$ = $$[$0-1]; - break; - case 29:this.$ = new yy.HashNode($$[$0], this._$); - break; - case 30:this.$ = [$$[$0-2], $$[$0]]; - break; - case 31:this.$ = new yy.PartialNameNode($$[$0], this._$); - break; - case 32:this.$ = new yy.PartialNameNode(new yy.StringNode($$[$0], this._$), this._$); - break; - case 33:this.$ = new yy.PartialNameNode(new yy.NumberNode($$[$0], this._$)); - break; - case 34:this.$ = new yy.DataNode($$[$0], this._$); - break; - case 35:this.$ = new yy.IdNode($$[$0], this._$); - break; - case 36: $$[$0-2].push({part: $$[$0], separator: $$[$0-1]}); this.$ = $$[$0-2]; - break; - case 37:this.$ = [{part: $$[$0]}]; - break; - case 38:this.$ = []; - break; - case 39:$$[$0-1].push($$[$0]); - break; - case 48:this.$ = []; - break; - case 49:$$[$0-1].push($$[$0]); - break; - case 52:this.$ = [$$[$0]]; - break; - case 53:$$[$0-1].push($$[$0]); - break; - } - }, - table: [{3:1,4:2,5:[2,38],6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],31:[2,38],32:[2,38],34:[2,38]},{1:[3]},{5:[1,4]},{5:[2,2],7:5,8:6,9:7,10:8,11:9,12:[1,10],13:[1,11],14:16,16:[1,20],19:14,22:15,24:[1,18],26:[1,19],28:[2,2],29:[2,2],31:[1,12],32:[1,13],34:[1,17]},{1:[2,1]},{5:[2,39],12:[2,39],13:[2,39],16:[2,39],24:[2,39],26:[2,39],28:[2,39],29:[2,39],31:[2,39],32:[2,39],34:[2,39]},{5:[2,3],12:[2,3],13:[2,3],16:[2,3],24:[2,3],26:[2,3],28:[2,3],29:[2,3],31:[2,3],32:[2,3],34:[2,3]},{5:[2,4],12:[2,4],13:[2,4],16:[2,4],24:[2,4],26:[2,4],28:[2,4],29:[2,4],31:[2,4],32:[2,4],34:[2,4]},{5:[2,5],12:[2,5],13:[2,5],16:[2,5],24:[2,5],26:[2,5],28:[2,5],29:[2,5],31:[2,5],32:[2,5],34:[2,5]},{5:[2,6],12:[2,6],13:[2,6],16:[2,6],24:[2,6],26:[2,6],28:[2,6],29:[2,6],31:[2,6],32:[2,6],34:[2,6]},{5:[2,7],12:[2,7],13:[2,7],16:[2,7],24:[2,7],26:[2,7],28:[2,7],29:[2,7],31:[2,7],32:[2,7],34:[2,7]},{5:[2,8],12:[2,8],13:[2,8],16:[2,8],24:[2,8],26:[2,8],28:[2,8],29:[2,8],31:[2,8],32:[2,8],34:[2,8]},{17:21,30:22,41:23,50:[1,26],52:[1,25],53:24},{17:27,30:22,41:23,50:[1,26],52:[1,25],53:24},{4:28,6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],28:[2,38],29:[2,38],31:[2,38],32:[2,38],34:[2,38]},{4:29,6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],28:[2,38],29:[2,38],31:[2,38],32:[2,38],34:[2,38]},{12:[1,30]},{30:32,35:31,42:[1,33],43:[1,34],50:[1,26],53:24},{17:35,30:22,41:23,50:[1,26],52:[1,25],53:24},{17:36,30:22,41:23,50:[1,26],52:[1,25],53:24},{17:37,30:22,41:23,50:[1,26],52:[1,25],53:24},{25:[1,38]},{18:[2,48],25:[2,48],33:[2,48],39:39,42:[2,48],43:[2,48],44:[2,48],45:[2,48],46:[2,48],50:[2,48],52:[2,48]},{18:[2,22],25:[2,22],33:[2,22],46:[2,22]},{18:[2,35],25:[2,35],33:[2,35],42:[2,35],43:[2,35],44:[2,35],45:[2,35],46:[2,35],50:[2,35],52:[2,35],54:[1,40]},{30:41,50:[1,26],53:24},{18:[2,37],25:[2,37],33:[2,37],42:[2,37],43:[2,37],44:[2,37],45:[2,37],46:[2,37],50:[2,37],52:[2,37],54:[2,37]},{33:[1,42]},{20:43,27:44,28:[1,45],29:[2,40]},{23:46,27:47,28:[1,45],29:[2,42]},{15:[1,48]},{25:[2,46],30:51,36:49,38:50,41:55,42:[1,52],43:[1,53],44:[1,54],45:[1,56],47:57,48:58,49:60,50:[1,59],52:[1,25],53:24},{25:[2,31],42:[2,31],43:[2,31],44:[2,31],45:[2,31],50:[2,31],52:[2,31]},{25:[2,32],42:[2,32],43:[2,32],44:[2,32],45:[2,32],50:[2,32],52:[2,32]},{25:[2,33],42:[2,33],43:[2,33],44:[2,33],45:[2,33],50:[2,33],52:[2,33]},{25:[1,61]},{25:[1,62]},{18:[1,63]},{5:[2,17],12:[2,17],13:[2,17],16:[2,17],24:[2,17],26:[2,17],28:[2,17],29:[2,17],31:[2,17],32:[2,17],34:[2,17]},{18:[2,50],25:[2,50],30:51,33:[2,50],36:65,40:64,41:55,42:[1,52],43:[1,53],44:[1,54],45:[1,56],46:[2,50],47:66,48:58,49:60,50:[1,59],52:[1,25],53:24},{50:[1,67]},{18:[2,34],25:[2,34],33:[2,34],42:[2,34],43:[2,34],44:[2,34],45:[2,34],46:[2,34],50:[2,34],52:[2,34]},{5:[2,18],12:[2,18],13:[2,18],16:[2,18],24:[2,18],26:[2,18],28:[2,18],29:[2,18],31:[2,18],32:[2,18],34:[2,18]},{21:68,29:[1,69]},{29:[2,41]},{4:70,6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],29:[2,38],31:[2,38],32:[2,38],34:[2,38]},{21:71,29:[1,69]},{29:[2,43]},{5:[2,9],12:[2,9],13:[2,9],16:[2,9],24:[2,9],26:[2,9],28:[2,9],29:[2,9],31:[2,9],32:[2,9],34:[2,9]},{25:[2,44],37:72,47:73,48:58,49:60,50:[1,74]},{25:[1,75]},{18:[2,23],25:[2,23],33:[2,23],42:[2,23],43:[2,23],44:[2,23],45:[2,23],46:[2,23],50:[2,23],52:[2,23]},{18:[2,24],25:[2,24],33:[2,24],42:[2,24],43:[2,24],44:[2,24],45:[2,24],46:[2,24],50:[2,24],52:[2,24]},{18:[2,25],25:[2,25],33:[2,25],42:[2,25],43:[2,25],44:[2,25],45:[2,25],46:[2,25],50:[2,25],52:[2,25]},{18:[2,26],25:[2,26],33:[2,26],42:[2,26],43:[2,26],44:[2,26],45:[2,26],46:[2,26],50:[2,26],52:[2,26]},{18:[2,27],25:[2,27],33:[2,27],42:[2,27],43:[2,27],44:[2,27],45:[2,27],46:[2,27],50:[2,27],52:[2,27]},{17:76,30:22,41:23,50:[1,26],52:[1,25],53:24},{25:[2,47]},{18:[2,29],25:[2,29],33:[2,29],46:[2,29],49:77,50:[1,74]},{18:[2,37],25:[2,37],33:[2,37],42:[2,37],43:[2,37],44:[2,37],45:[2,37],46:[2,37],50:[2,37],51:[1,78],52:[2,37],54:[2,37]},{18:[2,52],25:[2,52],33:[2,52],46:[2,52],50:[2,52]},{12:[2,13],13:[2,13],16:[2,13],24:[2,13],26:[2,13],28:[2,13],29:[2,13],31:[2,13],32:[2,13],34:[2,13]},{12:[2,14],13:[2,14],16:[2,14],24:[2,14],26:[2,14],28:[2,14],29:[2,14],31:[2,14],32:[2,14],34:[2,14]},{12:[2,10]},{18:[2,21],25:[2,21],33:[2,21],46:[2,21]},{18:[2,49],25:[2,49],33:[2,49],42:[2,49],43:[2,49],44:[2,49],45:[2,49],46:[2,49],50:[2,49],52:[2,49]},{18:[2,51],25:[2,51],33:[2,51],46:[2,51]},{18:[2,36],25:[2,36],33:[2,36],42:[2,36],43:[2,36],44:[2,36],45:[2,36],46:[2,36],50:[2,36],52:[2,36],54:[2,36]},{5:[2,11],12:[2,11],13:[2,11],16:[2,11],24:[2,11],26:[2,11],28:[2,11],29:[2,11],31:[2,11],32:[2,11],34:[2,11]},{30:79,50:[1,26],53:24},{29:[2,15]},{5:[2,12],12:[2,12],13:[2,12],16:[2,12],24:[2,12],26:[2,12],28:[2,12],29:[2,12],31:[2,12],32:[2,12],34:[2,12]},{25:[1,80]},{25:[2,45]},{51:[1,78]},{5:[2,20],12:[2,20],13:[2,20],16:[2,20],24:[2,20],26:[2,20],28:[2,20],29:[2,20],31:[2,20],32:[2,20],34:[2,20]},{46:[1,81]},{18:[2,53],25:[2,53],33:[2,53],46:[2,53],50:[2,53]},{30:51,36:82,41:55,42:[1,52],43:[1,53],44:[1,54],45:[1,56],50:[1,26],52:[1,25],53:24},{25:[1,83]},{5:[2,19],12:[2,19],13:[2,19],16:[2,19],24:[2,19],26:[2,19],28:[2,19],29:[2,19],31:[2,19],32:[2,19],34:[2,19]},{18:[2,28],25:[2,28],33:[2,28],42:[2,28],43:[2,28],44:[2,28],45:[2,28],46:[2,28],50:[2,28],52:[2,28]},{18:[2,30],25:[2,30],33:[2,30],46:[2,30],50:[2,30]},{5:[2,16],12:[2,16],13:[2,16],16:[2,16],24:[2,16],26:[2,16],28:[2,16],29:[2,16],31:[2,16],32:[2,16],34:[2,16]}], - defaultActions: {4:[2,1],44:[2,41],47:[2,43],57:[2,47],63:[2,10],70:[2,15],73:[2,45]}, - parseError: function parseError(str, hash) { - throw new Error(str); - }, - parse: function parse(input) { - var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = "", yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; - this.lexer.setInput(input); - this.lexer.yy = this.yy; - this.yy.lexer = this.lexer; - this.yy.parser = this; - if (typeof this.lexer.yylloc == "undefined") - this.lexer.yylloc = {}; - var yyloc = this.lexer.yylloc; - lstack.push(yyloc); - var ranges = this.lexer.options && this.lexer.options.ranges; - if (typeof this.yy.parseError === "function") - this.parseError = this.yy.parseError; - function popStack(n) { - stack.length = stack.length - 2 * n; - vstack.length = vstack.length - n; - lstack.length = lstack.length - n; - } - function lex() { - var token; - token = self.lexer.lex() || 1; - if (typeof token !== "number") { - token = self.symbols_[token] || token; - } - return token; - } - var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; - while (true) { - state = stack[stack.length - 1]; - if (this.defaultActions[state]) { - action = this.defaultActions[state]; - } else { - if (symbol === null || typeof symbol == "undefined") { - symbol = lex(); - } - action = table[state] && table[state][symbol]; - } - if (typeof action === "undefined" || !action.length || !action[0]) { - var errStr = ""; - if (!recovering) { - expected = []; - for (p in table[state]) - if (this.terminals_[p] && p > 2) { - expected.push("'" + this.terminals_[p] + "'"); - } - if (this.lexer.showPosition) { - errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'"; - } else { - errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); - } - this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); - } - } - if (action[0] instanceof Array && action.length > 1) { - throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); - } - switch (action[0]) { - case 1: - stack.push(symbol); - vstack.push(this.lexer.yytext); - lstack.push(this.lexer.yylloc); - stack.push(action[1]); - symbol = null; - if (!preErrorSymbol) { - yyleng = this.lexer.yyleng; - yytext = this.lexer.yytext; - yylineno = this.lexer.yylineno; - yyloc = this.lexer.yylloc; - if (recovering > 0) - recovering--; - } else { - symbol = preErrorSymbol; - preErrorSymbol = null; - } - break; - case 2: - len = this.productions_[action[1]][1]; - yyval.$ = vstack[vstack.length - len]; - yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; - if (ranges) { - yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; - } - r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); - if (typeof r !== "undefined") { - return r; - } - if (len) { - stack = stack.slice(0, -1 * len * 2); - vstack = vstack.slice(0, -1 * len); - lstack = lstack.slice(0, -1 * len); - } - stack.push(this.productions_[action[1]][0]); - vstack.push(yyval.$); - lstack.push(yyval._$); - newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; - stack.push(newState); - break; - case 3: - return true; - } - } - return true; - } - }; - /* Jison generated lexer */ - var lexer = (function(){ - var lexer = ({EOF:1, - parseError:function parseError(str, hash) { - if (this.yy.parser) { - this.yy.parser.parseError(str, hash); - } else { - throw new Error(str); - } - }, - setInput:function (input) { - this._input = input; - this._more = this._less = this.done = false; - this.yylineno = this.yyleng = 0; - this.yytext = this.matched = this.match = ''; - this.conditionStack = ['INITIAL']; - this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; - if (this.options.ranges) this.yylloc.range = [0,0]; - this.offset = 0; - return this; - }, - input:function () { - var ch = this._input[0]; - this.yytext += ch; - this.yyleng++; - this.offset++; - this.match += ch; - this.matched += ch; - var lines = ch.match(/(?:\r\n?|\n).*/g); - if (lines) { - this.yylineno++; - this.yylloc.last_line++; - } else { - this.yylloc.last_column++; - } - if (this.options.ranges) this.yylloc.range[1]++; - - this._input = this._input.slice(1); - return ch; - }, - unput:function (ch) { - var len = ch.length; - var lines = ch.split(/(?:\r\n?|\n)/g); - - this._input = ch + this._input; - this.yytext = this.yytext.substr(0, this.yytext.length-len-1); - //this.yyleng -= len; - this.offset -= len; - var oldLines = this.match.split(/(?:\r\n?|\n)/g); - this.match = this.match.substr(0, this.match.length-1); - this.matched = this.matched.substr(0, this.matched.length-1); - - if (lines.length-1) this.yylineno -= lines.length-1; - var r = this.yylloc.range; - - this.yylloc = {first_line: this.yylloc.first_line, - last_line: this.yylineno+1, - first_column: this.yylloc.first_column, - last_column: lines ? - (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length: - this.yylloc.first_column - len - }; - - if (this.options.ranges) { - this.yylloc.range = [r[0], r[0] + this.yyleng - len]; - } - return this; - }, - more:function () { - this._more = true; - return this; - }, - less:function (n) { - this.unput(this.match.slice(n)); - }, - pastInput:function () { - var past = this.matched.substr(0, this.matched.length - this.match.length); - return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); - }, - upcomingInput:function () { - var next = this.match; - if (next.length < 20) { - next += this._input.substr(0, 20-next.length); - } - return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); - }, - showPosition:function () { - var pre = this.pastInput(); - var c = new Array(pre.length + 1).join("-"); - return pre + this.upcomingInput() + "\n" + c+"^"; - }, - next:function () { - if (this.done) { - return this.EOF; - } - if (!this._input) this.done = true; - - var token, - match, - tempMatch, - index, - col, - lines; - if (!this._more) { - this.yytext = ''; - this.match = ''; - } - var rules = this._currentRules(); - for (var i=0;i < rules.length; i++) { - tempMatch = this._input.match(this.rules[rules[i]]); - if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { - match = tempMatch; - index = i; - if (!this.options.flex) break; - } - } - if (match) { - lines = match[0].match(/(?:\r\n?|\n).*/g); - if (lines) this.yylineno += lines.length; - this.yylloc = {first_line: this.yylloc.last_line, - last_line: this.yylineno+1, - first_column: this.yylloc.last_column, - last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length}; - this.yytext += match[0]; - this.match += match[0]; - this.matches = match; - this.yyleng = this.yytext.length; - if (this.options.ranges) { - this.yylloc.range = [this.offset, this.offset += this.yyleng]; - } - this._more = false; - this._input = this._input.slice(match[0].length); - this.matched += match[0]; - token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]); - if (this.done && this._input) this.done = false; - if (token) return token; - else return; - } - if (this._input === "") { - return this.EOF; - } else { - return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), - {text: "", token: null, line: this.yylineno}); - } - }, - lex:function lex() { - var r = this.next(); - if (typeof r !== 'undefined') { - return r; - } else { - return this.lex(); - } - }, - begin:function begin(condition) { - this.conditionStack.push(condition); - }, - popState:function popState() { - return this.conditionStack.pop(); - }, - _currentRules:function _currentRules() { - return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; - }, - topState:function () { - return this.conditionStack[this.conditionStack.length-2]; - }, - pushState:function begin(condition) { - this.begin(condition); - }}); - lexer.options = {}; - lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { - - - function strip(start, end) { - return yy_.yytext = yy_.yytext.substr(start, yy_.yyleng-end); - } - - - var YYSTATE=YY_START - switch($avoiding_name_collisions) { - case 0: - if(yy_.yytext.slice(-2) === "\\\\") { - strip(0,1); - this.begin("mu"); - } else if(yy_.yytext.slice(-1) === "\\") { - strip(0,1); - this.begin("emu"); - } else { - this.begin("mu"); - } - if(yy_.yytext) return 12; - - break; - case 1:return 12; - break; - case 2: - this.popState(); - return 12; - - break; - case 3: - yy_.yytext = yy_.yytext.substr(5, yy_.yyleng-9); - this.popState(); - return 15; - - break; - case 4: return 12; - break; - case 5:strip(0,4); this.popState(); return 13; - break; - case 6:return 45; - break; - case 7:return 46; - break; - case 8: return 16; - break; - case 9: - this.popState(); - this.begin('raw'); - return 18; - - break; - case 10:return 34; - break; - case 11:return 24; - break; - case 12:return 29; - break; - case 13:this.popState(); return 28; - break; - case 14:this.popState(); return 28; - break; - case 15:return 26; - break; - case 16:return 26; - break; - case 17:return 32; - break; - case 18:return 31; - break; - case 19:this.popState(); this.begin('com'); - break; - case 20:strip(3,5); this.popState(); return 13; - break; - case 21:return 31; - break; - case 22:return 51; - break; - case 23:return 50; - break; - case 24:return 50; - break; - case 25:return 54; - break; - case 26:// ignore whitespace - break; - case 27:this.popState(); return 33; - break; - case 28:this.popState(); return 25; - break; - case 29:yy_.yytext = strip(1,2).replace(/\\"/g,'"'); return 42; - break; - case 30:yy_.yytext = strip(1,2).replace(/\\'/g,"'"); return 42; - break; - case 31:return 52; - break; - case 32:return 44; - break; - case 33:return 44; - break; - case 34:return 43; - break; - case 35:return 50; - break; - case 36:yy_.yytext = strip(1,2); return 50; - break; - case 37:return 'INVALID'; - break; - case 38:return 5; - break; - } - }; - lexer.rules = [/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|\\\{\{|\\\\\{\{|$)))/,/^(?:\{\{\{\{\/[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.])\}\}\}\})/,/^(?:[^\x00]*?(?=(\{\{\{\{\/)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\()/,/^(?:\))/,/^(?:\{\{\{\{)/,/^(?:\}\}\}\})/,/^(?:\{\{(~)?>)/,/^(?:\{\{(~)?#)/,/^(?:\{\{(~)?\/)/,/^(?:\{\{(~)?\^\s*(~)?\}\})/,/^(?:\{\{(~)?\s*else\s*(~)?\}\})/,/^(?:\{\{(~)?\^)/,/^(?:\{\{(~)?\s*else\b)/,/^(?:\{\{(~)?\{)/,/^(?:\{\{(~)?&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{(~)?)/,/^(?:=)/,/^(?:\.\.)/,/^(?:\.(?=([=~}\s\/.)])))/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}(~)?\}\})/,/^(?:(~)?\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=([~}\s)])))/,/^(?:false(?=([~}\s)])))/,/^(?:-?[0-9]+(?:\.[0-9]+)?(?=([~}\s)])))/,/^(?:([^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=([=~}\s\/.)]))))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/]; - lexer.conditions = {"mu":{"rules":[6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"com":{"rules":[5],"inclusive":false},"raw":{"rules":[3,4],"inclusive":false},"INITIAL":{"rules":[0,1,38],"inclusive":true}}; - return lexer;})() - parser.lexer = lexer; - function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser; - return new Parser; - })();__exports__["default"] = handlebars; - /* jshint ignore:end */ - }); -define( - 'handlebars/compiler/helpers',["../exception","exports"], - function(__dependency1__, __exports__) { - - var Exception = __dependency1__["default"]; - - function stripFlags(open, close) { - return { - left: open.charAt(2) === '~', - right: close.charAt(close.length-3) === '~' - }; - } - - __exports__.stripFlags = stripFlags; - function prepareBlock(mustache, program, inverseAndProgram, close, inverted, locInfo) { - /*jshint -W040 */ - if (mustache.sexpr.id.original !== close.path.original) { - throw new Exception(mustache.sexpr.id.original + ' doesn\'t match ' + close.path.original, mustache); - } - - var inverse = inverseAndProgram && inverseAndProgram.program; - - var strip = { - left: mustache.strip.left, - right: close.strip.right, - - // Determine the standalone candiacy. Basically flag our content as being possibly standalone - // so our parent can determine if we actually are standalone - openStandalone: isNextWhitespace(program.statements), - closeStandalone: isPrevWhitespace((inverse || program).statements) - }; - - if (mustache.strip.right) { - omitRight(program.statements, null, true); - } - - if (inverse) { - var inverseStrip = inverseAndProgram.strip; - - if (inverseStrip.left) { - omitLeft(program.statements, null, true); - } - if (inverseStrip.right) { - omitRight(inverse.statements, null, true); - } - if (close.strip.left) { - omitLeft(inverse.statements, null, true); - } - - // Find standalone else statments - if (isPrevWhitespace(program.statements) - && isNextWhitespace(inverse.statements)) { - - omitLeft(program.statements); - omitRight(inverse.statements); - } - } else { - if (close.strip.left) { - omitLeft(program.statements, null, true); - } - } - - if (inverted) { - return new this.BlockNode(mustache, inverse, program, strip, locInfo); - } else { - return new this.BlockNode(mustache, program, inverse, strip, locInfo); - } - } - - __exports__.prepareBlock = prepareBlock; - function prepareProgram(statements, isRoot) { - for (var i = 0, l = statements.length; i < l; i++) { - var current = statements[i], - strip = current.strip; - - if (!strip) { - continue; - } - - var _isPrevWhitespace = isPrevWhitespace(statements, i, isRoot, current.type === 'partial'), - _isNextWhitespace = isNextWhitespace(statements, i, isRoot), - - openStandalone = strip.openStandalone && _isPrevWhitespace, - closeStandalone = strip.closeStandalone && _isNextWhitespace, - inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace; - - if (strip.right) { - omitRight(statements, i, true); - } - if (strip.left) { - omitLeft(statements, i, true); - } - - if (inlineStandalone) { - omitRight(statements, i); - - if (omitLeft(statements, i)) { - // If we are on a standalone node, save the indent info for partials - if (current.type === 'partial') { - current.indent = (/([ \t]+$)/).exec(statements[i-1].original) ? RegExp.$1 : ''; - } - } - } - if (openStandalone) { - omitRight((current.program || current.inverse).statements); - - // Strip out the previous content node if it's whitespace only - omitLeft(statements, i); - } - if (closeStandalone) { - // Always strip the next node - omitRight(statements, i); - - omitLeft((current.inverse || current.program).statements); - } - } - - return statements; - } - - __exports__.prepareProgram = prepareProgram;function isPrevWhitespace(statements, i, isRoot) { - if (i === undefined) { - i = statements.length; - } - - // Nodes that end with newlines are considered whitespace (but are special - // cased for strip operations) - var prev = statements[i-1], - sibling = statements[i-2]; - if (!prev) { - return isRoot; - } - - if (prev.type === 'content') { - return (sibling || !isRoot ? (/\r?\n\s*?$/) : (/(^|\r?\n)\s*?$/)).test(prev.original); - } - } - function isNextWhitespace(statements, i, isRoot) { - if (i === undefined) { - i = -1; - } - - var next = statements[i+1], - sibling = statements[i+2]; - if (!next) { - return isRoot; - } - - if (next.type === 'content') { - return (sibling || !isRoot ? (/^\s*?\r?\n/) : (/^\s*?(\r?\n|$)/)).test(next.original); - } - } - - // Marks the node to the right of the position as omitted. - // I.e. {{foo}}' ' will mark the ' ' node as omitted. - // - // If i is undefined, then the first child will be marked as such. - // - // If mulitple is truthy then all whitespace will be stripped out until non-whitespace - // content is met. - function omitRight(statements, i, multiple) { - var current = statements[i == null ? 0 : i + 1]; - if (!current || current.type !== 'content' || (!multiple && current.rightStripped)) { - return; - } - - var original = current.string; - current.string = current.string.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), ''); - current.rightStripped = current.string !== original; - } - - // Marks the node to the left of the position as omitted. - // I.e. ' '{{foo}} will mark the ' ' node as omitted. - // - // If i is undefined then the last child will be marked as such. - // - // If mulitple is truthy then all whitespace will be stripped out until non-whitespace - // content is met. - function omitLeft(statements, i, multiple) { - var current = statements[i == null ? statements.length - 1 : i - 1]; - if (!current || current.type !== 'content' || (!multiple && current.leftStripped)) { - return; - } - - // We omit the last node if it's whitespace only and not preceeded by a non-content node. - var original = current.string; - current.string = current.string.replace(multiple ? (/\s+$/) : (/[ \t]+$/), ''); - current.leftStripped = current.string !== original; - return current.leftStripped; - } - }); -define( - 'handlebars/compiler/base',["./parser","./ast","./helpers","../utils","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { - - var parser = __dependency1__["default"]; - var AST = __dependency2__["default"]; - var Helpers = __dependency3__; - var extend = __dependency4__.extend; - - __exports__.parser = parser; - - var yy = {}; - extend(yy, Helpers, AST); - - function parse(input) { - // Just return if an already-compile AST was passed in. - if (input.constructor === AST.ProgramNode) { return input; } - - parser.yy = yy; - - return parser.parse(input); - } - - __exports__.parse = parse; - }); -define( - 'handlebars/compiler/compiler',["../exception","../utils","exports"], - function(__dependency1__, __dependency2__, __exports__) { - - var Exception = __dependency1__["default"]; - var isArray = __dependency2__.isArray; - - var slice = [].slice; - - function Compiler() {} - - __exports__.Compiler = Compiler;// the foundHelper register will disambiguate helper lookup from finding a - // function in a context. This is necessary for mustache compatibility, which - // requires that context functions in blocks are evaluated by blockHelperMissing, - // and then proceed as if the resulting value was provided to blockHelperMissing. - - Compiler.prototype = { - compiler: Compiler, - - equals: function(other) { - var len = this.opcodes.length; - if (other.opcodes.length !== len) { - return false; - } - - for (var i = 0; i < len; i++) { - var opcode = this.opcodes[i], - otherOpcode = other.opcodes[i]; - if (opcode.opcode !== otherOpcode.opcode || !argEquals(opcode.args, otherOpcode.args)) { - return false; - } - } - - // We know that length is the same between the two arrays because they are directly tied - // to the opcode behavior above. - len = this.children.length; - for (i = 0; i < len; i++) { - if (!this.children[i].equals(other.children[i])) { - return false; - } - } - - return true; - }, - - guid: 0, - - compile: function(program, options) { - this.opcodes = []; - this.children = []; - this.depths = {list: []}; - this.options = options; - this.stringParams = options.stringParams; - this.trackIds = options.trackIds; - - // These changes will propagate to the other compiler components - var knownHelpers = this.options.knownHelpers; - this.options.knownHelpers = { - 'helperMissing': true, - 'blockHelperMissing': true, - 'each': true, - 'if': true, - 'unless': true, - 'with': true, - 'log': true, - 'lookup': true - }; - if (knownHelpers) { - for (var name in knownHelpers) { - this.options.knownHelpers[name] = knownHelpers[name]; - } - } - - return this.accept(program); - }, - - accept: function(node) { - return this[node.type](node); - }, - - program: function(program) { - var statements = program.statements; - - for(var i=0, l=statements.length; i 0) { - varDeclarations += ", " + locals.join(", "); - } - - // Generate minimizer alias mappings - for (var alias in this.aliases) { - if (this.aliases.hasOwnProperty(alias)) { - varDeclarations += ', ' + alias + '=' + this.aliases[alias]; - } - } - - var params = ["depth0", "helpers", "partials", "data"]; - - if (this.useDepths) { - params.push('depths'); - } - - // Perform a second pass over the output to merge content when possible - var source = this.mergeSource(varDeclarations); - - if (asObject) { - params.push(source); - - return Function.apply(this, params); - } else { - return 'function(' + params.join(',') + ') {\n ' + source + '}'; - } - }, - mergeSource: function(varDeclarations) { - var source = '', - buffer, - appendOnly = !this.forceBuffer, - appendFirst; - - for (var i = 0, len = this.source.length; i < len; i++) { - var line = this.source[i]; - if (line.appendToBuffer) { - if (buffer) { - buffer = buffer + '\n + ' + line.content; - } else { - buffer = line.content; - } - } else { - if (buffer) { - if (!source) { - appendFirst = true; - source = buffer + ';\n '; - } else { - source += 'buffer += ' + buffer + ';\n '; - } - buffer = undefined; - } - source += line + '\n '; - - if (!this.environment.isSimple) { - appendOnly = false; - } - } - } - - if (appendOnly) { - if (buffer || !source) { - source += 'return ' + (buffer || '""') + ';\n'; - } - } else { - varDeclarations += ", buffer = " + (appendFirst ? '' : this.initializeBuffer()); - if (buffer) { - source += 'return buffer + ' + buffer + ';\n'; - } else { - source += 'return buffer;\n'; - } - } - - if (varDeclarations) { - source = 'var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n ') + source; - } - - return source; - }, - - // [blockValue] - // - // On stack, before: hash, inverse, program, value - // On stack, after: return value of blockHelperMissing - // - // The purpose of this opcode is to take a block of the form - // `{{#this.foo}}...{{/this.foo}}`, resolve the value of `foo`, and - // replace it on the stack with the result of properly - // invoking blockHelperMissing. - blockValue: function(name) { - this.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; - - var params = [this.contextName(0)]; - this.setupParams(name, 0, params); - - var blockName = this.popStack(); - params.splice(1, 0, blockName); - - this.push('blockHelperMissing.call(' + params.join(', ') + ')'); - }, - - // [ambiguousBlockValue] - // - // On stack, before: hash, inverse, program, value - // Compiler value, before: lastHelper=value of last found helper, if any - // On stack, after, if no lastHelper: same as [blockValue] - // On stack, after, if lastHelper: value - ambiguousBlockValue: function() { - this.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; - - // We're being a bit cheeky and reusing the options value from the prior exec - var params = [this.contextName(0)]; - this.setupParams('', 0, params, true); - - this.flushInline(); - - var current = this.topStack(); - params.splice(1, 0, current); - - this.pushSource("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }"); - }, - - // [appendContent] - // - // On stack, before: ... - // On stack, after: ... - // - // Appends the string value of `content` to the current buffer - appendContent: function(content) { - if (this.pendingContent) { - content = this.pendingContent + content; - } - - this.pendingContent = content; - }, - - // [append] - // - // On stack, before: value, ... - // On stack, after: ... - // - // Coerces `value` to a String and appends it to the current buffer. - // - // If `value` is truthy, or 0, it is coerced into a string and appended - // Otherwise, the empty string is appended - append: function() { - // Force anything that is inlined onto the stack so we don't have duplication - // when we examine local - this.flushInline(); - var local = this.popStack(); - this.pushSource('if (' + local + ' != null) { ' + this.appendToBuffer(local) + ' }'); - if (this.environment.isSimple) { - this.pushSource("else { " + this.appendToBuffer("''") + " }"); - } - }, - - // [appendEscaped] - // - // On stack, before: value, ... - // On stack, after: ... - // - // Escape `value` and append it to the buffer - appendEscaped: function() { - this.aliases.escapeExpression = 'this.escapeExpression'; - - this.pushSource(this.appendToBuffer("escapeExpression(" + this.popStack() + ")")); - }, - - // [getContext] - // - // On stack, before: ... - // On stack, after: ... - // Compiler value, after: lastContext=depth - // - // Set the value of the `lastContext` compiler value to the depth - getContext: function(depth) { - this.lastContext = depth; - }, - - // [pushContext] - // - // On stack, before: ... - // On stack, after: currentContext, ... - // - // Pushes the value of the current context onto the stack. - pushContext: function() { - this.pushStackLiteral(this.contextName(this.lastContext)); - }, - - // [lookupOnContext] - // - // On stack, before: ... - // On stack, after: currentContext[name], ... - // - // Looks up the value of `name` on the current context and pushes - // it onto the stack. - lookupOnContext: function(parts, falsy, scoped) { - /*jshint -W083 */ - var i = 0, - len = parts.length; - - if (!scoped && this.options.compat && !this.lastContext) { - // The depthed query is expected to handle the undefined logic for the root level that - // is implemented below, so we evaluate that directly in compat mode - this.push(this.depthedLookup(parts[i++])); - } else { - this.pushContext(); - } - - for (; i < len; i++) { - this.replaceStack(function(current) { - var lookup = this.nameLookup(current, parts[i], 'context'); - // We want to ensure that zero and false are handled properly if the context (falsy flag) - // needs to have the special handling for these values. - if (!falsy) { - return ' != null ? ' + lookup + ' : ' + current; - } else { - // Otherwise we can use generic falsy handling - return ' && ' + lookup; - } - }); - } - }, - - // [lookupData] - // - // On stack, before: ... - // On stack, after: data, ... - // - // Push the data lookup operator - lookupData: function(depth, parts) { - /*jshint -W083 */ - if (!depth) { - this.pushStackLiteral('data'); - } else { - this.pushStackLiteral('this.data(data, ' + depth + ')'); - } - - var len = parts.length; - for (var i = 0; i < len; i++) { - this.replaceStack(function(current) { - return ' && ' + this.nameLookup(current, parts[i], 'data'); - }); - } - }, - - // [resolvePossibleLambda] - // - // On stack, before: value, ... - // On stack, after: resolved value, ... - // - // If the `value` is a lambda, replace it on the stack by - // the return value of the lambda - resolvePossibleLambda: function() { - this.aliases.lambda = 'this.lambda'; - - this.push('lambda(' + this.popStack() + ', ' + this.contextName(0) + ')'); - }, - - // [pushStringParam] - // - // On stack, before: ... - // On stack, after: string, currentContext, ... - // - // This opcode is designed for use in string mode, which - // provides the string value of a parameter along with its - // depth rather than resolving it immediately. - pushStringParam: function(string, type) { - this.pushContext(); - this.pushString(type); - - // If it's a subexpression, the string result - // will be pushed after this opcode. - if (type !== 'sexpr') { - if (typeof string === 'string') { - this.pushString(string); - } else { - this.pushStackLiteral(string); - } - } - }, - - emptyHash: function() { - this.pushStackLiteral('{}'); - - if (this.trackIds) { - this.push('{}'); // hashIds - } - if (this.stringParams) { - this.push('{}'); // hashContexts - this.push('{}'); // hashTypes - } - }, - pushHash: function() { - if (this.hash) { - this.hashes.push(this.hash); - } - this.hash = {values: [], types: [], contexts: [], ids: []}; - }, - popHash: function() { - var hash = this.hash; - this.hash = this.hashes.pop(); - - if (this.trackIds) { - this.push('{' + hash.ids.join(',') + '}'); - } - if (this.stringParams) { - this.push('{' + hash.contexts.join(',') + '}'); - this.push('{' + hash.types.join(',') + '}'); - } - - this.push('{\n ' + hash.values.join(',\n ') + '\n }'); - }, - - // [pushString] - // - // On stack, before: ... - // On stack, after: quotedString(string), ... - // - // Push a quoted version of `string` onto the stack - pushString: function(string) { - this.pushStackLiteral(this.quotedString(string)); - }, - - // [push] - // - // On stack, before: ... - // On stack, after: expr, ... - // - // Push an expression onto the stack - push: function(expr) { - this.inlineStack.push(expr); - return expr; - }, - - // [pushLiteral] - // - // On stack, before: ... - // On stack, after: value, ... - // - // Pushes a value onto the stack. This operation prevents - // the compiler from creating a temporary variable to hold - // it. - pushLiteral: function(value) { - this.pushStackLiteral(value); - }, - - // [pushProgram] - // - // On stack, before: ... - // On stack, after: program(guid), ... - // - // Push a program expression onto the stack. This takes - // a compile-time guid and converts it into a runtime-accessible - // expression. - pushProgram: function(guid) { - if (guid != null) { - this.pushStackLiteral(this.programExpression(guid)); - } else { - this.pushStackLiteral(null); - } - }, - - // [invokeHelper] - // - // On stack, before: hash, inverse, program, params..., ... - // On stack, after: result of helper invocation - // - // Pops off the helper's parameters, invokes the helper, - // and pushes the helper's return value onto the stack. - // - // If the helper is not found, `helperMissing` is called. - invokeHelper: function(paramSize, name, isSimple) { - this.aliases.helperMissing = 'helpers.helperMissing'; - - var nonHelper = this.popStack(); - var helper = this.setupHelper(paramSize, name); - - var lookup = (isSimple ? helper.name + ' || ' : '') + nonHelper + ' || helperMissing'; - this.push('((' + lookup + ').call(' + helper.callParams + '))'); - }, - - // [invokeKnownHelper] - // - // On stack, before: hash, inverse, program, params..., ... - // On stack, after: result of helper invocation - // - // This operation is used when the helper is known to exist, - // so a `helperMissing` fallback is not required. - invokeKnownHelper: function(paramSize, name) { - var helper = this.setupHelper(paramSize, name); - this.push(helper.name + ".call(" + helper.callParams + ")"); - }, - - // [invokeAmbiguous] - // - // On stack, before: hash, inverse, program, params..., ... - // On stack, after: result of disambiguation - // - // This operation is used when an expression like `{{foo}}` - // is provided, but we don't know at compile-time whether it - // is a helper or a path. - // - // This operation emits more code than the other options, - // and can be avoided by passing the `knownHelpers` and - // `knownHelpersOnly` flags at compile-time. - invokeAmbiguous: function(name, helperCall) { - this.aliases.functionType = '"function"'; - this.aliases.helperMissing = 'helpers.helperMissing'; - this.useRegister('helper'); - - var nonHelper = this.popStack(); - - this.emptyHash(); - var helper = this.setupHelper(0, name, helperCall); - - var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper'); - - this.push( - '((helper = (helper = ' + helperName + ' || ' + nonHelper + ') != null ? helper : helperMissing' - + (helper.paramsInit ? '),(' + helper.paramsInit : '') + '),' - + '(typeof helper === functionType ? helper.call(' + helper.callParams + ') : helper))'); - }, - - // [invokePartial] - // - // On stack, before: context, ... - // On stack after: result of partial invocation - // - // This operation pops off a context, invokes a partial with that context, - // and pushes the result of the invocation back. - invokePartial: function(name, indent) { - var params = [this.nameLookup('partials', name, 'partial'), "'" + indent + "'", "'" + name + "'", this.popStack(), this.popStack(), "helpers", "partials"]; - - if (this.options.data) { - params.push("data"); - } else if (this.options.compat) { - params.push('undefined'); - } - if (this.options.compat) { - params.push('depths'); - } - - this.push("this.invokePartial(" + params.join(", ") + ")"); - }, - - // [assignToHash] - // - // On stack, before: value, ..., hash, ... - // On stack, after: ..., hash, ... - // - // Pops a value off the stack and assigns it to the current hash - assignToHash: function(key) { - var value = this.popStack(), - context, - type, - id; - - if (this.trackIds) { - id = this.popStack(); - } - if (this.stringParams) { - type = this.popStack(); - context = this.popStack(); - } - - var hash = this.hash; - if (context) { - hash.contexts.push("'" + key + "': " + context); - } - if (type) { - hash.types.push("'" + key + "': " + type); - } - if (id) { - hash.ids.push("'" + key + "': " + id); - } - hash.values.push("'" + key + "': (" + value + ")"); - }, - - pushId: function(type, name) { - if (type === 'ID' || type === 'DATA') { - this.pushString(name); - } else if (type === 'sexpr') { - this.pushStackLiteral('true'); - } else { - this.pushStackLiteral('null'); - } - }, - - // HELPERS - - compiler: JavaScriptCompiler, - - compileChildren: function(environment, options) { - var children = environment.children, child, compiler; - - for(var i=0, l=children.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } - return this.topStackName(); - }, - topStackName: function() { - return "stack" + this.stackSlot; - }, - flushInline: function() { - var inlineStack = this.inlineStack; - if (inlineStack.length) { - this.inlineStack = []; - for (var i = 0, len = inlineStack.length; i < len; i++) { - var entry = inlineStack[i]; - if (entry instanceof Literal) { - this.compileStack.push(entry); - } else { - this.pushStack(entry); - } - } - } - }, - isInline: function() { - return this.inlineStack.length; - }, - - popStack: function(wrapped) { - var inline = this.isInline(), - item = (inline ? this.inlineStack : this.compileStack).pop(); - - if (!wrapped && (item instanceof Literal)) { - return item.value; - } else { - if (!inline) { - /* istanbul ignore next */ - if (!this.stackSlot) { - throw new Exception('Invalid stack pop'); - } - this.stackSlot--; - } - return item; - } - }, - - topStack: function() { - var stack = (this.isInline() ? this.inlineStack : this.compileStack), - item = stack[stack.length - 1]; - - if (item instanceof Literal) { - return item.value; - } else { - return item; - } - }, - - contextName: function(context) { - if (this.useDepths && context) { - return 'depths[' + context + ']'; - } else { - return 'depth' + context; - } - }, - - quotedString: function(str) { - return '"' + str - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 - .replace(/\u2029/g, '\\u2029') + '"'; - }, - - objectLiteral: function(obj) { - var pairs = []; - - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - pairs.push(this.quotedString(key) + ':' + obj[key]); - } - } - - return '{' + pairs.join(',') + '}'; - }, - - setupHelper: function(paramSize, name, blockHelper) { - var params = [], - paramsInit = this.setupParams(name, paramSize, params, blockHelper); - var foundHelper = this.nameLookup('helpers', name, 'helper'); - - return { - params: params, - paramsInit: paramsInit, - name: foundHelper, - callParams: [this.contextName(0)].concat(params).join(", ") - }; - }, - - setupOptions: function(helper, paramSize, params) { - var options = {}, contexts = [], types = [], ids = [], param, inverse, program; - - options.name = this.quotedString(helper); - options.hash = this.popStack(); - - if (this.trackIds) { - options.hashIds = this.popStack(); - } - if (this.stringParams) { - options.hashTypes = this.popStack(); - options.hashContexts = this.popStack(); - } - - inverse = this.popStack(); - program = this.popStack(); - - // Avoid setting fn and inverse if neither are set. This allows - // helpers to do a check for `if (options.fn)` - if (program || inverse) { - if (!program) { - program = 'this.noop'; - } - - if (!inverse) { - inverse = 'this.noop'; - } - - options.fn = program; - options.inverse = inverse; - } - - // The parameters go on to the stack in order (making sure that they are evaluated in order) - // so we need to pop them off the stack in reverse order - var i = paramSize; - while (i--) { - param = this.popStack(); - params[i] = param; - - if (this.trackIds) { - ids[i] = this.popStack(); - } - if (this.stringParams) { - types[i] = this.popStack(); - contexts[i] = this.popStack(); - } - } - - if (this.trackIds) { - options.ids = "[" + ids.join(",") + "]"; - } - if (this.stringParams) { - options.types = "[" + types.join(",") + "]"; - options.contexts = "[" + contexts.join(",") + "]"; - } - - if (this.options.data) { - options.data = "data"; - } - - return options; - }, - - // the params and contexts arguments are passed in arrays - // to fill in - setupParams: function(helperName, paramSize, params, useRegister) { - var options = this.objectLiteral(this.setupOptions(helperName, paramSize, params)); - - if (useRegister) { - this.useRegister('options'); - params.push('options'); - return 'options=' + options; - } else { - params.push(options); - return ''; - } - } - }; - - var reservedWords = ( - "break else new var" + - " case finally return void" + - " catch for switch while" + - " continue function this with" + - " default if throw" + - " delete in try" + - " do instanceof typeof" + - " abstract enum int short" + - " boolean export interface static" + - " byte extends long super" + - " char final native synchronized" + - " class float package throws" + - " const goto private transient" + - " debugger implements protected volatile" + - " double import public let yield" - ).split(" "); - - var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {}; - - for(var i=0, l=reservedWords.length; i":">",'"':""","'":"'","`":"`"},j=/[&<>"'`]/g,k=/[&<>"'`]/;b.extend=d;var l=Object.prototype.toString;b.toString=l;var m=function(a){return"function"==typeof a};m(/x/)&&(m=function(a){return"function"==typeof a&&"[object Function]"===l.call(a)});var m;b.isFunction=m;var n=Array.isArray||function(a){return a&&"object"==typeof a?"[object Array]"===l.call(a):!1};b.isArray=n,b.escapeExpression=e,b.isEmpty=f,b.appendContextPath=g}),define("handlebars/exception",["exports"],function(a){function b(a,b){var d;b&&b.firstLine&&(d=b.firstLine,a+=" - "+d+":"+b.firstColumn);for(var e=Error.prototype.constructor.call(this,a),f=0;f0?(c.ids&&(c.ids=[c.name]),a.helpers.each(b,c)):d(this);if(c.data&&c.ids){var g=q(c.data);g.contextPath=f.appendContextPath(c.data.contextPath,c.name),c={data:g}}return e(b,c)}),a.registerHelper("each",function(a,b){if(!b)throw new g("Must pass iterator to #each");var c,d,e=b.fn,h=b.inverse,i=0,j="";if(b.data&&b.ids&&(d=f.appendContextPath(b.data.contextPath,b.ids[0])+"."),l(a)&&(a=a.call(this)),b.data&&(c=q(b.data)),a&&"object"==typeof a)if(k(a))for(var m=a.length;m>i;i++)c&&(c.index=i,c.first=0===i,c.last=i===a.length-1,d&&(c.contextPath=d+i)),j+=e(a[i],{data:c});else for(var n in a)a.hasOwnProperty(n)&&(c&&(c.key=n,c.index=i,c.first=0===i,d&&(c.contextPath=d+n)),j+=e(a[n],{data:c}),i++);return 0===i&&(j=h(this)),j}),a.registerHelper("if",function(a,b){return l(a)&&(a=a.call(this)),!b.hash.includeZero&&!a||f.isEmpty(a)?b.inverse(this):b.fn(this)}),a.registerHelper("unless",function(b,c){return a.helpers["if"].call(this,b,{fn:c.inverse,inverse:c.fn,hash:c.hash})}),a.registerHelper("with",function(a,b){l(a)&&(a=a.call(this));var c=b.fn;if(f.isEmpty(a))return b.inverse(this);if(b.data&&b.ids){var d=q(b.data);d.contextPath=f.appendContextPath(b.data.contextPath,b.ids[0]),b={data:d}}return c(a,b)}),a.registerHelper("log",function(b,c){var d=c.data&&null!=c.data.level?parseInt(c.data.level,10):1;a.log(d,b)}),a.registerHelper("lookup",function(a,b){return a&&a[b]})}var f=a,g=b["default"],h="2.0.0";c.VERSION=h;var i=6;c.COMPILER_REVISION=i;var j={1:"<= 1.0.rc.2",2:"== 1.0.0-rc.3",3:"== 1.0.0-rc.4",4:"== 1.x.x",5:"== 2.0.0-alpha.x",6:">= 2.0.0-beta.1"};c.REVISION_CHANGES=j;var k=f.isArray,l=f.isFunction,m=f.toString,n="[object Object]";c.HandlebarsEnvironment=d,d.prototype={constructor:d,logger:o,log:p,registerHelper:function(a,b){if(m.call(a)===n){if(b)throw new g("Arg not supported with multiple helpers");f.extend(this.helpers,a)}else this.helpers[a]=b},unregisterHelper:function(a){delete this.helpers[a]},registerPartial:function(a,b){m.call(a)===n?f.extend(this.partials,a):this.partials[a]=b},unregisterPartial:function(a){delete this.partials[a]}};var o={methodMap:{0:"debug",1:"info",2:"warn",3:"error"},DEBUG:0,INFO:1,WARN:2,ERROR:3,level:3,log:function(a,b){if(o.level<=a){var c=o.methodMap[a];"undefined"!=typeof console&&console[c]&&console[c].call(console,b)}}};c.logger=o;var p=o.log;c.log=p;var q=function(a){var b=f.extend({},a);return b._parent=a,b};c.createFrame=q}),define("handlebars/runtime",["./utils","./exception","./base","exports"],function(a,b,c,d){function e(a){var b=a&&a[0]||1,c=m;if(b!==c){if(c>b){var d=n[c],e=n[b];throw new l("Template was precompiled with an older version of Handlebars than the current runtime. Please update your precompiler to a newer version ("+d+") or downgrade your runtime to an older version ("+e+").")}throw new l("Template was precompiled with a newer version of Handlebars than the current runtime. Please update your runtime to a newer version ("+a[1]+").")}}function f(a,b){if(!b)throw new l("No environment passed to template");if(!a||!a.main)throw new l("Unknown template object: "+typeof a);b.VM.checkRevision(a.compiler);var c=function(c,d,e,f,g,h,i,j,m){g&&(f=k.extend({},f,g));var n=b.VM.invokePartial.call(this,c,e,f,h,i,j,m);if(null==n&&b.compile){var o={helpers:h,partials:i,data:j,depths:m};i[e]=b.compile(c,{data:void 0!==j,compat:a.compat},b),n=i[e](f,o)}if(null!=n){if(d){for(var p=n.split("\n"),q=0,r=p.length;r>q&&(p[q]||q+1!==r);q++)p[q]=d+p[q];n=p.join("\n")}return n}throw new l("The partial "+e+" could not be compiled when running in runtime-only mode")},d={lookup:function(a,b){for(var c=a.length,d=0;c>d;d++)if(a[d]&&null!=a[d][b])return a[d][b]},lambda:function(a,b){return"function"==typeof a?a.call(b):a},escapeExpression:k.escapeExpression,invokePartial:c,fn:function(b){return a[b]},programs:[],program:function(a,b,c){var d=this.programs[a],e=this.fn(a);return b||c?d=g(this,a,e,b,c):d||(d=this.programs[a]=g(this,a,e)),d},data:function(a,b){for(;a&&b--;)a=a._parent;return a},merge:function(a,b){var c=a||b;return a&&b&&a!==b&&(c=k.extend({},b,a)),c},noop:b.VM.noop,compilerInfo:a.compiler},e=function(b,c){c=c||{};var f=c.data;e._setup(c),!c.partial&&a.useData&&(f=j(b,f));var g;return a.useDepths&&(g=c.depths?[b].concat(c.depths):[b]),a.main.call(d,b,d.helpers,d.partials,f,g)};return e.isTop=!0,e._setup=function(c){c.partial?(d.helpers=c.helpers,d.partials=c.partials):(d.helpers=d.merge(c.helpers,b.helpers),a.usePartial&&(d.partials=d.merge(c.partials,b.partials)))},e._child=function(b,c,e){if(a.useDepths&&!e)throw new l("must pass parent depths");return g(d,b,a[b],c,e)},e}function g(a,b,c,d,e){var f=function(b,f){return f=f||{},c.call(a,b,a.helpers,a.partials,f.data||d,e&&[b].concat(e))};return f.program=b,f.depth=e?e.length:0,f}function h(a,b,c,d,e,f,g){var h={partial:!0,helpers:d,partials:e,data:f,depths:g};if(void 0===a)throw new l("The partial "+b+" could not be found");return a instanceof Function?a(c,h):void 0}function i(){return""}function j(a,b){return b&&"root"in b||(b=b?o(b):{},b.root=a),b}var k=a,l=b["default"],m=c.COMPILER_REVISION,n=c.REVISION_CHANGES,o=c.createFrame;d.checkRevision=e,d.template=f,d.program=g,d.invokePartial=h,d.noop=i}),define("handlebars.runtime",["./handlebars/base","./handlebars/safe-string","./handlebars/exception","./handlebars/utils","./handlebars/runtime","exports"],function(a,b,c,d,e,f){var g=a,h=b["default"],i=c["default"],j=d,k=e,l=function(){var a=new g.HandlebarsEnvironment;return j.extend(a,g),a.SafeString=h,a.Exception=i,a.Utils=j,a.escapeExpression=j.escapeExpression,a.VM=k,a.template=function(b){return k.template(b,a)},a},m=l();m.create=l,m["default"]=m,f["default"]=m}),define("handlebars/compiler/ast",["../exception","exports"],function(a,b){function c(a){a=a||{},this.firstLine=a.first_line,this.firstColumn=a.first_column,this.lastColumn=a.last_column,this.lastLine=a.last_line}var d=a["default"],e={ProgramNode:function(a,b,d){c.call(this,d),this.type="program",this.statements=a,this.strip=b},MustacheNode:function(a,b,d,f,g){if(c.call(this,g),this.type="mustache",this.strip=f,null!=d&&d.charAt){var h=d.charAt(3)||d.charAt(2);this.escaped="{"!==h&&"&"!==h}else this.escaped=!!d;this.sexpr=a instanceof e.SexprNode?a:new e.SexprNode(a,b),this.id=this.sexpr.id,this.params=this.sexpr.params,this.hash=this.sexpr.hash,this.eligibleHelper=this.sexpr.eligibleHelper,this.isHelper=this.sexpr.isHelper},SexprNode:function(a,b,d){c.call(this,d),this.type="sexpr",this.hash=b;var e=this.id=a[0],f=this.params=a.slice(1);this.isHelper=!(!f.length&&!b),this.eligibleHelper=this.isHelper||e.isSimple},PartialNode:function(a,b,d,e,f){c.call(this,f),this.type="partial",this.partialName=a,this.context=b,this.hash=d,this.strip=e,this.strip.inlineStandalone=!0},BlockNode:function(a,b,d,e,f){c.call(this,f),this.type="block",this.mustache=a,this.program=b,this.inverse=d,this.strip=e,d&&!b&&(this.isInverse=!0)},RawBlockNode:function(a,b,f,g){if(c.call(this,g),a.sexpr.id.original!==f)throw new d(a.sexpr.id.original+" doesn't match "+f,this);b=new e.ContentNode(b,g),this.type="block",this.mustache=a,this.program=new e.ProgramNode([b],{},g)},ContentNode:function(a,b){c.call(this,b),this.type="content",this.original=this.string=a},HashNode:function(a,b){c.call(this,b),this.type="hash",this.pairs=a},IdNode:function(a,b){c.call(this,b),this.type="ID";for(var e="",f=[],g=0,h="",i=0,j=a.length;j>i;i++){var k=a[i].part;if(e+=(a[i].separator||"")+k,".."===k||"."===k||"this"===k){if(f.length>0)throw new d("Invalid path: "+e,this);".."===k?(g++,h+="../"):this.isScoped=!0}else f.push(k)}this.original=e,this.parts=f,this.string=f.join("."),this.depth=g,this.idName=h+this.string,this.isSimple=1===a.length&&!this.isScoped&&0===g,this.stringModeValue=this.string},PartialNameNode:function(a,b){c.call(this,b),this.type="PARTIAL_NAME",this.name=a.original},DataNode:function(a,b){c.call(this,b),this.type="DATA",this.id=a,this.stringModeValue=a.stringModeValue,this.idName="@"+a.stringModeValue},StringNode:function(a,b){c.call(this,b),this.type="STRING",this.original=this.string=this.stringModeValue=a},NumberNode:function(a,b){c.call(this,b),this.type="NUMBER",this.original=this.number=a,this.stringModeValue=Number(a)},BooleanNode:function(a,b){c.call(this,b),this.type="BOOLEAN",this.bool=a,this.stringModeValue="true"===a},CommentNode:function(a,b){c.call(this,b),this.type="comment",this.comment=a,this.strip={inlineStandalone:!0}}};b["default"]=e}),define("handlebars/compiler/parser",["exports"],function(a){var b=function(){function a(){this.yy={}}var b={trace:function(){},yy:{},symbols_:{error:2,root:3,program:4,EOF:5,program_repetition0:6,statement:7,mustache:8,block:9,rawBlock:10,partial:11,CONTENT:12,COMMENT:13,openRawBlock:14,END_RAW_BLOCK:15,OPEN_RAW_BLOCK:16,sexpr:17,CLOSE_RAW_BLOCK:18,openBlock:19,block_option0:20,closeBlock:21,openInverse:22,block_option1:23,OPEN_BLOCK:24,CLOSE:25,OPEN_INVERSE:26,inverseAndProgram:27,INVERSE:28,OPEN_ENDBLOCK:29,path:30,OPEN:31,OPEN_UNESCAPED:32,CLOSE_UNESCAPED:33,OPEN_PARTIAL:34,partialName:35,param:36,partial_option0:37,partial_option1:38,sexpr_repetition0:39,sexpr_option0:40,dataName:41,STRING:42,NUMBER:43,BOOLEAN:44,OPEN_SEXPR:45,CLOSE_SEXPR:46,hash:47,hash_repetition_plus0:48,hashSegment:49,ID:50,EQUALS:51,DATA:52,pathSegments:53,SEP:54,$accept:0,$end:1},terminals_:{2:"error",5:"EOF",12:"CONTENT",13:"COMMENT",15:"END_RAW_BLOCK",16:"OPEN_RAW_BLOCK",18:"CLOSE_RAW_BLOCK",24:"OPEN_BLOCK",25:"CLOSE",26:"OPEN_INVERSE",28:"INVERSE",29:"OPEN_ENDBLOCK",31:"OPEN",32:"OPEN_UNESCAPED",33:"CLOSE_UNESCAPED",34:"OPEN_PARTIAL",42:"STRING",43:"NUMBER",44:"BOOLEAN",45:"OPEN_SEXPR",46:"CLOSE_SEXPR",50:"ID",51:"EQUALS",52:"DATA",54:"SEP"},productions_:[0,[3,2],[4,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[10,3],[14,3],[9,4],[9,4],[19,3],[22,3],[27,2],[21,3],[8,3],[8,3],[11,5],[11,4],[17,3],[17,1],[36,1],[36,1],[36,1],[36,1],[36,1],[36,3],[47,1],[49,3],[35,1],[35,1],[35,1],[41,2],[30,1],[53,3],[53,1],[6,0],[6,2],[20,0],[20,1],[23,0],[23,1],[37,0],[37,1],[38,0],[38,1],[39,0],[39,2],[40,0],[40,1],[48,1],[48,2]],performAction:function(a,b,c,d,e,f){var g=f.length-1;switch(e){case 1:return d.prepareProgram(f[g-1].statements,!0),f[g-1];case 2:this.$=new d.ProgramNode(d.prepareProgram(f[g]),{},this._$);break;case 3:this.$=f[g];break;case 4:this.$=f[g];break;case 5:this.$=f[g];break;case 6:this.$=f[g];break;case 7:this.$=new d.ContentNode(f[g],this._$);break;case 8:this.$=new d.CommentNode(f[g],this._$);break;case 9:this.$=new d.RawBlockNode(f[g-2],f[g-1],f[g],this._$);break;case 10:this.$=new d.MustacheNode(f[g-1],null,"","",this._$);break;case 11:this.$=d.prepareBlock(f[g-3],f[g-2],f[g-1],f[g],!1,this._$);break;case 12:this.$=d.prepareBlock(f[g-3],f[g-2],f[g-1],f[g],!0,this._$);break;case 13:this.$=new d.MustacheNode(f[g-1],null,f[g-2],d.stripFlags(f[g-2],f[g]),this._$);break;case 14:this.$=new d.MustacheNode(f[g-1],null,f[g-2],d.stripFlags(f[g-2],f[g]),this._$);break;case 15:this.$={strip:d.stripFlags(f[g-1],f[g-1]),program:f[g]};break;case 16:this.$={path:f[g-1],strip:d.stripFlags(f[g-2],f[g])};break;case 17:this.$=new d.MustacheNode(f[g-1],null,f[g-2],d.stripFlags(f[g-2],f[g]),this._$);break;case 18:this.$=new d.MustacheNode(f[g-1],null,f[g-2],d.stripFlags(f[g-2],f[g]),this._$);break;case 19:this.$=new d.PartialNode(f[g-3],f[g-2],f[g-1],d.stripFlags(f[g-4],f[g]),this._$);break;case 20:this.$=new d.PartialNode(f[g-2],void 0,f[g-1],d.stripFlags(f[g-3],f[g]),this._$);break;case 21:this.$=new d.SexprNode([f[g-2]].concat(f[g-1]),f[g],this._$);break;case 22:this.$=new d.SexprNode([f[g]],null,this._$);break;case 23:this.$=f[g];break;case 24:this.$=new d.StringNode(f[g],this._$);break;case 25:this.$=new d.NumberNode(f[g],this._$);break;case 26:this.$=new d.BooleanNode(f[g],this._$);break;case 27:this.$=f[g];break;case 28:f[g-1].isHelper=!0,this.$=f[g-1];break;case 29:this.$=new d.HashNode(f[g],this._$);break;case 30:this.$=[f[g-2],f[g]];break;case 31:this.$=new d.PartialNameNode(f[g],this._$);break;case 32:this.$=new d.PartialNameNode(new d.StringNode(f[g],this._$),this._$);break;case 33:this.$=new d.PartialNameNode(new d.NumberNode(f[g],this._$));break;case 34:this.$=new d.DataNode(f[g],this._$);break;case 35:this.$=new d.IdNode(f[g],this._$);break;case 36:f[g-2].push({part:f[g],separator:f[g-1]}),this.$=f[g-2];break;case 37:this.$=[{part:f[g]}];break;case 38:this.$=[];break;case 39:f[g-1].push(f[g]);break;case 48:this.$=[];break;case 49:f[g-1].push(f[g]);break;case 52:this.$=[f[g]];break;case 53:f[g-1].push(f[g])}},table:[{3:1,4:2,5:[2,38],6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],31:[2,38],32:[2,38],34:[2,38]},{1:[3]},{5:[1,4]},{5:[2,2],7:5,8:6,9:7,10:8,11:9,12:[1,10],13:[1,11],14:16,16:[1,20],19:14,22:15,24:[1,18],26:[1,19],28:[2,2],29:[2,2],31:[1,12],32:[1,13],34:[1,17]},{1:[2,1]},{5:[2,39],12:[2,39],13:[2,39],16:[2,39],24:[2,39],26:[2,39],28:[2,39],29:[2,39],31:[2,39],32:[2,39],34:[2,39]},{5:[2,3],12:[2,3],13:[2,3],16:[2,3],24:[2,3],26:[2,3],28:[2,3],29:[2,3],31:[2,3],32:[2,3],34:[2,3]},{5:[2,4],12:[2,4],13:[2,4],16:[2,4],24:[2,4],26:[2,4],28:[2,4],29:[2,4],31:[2,4],32:[2,4],34:[2,4]},{5:[2,5],12:[2,5],13:[2,5],16:[2,5],24:[2,5],26:[2,5],28:[2,5],29:[2,5],31:[2,5],32:[2,5],34:[2,5]},{5:[2,6],12:[2,6],13:[2,6],16:[2,6],24:[2,6],26:[2,6],28:[2,6],29:[2,6],31:[2,6],32:[2,6],34:[2,6]},{5:[2,7],12:[2,7],13:[2,7],16:[2,7],24:[2,7],26:[2,7],28:[2,7],29:[2,7],31:[2,7],32:[2,7],34:[2,7]},{5:[2,8],12:[2,8],13:[2,8],16:[2,8],24:[2,8],26:[2,8],28:[2,8],29:[2,8],31:[2,8],32:[2,8],34:[2,8]},{17:21,30:22,41:23,50:[1,26],52:[1,25],53:24},{17:27,30:22,41:23,50:[1,26],52:[1,25],53:24},{4:28,6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],28:[2,38],29:[2,38],31:[2,38],32:[2,38],34:[2,38]},{4:29,6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],28:[2,38],29:[2,38],31:[2,38],32:[2,38],34:[2,38]},{12:[1,30]},{30:32,35:31,42:[1,33],43:[1,34],50:[1,26],53:24},{17:35,30:22,41:23,50:[1,26],52:[1,25],53:24},{17:36,30:22,41:23,50:[1,26],52:[1,25],53:24},{17:37,30:22,41:23,50:[1,26],52:[1,25],53:24},{25:[1,38]},{18:[2,48],25:[2,48],33:[2,48],39:39,42:[2,48],43:[2,48],44:[2,48],45:[2,48],46:[2,48],50:[2,48],52:[2,48]},{18:[2,22],25:[2,22],33:[2,22],46:[2,22]},{18:[2,35],25:[2,35],33:[2,35],42:[2,35],43:[2,35],44:[2,35],45:[2,35],46:[2,35],50:[2,35],52:[2,35],54:[1,40]},{30:41,50:[1,26],53:24},{18:[2,37],25:[2,37],33:[2,37],42:[2,37],43:[2,37],44:[2,37],45:[2,37],46:[2,37],50:[2,37],52:[2,37],54:[2,37]},{33:[1,42]},{20:43,27:44,28:[1,45],29:[2,40]},{23:46,27:47,28:[1,45],29:[2,42]},{15:[1,48]},{25:[2,46],30:51,36:49,38:50,41:55,42:[1,52],43:[1,53],44:[1,54],45:[1,56],47:57,48:58,49:60,50:[1,59],52:[1,25],53:24},{25:[2,31],42:[2,31],43:[2,31],44:[2,31],45:[2,31],50:[2,31],52:[2,31]},{25:[2,32],42:[2,32],43:[2,32],44:[2,32],45:[2,32],50:[2,32],52:[2,32]},{25:[2,33],42:[2,33],43:[2,33],44:[2,33],45:[2,33],50:[2,33],52:[2,33]},{25:[1,61]},{25:[1,62]},{18:[1,63]},{5:[2,17],12:[2,17],13:[2,17],16:[2,17],24:[2,17],26:[2,17],28:[2,17],29:[2,17],31:[2,17],32:[2,17],34:[2,17]},{18:[2,50],25:[2,50],30:51,33:[2,50],36:65,40:64,41:55,42:[1,52],43:[1,53],44:[1,54],45:[1,56],46:[2,50],47:66,48:58,49:60,50:[1,59],52:[1,25],53:24},{50:[1,67]},{18:[2,34],25:[2,34],33:[2,34],42:[2,34],43:[2,34],44:[2,34],45:[2,34],46:[2,34],50:[2,34],52:[2,34]},{5:[2,18],12:[2,18],13:[2,18],16:[2,18],24:[2,18],26:[2,18],28:[2,18],29:[2,18],31:[2,18],32:[2,18],34:[2,18]},{21:68,29:[1,69]},{29:[2,41]},{4:70,6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],29:[2,38],31:[2,38],32:[2,38],34:[2,38]},{21:71,29:[1,69]},{29:[2,43]},{5:[2,9],12:[2,9],13:[2,9],16:[2,9],24:[2,9],26:[2,9],28:[2,9],29:[2,9],31:[2,9],32:[2,9],34:[2,9]},{25:[2,44],37:72,47:73,48:58,49:60,50:[1,74]},{25:[1,75]},{18:[2,23],25:[2,23],33:[2,23],42:[2,23],43:[2,23],44:[2,23],45:[2,23],46:[2,23],50:[2,23],52:[2,23]},{18:[2,24],25:[2,24],33:[2,24],42:[2,24],43:[2,24],44:[2,24],45:[2,24],46:[2,24],50:[2,24],52:[2,24]},{18:[2,25],25:[2,25],33:[2,25],42:[2,25],43:[2,25],44:[2,25],45:[2,25],46:[2,25],50:[2,25],52:[2,25]},{18:[2,26],25:[2,26],33:[2,26],42:[2,26],43:[2,26],44:[2,26],45:[2,26],46:[2,26],50:[2,26],52:[2,26]},{18:[2,27],25:[2,27],33:[2,27],42:[2,27],43:[2,27],44:[2,27],45:[2,27],46:[2,27],50:[2,27],52:[2,27]},{17:76,30:22,41:23,50:[1,26],52:[1,25],53:24},{25:[2,47]},{18:[2,29],25:[2,29],33:[2,29],46:[2,29],49:77,50:[1,74]},{18:[2,37],25:[2,37],33:[2,37],42:[2,37],43:[2,37],44:[2,37],45:[2,37],46:[2,37],50:[2,37],51:[1,78],52:[2,37],54:[2,37]},{18:[2,52],25:[2,52],33:[2,52],46:[2,52],50:[2,52]},{12:[2,13],13:[2,13],16:[2,13],24:[2,13],26:[2,13],28:[2,13],29:[2,13],31:[2,13],32:[2,13],34:[2,13]},{12:[2,14],13:[2,14],16:[2,14],24:[2,14],26:[2,14],28:[2,14],29:[2,14],31:[2,14],32:[2,14],34:[2,14]},{12:[2,10]},{18:[2,21],25:[2,21],33:[2,21],46:[2,21]},{18:[2,49],25:[2,49],33:[2,49],42:[2,49],43:[2,49],44:[2,49],45:[2,49],46:[2,49],50:[2,49],52:[2,49]},{18:[2,51],25:[2,51],33:[2,51],46:[2,51]},{18:[2,36],25:[2,36],33:[2,36],42:[2,36],43:[2,36],44:[2,36],45:[2,36],46:[2,36],50:[2,36],52:[2,36],54:[2,36]},{5:[2,11],12:[2,11],13:[2,11],16:[2,11],24:[2,11],26:[2,11],28:[2,11],29:[2,11],31:[2,11],32:[2,11],34:[2,11]},{30:79,50:[1,26],53:24},{29:[2,15]},{5:[2,12],12:[2,12],13:[2,12],16:[2,12],24:[2,12],26:[2,12],28:[2,12],29:[2,12],31:[2,12],32:[2,12],34:[2,12]},{25:[1,80]},{25:[2,45]},{51:[1,78]},{5:[2,20],12:[2,20],13:[2,20],16:[2,20],24:[2,20],26:[2,20],28:[2,20],29:[2,20],31:[2,20],32:[2,20],34:[2,20]},{46:[1,81]},{18:[2,53],25:[2,53],33:[2,53],46:[2,53],50:[2,53]},{30:51,36:82,41:55,42:[1,52],43:[1,53],44:[1,54],45:[1,56],50:[1,26],52:[1,25],53:24},{25:[1,83]},{5:[2,19],12:[2,19],13:[2,19],16:[2,19],24:[2,19],26:[2,19],28:[2,19],29:[2,19],31:[2,19],32:[2,19],34:[2,19]},{18:[2,28],25:[2,28],33:[2,28],42:[2,28],43:[2,28],44:[2,28],45:[2,28],46:[2,28],50:[2,28],52:[2,28]},{18:[2,30],25:[2,30],33:[2,30],46:[2,30],50:[2,30]},{5:[2,16],12:[2,16],13:[2,16],16:[2,16],24:[2,16],26:[2,16],28:[2,16],29:[2,16],31:[2,16],32:[2,16],34:[2,16]}],defaultActions:{4:[2,1],44:[2,41],47:[2,43],57:[2,47],63:[2,10],70:[2,15],73:[2,45]},parseError:function(a){throw new Error(a)},parse:function(a){function b(){var a;return a=c.lexer.lex()||1,"number"!=typeof a&&(a=c.symbols_[a]||a),a}var c=this,d=[0],e=[null],f=[],g=this.table,h="",i=0,j=0,k=0;this.lexer.setInput(a),this.lexer.yy=this.yy,this.yy.lexer=this.lexer,this.yy.parser=this,"undefined"==typeof this.lexer.yylloc&&(this.lexer.yylloc={});var l=this.lexer.yylloc;f.push(l);var m=this.lexer.options&&this.lexer.options.ranges;"function"==typeof this.yy.parseError&&(this.parseError=this.yy.parseError);for(var n,o,p,q,r,s,t,u,v,w={};;){if(p=d[d.length-1],this.defaultActions[p]?q=this.defaultActions[p]:((null===n||"undefined"==typeof n)&&(n=b()),q=g[p]&&g[p][n]),"undefined"==typeof q||!q.length||!q[0]){var x="";if(!k){v=[];for(s in g[p])this.terminals_[s]&&s>2&&v.push("'"+this.terminals_[s]+"'");x=this.lexer.showPosition?"Parse error on line "+(i+1)+":\n"+this.lexer.showPosition()+"\nExpecting "+v.join(", ")+", got '"+(this.terminals_[n]||n)+"'":"Parse error on line "+(i+1)+": Unexpected "+(1==n?"end of input":"'"+(this.terminals_[n]||n)+"'"),this.parseError(x,{text:this.lexer.match,token:this.terminals_[n]||n,line:this.lexer.yylineno,loc:l,expected:v})}}if(q[0]instanceof Array&&q.length>1)throw new Error("Parse Error: multiple actions possible at state: "+p+", token: "+n);switch(q[0]){case 1:d.push(n),e.push(this.lexer.yytext),f.push(this.lexer.yylloc),d.push(q[1]),n=null,o?(n=o,o=null):(j=this.lexer.yyleng,h=this.lexer.yytext,i=this.lexer.yylineno,l=this.lexer.yylloc,k>0&&k--);break;case 2:if(t=this.productions_[q[1]][1],w.$=e[e.length-t],w._$={first_line:f[f.length-(t||1)].first_line,last_line:f[f.length-1].last_line,first_column:f[f.length-(t||1)].first_column,last_column:f[f.length-1].last_column},m&&(w._$.range=[f[f.length-(t||1)].range[0],f[f.length-1].range[1]]),r=this.performAction.call(w,h,j,i,this.yy,q[1],e,f),"undefined"!=typeof r)return r;t&&(d=d.slice(0,-1*t*2),e=e.slice(0,-1*t),f=f.slice(0,-1*t)),d.push(this.productions_[q[1]][0]),e.push(w.$),f.push(w._$),u=g[d[d.length-2]][d[d.length-1]],d.push(u);break;case 3:return!0}}return!0}},c=function(){var a={EOF:1,parseError:function(a,b){if(!this.yy.parser)throw new Error(a);this.yy.parser.parseError(a,b)},setInput:function(a){return this._input=a,this._more=this._less=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var a=this._input[0];this.yytext+=a,this.yyleng++,this.offset++,this.match+=a,this.matched+=a;var b=a.match(/(?:\r\n?|\n).*/g);return b?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),a},unput:function(a){var b=a.length,c=a.split(/(?:\r\n?|\n)/g);this._input=a+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-b-1),this.offset-=b;var d=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),c.length-1&&(this.yylineno-=c.length-1);var e=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:c?(c.length===d.length?this.yylloc.first_column:0)+d[d.length-c.length].length-c[0].length:this.yylloc.first_column-b},this.options.ranges&&(this.yylloc.range=[e[0],e[0]+this.yyleng-b]),this},more:function(){return this._more=!0,this},less:function(a){this.unput(this.match.slice(a))},pastInput:function(){var a=this.matched.substr(0,this.matched.length-this.match.length);return(a.length>20?"...":"")+a.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var a=this.match;return a.length<20&&(a+=this._input.substr(0,20-a.length)),(a.substr(0,20)+(a.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var a=this.pastInput(),b=new Array(a.length+1).join("-");return a+this.upcomingInput()+"\n"+b+"^"},next:function(){if(this.done)return this.EOF;this._input||(this.done=!0);var a,b,c,d,e;this._more||(this.yytext="",this.match="");for(var f=this._currentRules(),g=0;gb[0].length)||(b=c,d=g,this.options.flex));g++);return b?(e=b[0].match(/(?:\r\n?|\n).*/g),e&&(this.yylineno+=e.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:e?e[e.length-1].length-e[e.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+b[0].length},this.yytext+=b[0],this.match+=b[0],this.matches=b,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._input=this._input.slice(b[0].length),this.matched+=b[0],a=this.performAction.call(this,this.yy,this,f[d],this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),a?a:void 0):""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){var a=this.next();return"undefined"!=typeof a?a:this.lex()},begin:function(a){this.conditionStack.push(a)},popState:function(){return this.conditionStack.pop()},_currentRules:function(){return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules},topState:function(){return this.conditionStack[this.conditionStack.length-2]},pushState:function(a){this.begin(a)}};return a.options={},a.performAction=function(a,b,c,d){function e(a,c){return b.yytext=b.yytext.substr(a,b.yyleng-c)}switch(c){case 0:if("\\\\"===b.yytext.slice(-2)?(e(0,1),this.begin("mu")):"\\"===b.yytext.slice(-1)?(e(0,1),this.begin("emu")):this.begin("mu"),b.yytext)return 12;break;case 1:return 12;case 2:return this.popState(),12;case 3:return b.yytext=b.yytext.substr(5,b.yyleng-9),this.popState(),15;case 4:return 12;case 5:return e(0,4),this.popState(),13;case 6:return 45;case 7:return 46;case 8:return 16;case 9:return this.popState(),this.begin("raw"),18;case 10:return 34;case 11:return 24;case 12:return 29;case 13:return this.popState(),28;case 14:return this.popState(),28;case 15:return 26;case 16:return 26;case 17:return 32;case 18:return 31;case 19:this.popState(),this.begin("com");break;case 20:return e(3,5),this.popState(),13;case 21:return 31;case 22:return 51;case 23:return 50;case 24:return 50;case 25:return 54;case 26:break;case 27:return this.popState(),33;case 28:return this.popState(),25;case 29:return b.yytext=e(1,2).replace(/\\"/g,'"'),42;case 30:return b.yytext=e(1,2).replace(/\\'/g,"'"),42;case 31:return 52;case 32:return 44;case 33:return 44;case 34:return 43;case 35:return 50;case 36:return b.yytext=e(1,2),50;case 37:return"INVALID";case 38:return 5}},a.rules=[/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|\\\{\{|\\\\\{\{|$)))/,/^(?:\{\{\{\{\/[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.])\}\}\}\})/,/^(?:[^\x00]*?(?=(\{\{\{\{\/)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\()/,/^(?:\))/,/^(?:\{\{\{\{)/,/^(?:\}\}\}\})/,/^(?:\{\{(~)?>)/,/^(?:\{\{(~)?#)/,/^(?:\{\{(~)?\/)/,/^(?:\{\{(~)?\^\s*(~)?\}\})/,/^(?:\{\{(~)?\s*else\s*(~)?\}\})/,/^(?:\{\{(~)?\^)/,/^(?:\{\{(~)?\s*else\b)/,/^(?:\{\{(~)?\{)/,/^(?:\{\{(~)?&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{(~)?)/,/^(?:=)/,/^(?:\.\.)/,/^(?:\.(?=([=~}\s\/.)])))/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}(~)?\}\})/,/^(?:(~)?\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=([~}\s)])))/,/^(?:false(?=([~}\s)])))/,/^(?:-?[0-9]+(?:\.[0-9]+)?(?=([~}\s)])))/,/^(?:([^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=([=~}\s\/.)]))))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/],a.conditions={mu:{rules:[6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],inclusive:!1},emu:{rules:[2],inclusive:!1},com:{rules:[5],inclusive:!1},raw:{rules:[3,4],inclusive:!1},INITIAL:{rules:[0,1,38],inclusive:!0}},a}();return b.lexer=c,a.prototype=b,b.Parser=a,new a}();a["default"]=b}),define("handlebars/compiler/helpers",["../exception","exports"],function(a,b){function c(a,b){return{left:"~"===a.charAt(2),right:"~"===b.charAt(b.length-3)}}function d(a,b,c,d,e,k){if(a.sexpr.id.original!==d.path.original)throw new j(a.sexpr.id.original+" doesn't match "+d.path.original,a);var l=c&&c.program,m={left:a.strip.left,right:d.strip.right,openStandalone:g(b.statements),closeStandalone:f((l||b).statements)};if(a.strip.right&&h(b.statements,null,!0),l){var n=c.strip;n.left&&i(b.statements,null,!0),n.right&&h(l.statements,null,!0),d.strip.left&&i(l.statements,null,!0),f(b.statements)&&g(l.statements)&&(i(b.statements),h(l.statements))}else d.strip.left&&i(b.statements,null,!0);return e?new this.BlockNode(a,l,b,m,k):new this.BlockNode(a,b,l,m,k)}function e(a,b){for(var c=0,d=a.length;d>c;c++){var e=a[c],j=e.strip;if(j){var k=f(a,c,b,"partial"===e.type),l=g(a,c,b),m=j.openStandalone&&k,n=j.closeStandalone&&l,o=j.inlineStandalone&&k&&l;j.right&&h(a,c,!0),j.left&&i(a,c,!0),o&&(h(a,c),i(a,c)&&"partial"===e.type&&(e.indent=/([ \t]+$)/.exec(a[c-1].original)?RegExp.$1:"")),m&&(h((e.program||e.inverse).statements),i(a,c)),n&&(h(a,c),i((e.inverse||e.program).statements))}}return a}function f(a,b,c){void 0===b&&(b=a.length);var d=a[b-1],e=a[b-2];return d?"content"===d.type?(e||!c?/\r?\n\s*?$/:/(^|\r?\n)\s*?$/).test(d.original):void 0:c}function g(a,b,c){void 0===b&&(b=-1);var d=a[b+1],e=a[b+2];return d?"content"===d.type?(e||!c?/^\s*?\r?\n/:/^\s*?(\r?\n|$)/).test(d.original):void 0:c}function h(a,b,c){var d=a[null==b?0:b+1];if(d&&"content"===d.type&&(c||!d.rightStripped)){var e=d.string;d.string=d.string.replace(c?/^\s+/:/^[ \t]*\r?\n?/,""),d.rightStripped=d.string!==e}}function i(a,b,c){var d=a[null==b?a.length-1:b-1];if(d&&"content"===d.type&&(c||!d.leftStripped)){var e=d.string;return d.string=d.string.replace(c?/\s+$/:/[ \t]+$/,""),d.leftStripped=d.string!==e,d.leftStripped}}var j=a["default"];b.stripFlags=c,b.prepareBlock=d,b.prepareProgram=e}),define("handlebars/compiler/base",["./parser","./ast","./helpers","../utils","exports"],function(a,b,c,d,e){function f(a){return a.constructor===h.ProgramNode?a:(g.yy=k,g.parse(a))}var g=a["default"],h=b["default"],i=c,j=d.extend;e.parser=g;var k={};j(k,i,h),e.parse=f}),define("handlebars/compiler/compiler",["../exception","../utils","exports"],function(a,b,c){function d(){}function e(a,b,c){if(null==a||"string"!=typeof a&&a.constructor!==c.AST.ProgramNode)throw new h("You must pass a string or Handlebars AST to Handlebars.precompile. You passed "+a);b=b||{},"data"in b||(b.data=!0),b.compat&&(b.useDepths=!0);var d=c.parse(a),e=(new c.Compiler).compile(d,b);return(new c.JavaScriptCompiler).compile(e,b)}function f(a,b,c){function d(){var d=c.parse(a),e=(new c.Compiler).compile(d,b),f=(new c.JavaScriptCompiler).compile(e,b,void 0,!0);return c.template(f)}if(null==a||"string"!=typeof a&&a.constructor!==c.AST.ProgramNode)throw new h("You must pass a string or Handlebars AST to Handlebars.compile. You passed "+a);b=b||{},"data"in b||(b.data=!0),b.compat&&(b.useDepths=!0);var e,f=function(a,b){return e||(e=d()),e.call(this,a,b)};return f._setup=function(a){return e||(e=d()),e._setup(a)},f._child=function(a,b,c){return e||(e=d()),e._child(a,b,c)},f}function g(a,b){if(a===b)return!0;if(i(a)&&i(b)&&a.length===b.length){for(var c=0;cc;c++){var d=this.opcodes[c],e=a.opcodes[c];if(d.opcode!==e.opcode||!g(d.args,e.args))return!1}for(b=this.children.length,c=0;b>c;c++)if(!this.children[c].equals(a.children[c]))return!1;return!0},guid:0,compile:function(a,b){this.opcodes=[],this.children=[],this.depths={list:[]},this.options=b,this.stringParams=b.stringParams,this.trackIds=b.trackIds;var c=this.options.knownHelpers; -if(this.options.knownHelpers={helperMissing:!0,blockHelperMissing:!0,each:!0,"if":!0,unless:!0,"with":!0,log:!0,lookup:!0},c)for(var d in c)this.options.knownHelpers[d]=c[d];return this.accept(a)},accept:function(a){return this[a.type](a)},program:function(a){for(var b=a.statements,c=0,d=b.length;d>c;c++)this.accept(b[c]);return this.isSimple=1===d,this.depths.list=this.depths.list.sort(function(a,b){return a-b}),this},compileProgram:function(a){var b,c=(new this.compiler).compile(a,this.options),d=this.guid++;this.usePartial=this.usePartial||c.usePartial,this.children[d]=c;for(var e=0,f=c.depths.list.length;f>e;e++)b=c.depths.list[e],2>b||this.addDepth(b-1);return d},block:function(a){var b=a.mustache,c=a.program,d=a.inverse;c&&(c=this.compileProgram(c)),d&&(d=this.compileProgram(d));var e=b.sexpr,f=this.classifySexpr(e);"helper"===f?this.helperSexpr(e,c,d):"simple"===f?(this.simpleSexpr(e),this.opcode("pushProgram",c),this.opcode("pushProgram",d),this.opcode("emptyHash"),this.opcode("blockValue",e.id.original)):(this.ambiguousSexpr(e,c,d),this.opcode("pushProgram",c),this.opcode("pushProgram",d),this.opcode("emptyHash"),this.opcode("ambiguousBlockValue")),this.opcode("append")},hash:function(a){var b,c,d=a.pairs;for(this.opcode("pushHash"),b=0,c=d.length;c>b;b++)this.pushParam(d[b][1]);for(;b--;)this.opcode("assignToHash",d[b][0]);this.opcode("popHash")},partial:function(a){var b=a.partialName;this.usePartial=!0,a.hash?this.accept(a.hash):this.opcode("push","undefined"),a.context?this.accept(a.context):(this.opcode("getContext",0),this.opcode("pushContext")),this.opcode("invokePartial",b.name,a.indent||""),this.opcode("append")},content:function(a){a.string&&this.opcode("appendContent",a.string)},mustache:function(a){this.sexpr(a.sexpr),a.escaped&&!this.options.noEscape?this.opcode("appendEscaped"):this.opcode("append")},ambiguousSexpr:function(a,b,c){var d=a.id,e=d.parts[0],f=null!=b||null!=c;this.opcode("getContext",d.depth),this.opcode("pushProgram",b),this.opcode("pushProgram",c),this.ID(d),this.opcode("invokeAmbiguous",e,f)},simpleSexpr:function(a){var b=a.id;"DATA"===b.type?this.DATA(b):b.parts.length?this.ID(b):(this.addDepth(b.depth),this.opcode("getContext",b.depth),this.opcode("pushContext")),this.opcode("resolvePossibleLambda")},helperSexpr:function(a,b,c){var d=this.setupFullMustacheParams(a,b,c),e=a.id,f=e.parts[0];if(this.options.knownHelpers[f])this.opcode("invokeKnownHelper",d.length,f);else{if(this.options.knownHelpersOnly)throw new h("You specified knownHelpersOnly, but used the unknown helper "+f,a);e.falsy=!0,this.ID(e),this.opcode("invokeHelper",d.length,e.original,e.isSimple)}},sexpr:function(a){var b=this.classifySexpr(a);"simple"===b?this.simpleSexpr(a):"helper"===b?this.helperSexpr(a):this.ambiguousSexpr(a)},ID:function(a){this.addDepth(a.depth),this.opcode("getContext",a.depth);var b=a.parts[0];b?this.opcode("lookupOnContext",a.parts,a.falsy,a.isScoped):this.opcode("pushContext")},DATA:function(a){this.options.data=!0,this.opcode("lookupData",a.id.depth,a.id.parts)},STRING:function(a){this.opcode("pushString",a.string)},NUMBER:function(a){this.opcode("pushLiteral",a.number)},BOOLEAN:function(a){this.opcode("pushLiteral",a.bool)},comment:function(){},opcode:function(a){this.opcodes.push({opcode:a,args:j.call(arguments,1)})},addDepth:function(a){0!==a&&(this.depths[a]||(this.depths[a]=!0,this.depths.list.push(a)))},classifySexpr:function(a){var b=a.isHelper,c=a.eligibleHelper,d=this.options;if(c&&!b){var e=a.id.parts[0];d.knownHelpers[e]?b=!0:d.knownHelpersOnly&&(c=!1)}return b?"helper":c?"ambiguous":"simple"},pushParams:function(a){for(var b=0,c=a.length;c>b;b++)this.pushParam(a[b])},pushParam:function(a){this.stringParams?(a.depth&&this.addDepth(a.depth),this.opcode("getContext",a.depth||0),this.opcode("pushStringParam",a.stringModeValue,a.type),"sexpr"===a.type&&this.sexpr(a)):(this.trackIds&&this.opcode("pushId",a.type,a.idName||a.stringModeValue),this.accept(a))},setupFullMustacheParams:function(a,b,c){var d=a.params;return this.pushParams(d),this.opcode("pushProgram",b),this.opcode("pushProgram",c),a.hash?this.hash(a.hash):this.opcode("emptyHash"),d}},c.precompile=e,c.compile=f}),define("handlebars/compiler/javascript-compiler",["../base","../exception","exports"],function(a,b,c){function d(a){this.value=a}function e(){}var f=a.COMPILER_REVISION,g=a.REVISION_CHANGES,h=b["default"];e.prototype={nameLookup:function(a,b){return e.isValidJavaScriptVariableName(b)?a+"."+b:a+"['"+b+"']"},depthedLookup:function(a){return this.aliases.lookup="this.lookup",'lookup(depths, "'+a+'")'},compilerInfo:function(){var a=f,b=g[a];return[a,b]},appendToBuffer:function(a){return this.environment.isSimple?"return "+a+";":{appendToBuffer:!0,content:a,toString:function(){return"buffer += "+a+";"}}},initializeBuffer:function(){return this.quotedString("")},namespace:"Handlebars",compile:function(a,b,c,d){this.environment=a,this.options=b,this.stringParams=this.options.stringParams,this.trackIds=this.options.trackIds,this.precompile=!d,this.name=this.environment.name,this.isChild=!!c,this.context=c||{programs:[],environments:[]},this.preamble(),this.stackSlot=0,this.stackVars=[],this.aliases={},this.registers={list:[]},this.hashes=[],this.compileStack=[],this.inlineStack=[],this.compileChildren(a,b),this.useDepths=this.useDepths||a.depths.list.length||this.options.compat;var e,f,g,i=a.opcodes;for(f=0,g=i.length;g>f;f++)e=i[f],this[e.opcode].apply(this,e.args);if(this.pushSource(""),this.stackSlot||this.inlineStack.length||this.compileStack.length)throw new h("Compile completed with content left on stack");var j=this.createFunctionContext(d);if(this.isChild)return j;var k={compiler:this.compilerInfo(),main:j},l=this.context.programs;for(f=0,g=l.length;g>f;f++)l[f]&&(k[f]=l[f]);return this.environment.usePartial&&(k.usePartial=!0),this.options.data&&(k.useData=!0),this.useDepths&&(k.useDepths=!0),this.options.compat&&(k.compat=!0),d||(k.compiler=JSON.stringify(k.compiler),k=this.objectLiteral(k)),k},preamble:function(){this.lastContext=0,this.source=[]},createFunctionContext:function(a){var b="",c=this.stackVars.concat(this.registers.list);c.length>0&&(b+=", "+c.join(", "));for(var d in this.aliases)this.aliases.hasOwnProperty(d)&&(b+=", "+d+"="+this.aliases[d]);var e=["depth0","helpers","partials","data"];this.useDepths&&e.push("depths");var f=this.mergeSource(b);return a?(e.push(f),Function.apply(this,e)):"function("+e.join(",")+") {\n "+f+"}"},mergeSource:function(a){for(var b,c,d="",e=!this.forceBuffer,f=0,g=this.source.length;g>f;f++){var h=this.source[f];h.appendToBuffer?b=b?b+"\n + "+h.content:h.content:(b&&(d?d+="buffer += "+b+";\n ":(c=!0,d=b+";\n "),b=void 0),d+=h+"\n ",this.environment.isSimple||(e=!1))}return e?(b||!d)&&(d+="return "+(b||'""')+";\n"):(a+=", buffer = "+(c?"":this.initializeBuffer()),d+=b?"return buffer + "+b+";\n":"return buffer;\n"),a&&(d="var "+a.substring(2)+(c?"":";\n ")+d),d},blockValue:function(a){this.aliases.blockHelperMissing="helpers.blockHelperMissing";var b=[this.contextName(0)];this.setupParams(a,0,b);var c=this.popStack();b.splice(1,0,c),this.push("blockHelperMissing.call("+b.join(", ")+")")},ambiguousBlockValue:function(){this.aliases.blockHelperMissing="helpers.blockHelperMissing";var a=[this.contextName(0)];this.setupParams("",0,a,!0),this.flushInline();var b=this.topStack();a.splice(1,0,b),this.pushSource("if (!"+this.lastHelper+") { "+b+" = blockHelperMissing.call("+a.join(", ")+"); }")},appendContent:function(a){this.pendingContent&&(a=this.pendingContent+a),this.pendingContent=a},append:function(){this.flushInline();var a=this.popStack();this.pushSource("if ("+a+" != null) { "+this.appendToBuffer(a)+" }"),this.environment.isSimple&&this.pushSource("else { "+this.appendToBuffer("''")+" }")},appendEscaped:function(){this.aliases.escapeExpression="this.escapeExpression",this.pushSource(this.appendToBuffer("escapeExpression("+this.popStack()+")"))},getContext:function(a){this.lastContext=a},pushContext:function(){this.pushStackLiteral(this.contextName(this.lastContext))},lookupOnContext:function(a,b,c){var d=0,e=a.length;for(c||!this.options.compat||this.lastContext?this.pushContext():this.push(this.depthedLookup(a[d++]));e>d;d++)this.replaceStack(function(c){var e=this.nameLookup(c,a[d],"context");return b?" && "+e:" != null ? "+e+" : "+c})},lookupData:function(a,b){a?this.pushStackLiteral("this.data(data, "+a+")"):this.pushStackLiteral("data");for(var c=b.length,d=0;c>d;d++)this.replaceStack(function(a){return" && "+this.nameLookup(a,b[d],"data")})},resolvePossibleLambda:function(){this.aliases.lambda="this.lambda",this.push("lambda("+this.popStack()+", "+this.contextName(0)+")")},pushStringParam:function(a,b){this.pushContext(),this.pushString(b),"sexpr"!==b&&("string"==typeof a?this.pushString(a):this.pushStackLiteral(a))},emptyHash:function(){this.pushStackLiteral("{}"),this.trackIds&&this.push("{}"),this.stringParams&&(this.push("{}"),this.push("{}"))},pushHash:function(){this.hash&&this.hashes.push(this.hash),this.hash={values:[],types:[],contexts:[],ids:[]}},popHash:function(){var a=this.hash;this.hash=this.hashes.pop(),this.trackIds&&this.push("{"+a.ids.join(",")+"}"),this.stringParams&&(this.push("{"+a.contexts.join(",")+"}"),this.push("{"+a.types.join(",")+"}")),this.push("{\n "+a.values.join(",\n ")+"\n }")},pushString:function(a){this.pushStackLiteral(this.quotedString(a))},push:function(a){return this.inlineStack.push(a),a},pushLiteral:function(a){this.pushStackLiteral(a)},pushProgram:function(a){null!=a?this.pushStackLiteral(this.programExpression(a)):this.pushStackLiteral(null)},invokeHelper:function(a,b,c){this.aliases.helperMissing="helpers.helperMissing";var d=this.popStack(),e=this.setupHelper(a,b),f=(c?e.name+" || ":"")+d+" || helperMissing";this.push("(("+f+").call("+e.callParams+"))")},invokeKnownHelper:function(a,b){var c=this.setupHelper(a,b);this.push(c.name+".call("+c.callParams+")")},invokeAmbiguous:function(a,b){this.aliases.functionType='"function"',this.aliases.helperMissing="helpers.helperMissing",this.useRegister("helper");var c=this.popStack();this.emptyHash();var d=this.setupHelper(0,a,b),e=this.lastHelper=this.nameLookup("helpers",a,"helper");this.push("((helper = (helper = "+e+" || "+c+") != null ? helper : helperMissing"+(d.paramsInit?"),("+d.paramsInit:"")+"),(typeof helper === functionType ? helper.call("+d.callParams+") : helper))")},invokePartial:function(a,b){var c=[this.nameLookup("partials",a,"partial"),"'"+b+"'","'"+a+"'",this.popStack(),this.popStack(),"helpers","partials"];this.options.data?c.push("data"):this.options.compat&&c.push("undefined"),this.options.compat&&c.push("depths"),this.push("this.invokePartial("+c.join(", ")+")")},assignToHash:function(a){var b,c,d,e=this.popStack();this.trackIds&&(d=this.popStack()),this.stringParams&&(c=this.popStack(),b=this.popStack());var f=this.hash;b&&f.contexts.push("'"+a+"': "+b),c&&f.types.push("'"+a+"': "+c),d&&f.ids.push("'"+a+"': "+d),f.values.push("'"+a+"': ("+e+")")},pushId:function(a,b){"ID"===a||"DATA"===a?this.pushString(b):"sexpr"===a?this.pushStackLiteral("true"):this.pushStackLiteral("null")},compiler:e,compileChildren:function(a,b){for(var c,d,e=a.children,f=0,g=e.length;g>f;f++){c=e[f],d=new this.compiler;var h=this.matchExistingProgram(c);null==h?(this.context.programs.push(""),h=this.context.programs.length,c.index=h,c.name="program"+h,this.context.programs[h]=d.compile(c,b,this.context,!this.precompile),this.context.environments[h]=c,this.useDepths=this.useDepths||d.useDepths):(c.index=h,c.name="program"+h)}},matchExistingProgram:function(a){for(var b=0,c=this.context.environments.length;c>b;b++){var d=this.context.environments[b];if(d&&d.equals(a))return b}},programExpression:function(a){var b=this.environment.children[a],c=(b.depths.list,this.useDepths),d=[b.index,"data"];return c&&d.push("depths"),"this.program("+d.join(", ")+")"},useRegister:function(a){this.registers[a]||(this.registers[a]=!0,this.registers.list.push(a))},pushStackLiteral:function(a){return this.push(new d(a))},pushSource:function(a){this.pendingContent&&(this.source.push(this.appendToBuffer(this.quotedString(this.pendingContent))),this.pendingContent=void 0),a&&this.source.push(a)},pushStack:function(a){this.flushInline();var b=this.incrStack();return this.pushSource(b+" = "+a+";"),this.compileStack.push(b),b},replaceStack:function(a){{var b,c,e,f="";this.isInline()}if(!this.isInline())throw new h("replaceStack on non-inline");var g=this.popStack(!0);if(g instanceof d)f=b=g.value,e=!0;else{c=!this.stackSlot;var i=c?this.incrStack():this.topStackName();f="("+this.push(i)+" = "+g+")",b=this.topStack()}var j=a.call(this,b);e||this.popStack(),c&&this.stackSlot--,this.push("("+f+j+")")},incrStack:function(){return this.stackSlot++,this.stackSlot>this.stackVars.length&&this.stackVars.push("stack"+this.stackSlot),this.topStackName()},topStackName:function(){return"stack"+this.stackSlot},flushInline:function(){var a=this.inlineStack;if(a.length){this.inlineStack=[];for(var b=0,c=a.length;c>b;b++){var e=a[b];e instanceof d?this.compileStack.push(e):this.pushStack(e)}}},isInline:function(){return this.inlineStack.length},popStack:function(a){var b=this.isInline(),c=(b?this.inlineStack:this.compileStack).pop();if(!a&&c instanceof d)return c.value;if(!b){if(!this.stackSlot)throw new h("Invalid stack pop");this.stackSlot--}return c},topStack:function(){var a=this.isInline()?this.inlineStack:this.compileStack,b=a[a.length-1];return b instanceof d?b.value:b},contextName:function(a){return this.useDepths&&a?"depths["+a+"]":"depth"+a},quotedString:function(a){return'"'+a.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029")+'"'},objectLiteral:function(a){var b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(this.quotedString(c)+":"+a[c]);return"{"+b.join(",")+"}"},setupHelper:function(a,b,c){var d=[],e=this.setupParams(b,a,d,c),f=this.nameLookup("helpers",b,"helper");return{params:d,paramsInit:e,name:f,callParams:[this.contextName(0)].concat(d).join(", ")}},setupOptions:function(a,b,c){var d,e,f,g={},h=[],i=[],j=[];g.name=this.quotedString(a),g.hash=this.popStack(),this.trackIds&&(g.hashIds=this.popStack()),this.stringParams&&(g.hashTypes=this.popStack(),g.hashContexts=this.popStack()),e=this.popStack(),f=this.popStack(),(f||e)&&(f||(f="this.noop"),e||(e="this.noop"),g.fn=f,g.inverse=e);for(var k=b;k--;)d=this.popStack(),c[k]=d,this.trackIds&&(j[k]=this.popStack()),this.stringParams&&(i[k]=this.popStack(),h[k]=this.popStack());return this.trackIds&&(g.ids="["+j.join(",")+"]"),this.stringParams&&(g.types="["+i.join(",")+"]",g.contexts="["+h.join(",")+"]"),this.options.data&&(g.data="data"),g},setupParams:function(a,b,c,d){var e=this.objectLiteral(this.setupOptions(a,b,c));return d?(this.useRegister("options"),c.push("options"),"options="+e):(c.push(e),"")}};for(var i="break else new var case finally return void catch for switch while continue function this with default if throw delete in try do instanceof typeof abstract enum int short boolean export interface static byte extends long super char final native synchronized class float package throws const goto private transient debugger implements protected volatile double import public let yield".split(" "),j=e.RESERVED_WORDS={},k=0,l=i.length;l>k;k++)j[i[k]]=!0;e.isValidJavaScriptVariableName=function(a){return!e.RESERVED_WORDS[a]&&/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(a)},c["default"]=e}),define("handlebars",["./handlebars.runtime","./handlebars/compiler/ast","./handlebars/compiler/base","./handlebars/compiler/compiler","./handlebars/compiler/javascript-compiler","exports"],function(a,b,c,d,e,f){var g=a["default"],h=b["default"],i=c.parser,j=c.parse,k=d.Compiler,l=d.compile,m=d.precompile,n=e["default"],o=g.create,p=function(){var a=o();return a.compile=function(b,c){return l(b,c,a)},a.precompile=function(b,c){return m(b,c,a)},a.AST=h,a.Compiler=k,a.JavaScriptCompiler=n,a.Parser=i,a.parse=j,a};g=p(),g.create=p,g["default"]=g,f["default"]=g}); \ No newline at end of file diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.js b/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.js deleted file mode 100644 index f826bbfd38..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.js +++ /dev/null @@ -1,3079 +0,0 @@ -/*! - - handlebars v2.0.0 - -Copyright (C) 2011-2014 by Yehuda Katz - -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. - -@license -*/ -/* exported Handlebars */ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - define([], factory); - } else if (typeof exports === 'object') { - module.exports = factory(); - } else { - root.Handlebars = root.Handlebars || factory(); - } -}(this, function () { -// handlebars/safe-string.js -var __module4__ = (function() { - "use strict"; - var __exports__; - // Build out our basic SafeString type - function SafeString(string) { - this.string = string; - } - - SafeString.prototype.toString = function() { - return "" + this.string; - }; - - __exports__ = SafeString; - return __exports__; -})(); - -// handlebars/utils.js -var __module3__ = (function(__dependency1__) { - "use strict"; - var __exports__ = {}; - /*jshint -W004 */ - var SafeString = __dependency1__; - - var escape = { - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" - }; - - var badChars = /[&<>"'`]/g; - var possible = /[&<>"'`]/; - - function escapeChar(chr) { - return escape[chr]; - } - - function extend(obj /* , ...source */) { - for (var i = 1; i < arguments.length; i++) { - for (var key in arguments[i]) { - if (Object.prototype.hasOwnProperty.call(arguments[i], key)) { - obj[key] = arguments[i][key]; - } - } - } - - return obj; - } - - __exports__.extend = extend;var toString = Object.prototype.toString; - __exports__.toString = toString; - // Sourced from lodash - // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt - var isFunction = function(value) { - return typeof value === 'function'; - }; - // fallback for older versions of Chrome and Safari - /* istanbul ignore next */ - if (isFunction(/x/)) { - isFunction = function(value) { - return typeof value === 'function' && toString.call(value) === '[object Function]'; - }; - } - var isFunction; - __exports__.isFunction = isFunction; - /* istanbul ignore next */ - var isArray = Array.isArray || function(value) { - return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; - }; - __exports__.isArray = isArray; - - function escapeExpression(string) { - // don't escape SafeStrings, since they're already safe - if (string instanceof SafeString) { - return string.toString(); - } else if (string == null) { - return ""; - } else if (!string) { - return string + ''; - } - - // Force a string conversion as this will be done by the append regardless and - // the regex test will do this transparently behind the scenes, causing issues if - // an object's to string has escaped characters in it. - string = "" + string; - - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); - } - - __exports__.escapeExpression = escapeExpression;function isEmpty(value) { - if (!value && value !== 0) { - return true; - } else if (isArray(value) && value.length === 0) { - return true; - } else { - return false; - } - } - - __exports__.isEmpty = isEmpty;function appendContextPath(contextPath, id) { - return (contextPath ? contextPath + '.' : '') + id; - } - - __exports__.appendContextPath = appendContextPath; - return __exports__; -})(__module4__); - -// handlebars/exception.js -var __module5__ = (function() { - "use strict"; - var __exports__; - - var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; - - function Exception(message, node) { - var line; - if (node && node.firstLine) { - line = node.firstLine; - - message += ' - ' + line + ':' + node.firstColumn; - } - - var tmp = Error.prototype.constructor.call(this, message); - - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (var idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } - - if (line) { - this.lineNumber = line; - this.column = node.firstColumn; - } - } - - Exception.prototype = new Error(); - - __exports__ = Exception; - return __exports__; -})(); - -// handlebars/base.js -var __module2__ = (function(__dependency1__, __dependency2__) { - "use strict"; - var __exports__ = {}; - var Utils = __dependency1__; - var Exception = __dependency2__; - - var VERSION = "2.0.0"; - __exports__.VERSION = VERSION;var COMPILER_REVISION = 6; - __exports__.COMPILER_REVISION = COMPILER_REVISION; - var REVISION_CHANGES = { - 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it - 2: '== 1.0.0-rc.3', - 3: '== 1.0.0-rc.4', - 4: '== 1.x.x', - 5: '== 2.0.0-alpha.x', - 6: '>= 2.0.0-beta.1' - }; - __exports__.REVISION_CHANGES = REVISION_CHANGES; - var isArray = Utils.isArray, - isFunction = Utils.isFunction, - toString = Utils.toString, - objectType = '[object Object]'; - - function HandlebarsEnvironment(helpers, partials) { - this.helpers = helpers || {}; - this.partials = partials || {}; - - registerDefaultHelpers(this); - } - - __exports__.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = { - constructor: HandlebarsEnvironment, - - logger: logger, - log: log, - - registerHelper: function(name, fn) { - if (toString.call(name) === objectType) { - if (fn) { throw new Exception('Arg not supported with multiple helpers'); } - Utils.extend(this.helpers, name); - } else { - this.helpers[name] = fn; - } - }, - unregisterHelper: function(name) { - delete this.helpers[name]; - }, - - registerPartial: function(name, partial) { - if (toString.call(name) === objectType) { - Utils.extend(this.partials, name); - } else { - this.partials[name] = partial; - } - }, - unregisterPartial: function(name) { - delete this.partials[name]; - } - }; - - function registerDefaultHelpers(instance) { - instance.registerHelper('helperMissing', function(/* [args, ]options */) { - if(arguments.length === 1) { - // A missing field in a {{foo}} constuct. - return undefined; - } else { - // Someone is actually trying to call something, blow up. - throw new Exception("Missing helper: '" + arguments[arguments.length-1].name + "'"); - } - }); - - instance.registerHelper('blockHelperMissing', function(context, options) { - var inverse = options.inverse, - fn = options.fn; - - if(context === true) { - return fn(this); - } else if(context === false || context == null) { - return inverse(this); - } else if (isArray(context)) { - if(context.length > 0) { - if (options.ids) { - options.ids = [options.name]; - } - - return instance.helpers.each(context, options); - } else { - return inverse(this); - } - } else { - if (options.data && options.ids) { - var data = createFrame(options.data); - data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name); - options = {data: data}; - } - - return fn(context, options); - } - }); - - instance.registerHelper('each', function(context, options) { - if (!options) { - throw new Exception('Must pass iterator to #each'); - } - - var fn = options.fn, inverse = options.inverse; - var i = 0, ret = "", data; - - var contextPath; - if (options.data && options.ids) { - contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.'; - } - - if (isFunction(context)) { context = context.call(this); } - - if (options.data) { - data = createFrame(options.data); - } - - if(context && typeof context === 'object') { - if (isArray(context)) { - for(var j = context.length; i 0) { - throw new Exception("Invalid path: " + original, this); - } else if (part === "..") { - depth++; - depthString += '../'; - } else { - this.isScoped = true; - } - } else { - dig.push(part); - } - } - - this.original = original; - this.parts = dig; - this.string = dig.join('.'); - this.depth = depth; - this.idName = depthString + this.string; - - // an ID is simple if it only has one part, and that part is not - // `..` or `this`. - this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; - - this.stringModeValue = this.string; - }, - - PartialNameNode: function(name, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "PARTIAL_NAME"; - this.name = name.original; - }, - - DataNode: function(id, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "DATA"; - this.id = id; - this.stringModeValue = id.stringModeValue; - this.idName = '@' + id.stringModeValue; - }, - - StringNode: function(string, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "STRING"; - this.original = - this.string = - this.stringModeValue = string; - }, - - NumberNode: function(number, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "NUMBER"; - this.original = - this.number = number; - this.stringModeValue = Number(number); - }, - - BooleanNode: function(bool, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "BOOLEAN"; - this.bool = bool; - this.stringModeValue = bool === "true"; - }, - - CommentNode: function(comment, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "comment"; - this.comment = comment; - - this.strip = { - inlineStandalone: true - }; - } - }; - - - // Must be exported as an object rather than the root of the module as the jison lexer - // most modify the object to operate properly. - __exports__ = AST; - return __exports__; -})(__module5__); - -// handlebars/compiler/parser.js -var __module9__ = (function() { - "use strict"; - var __exports__; - /* jshint ignore:start */ - /* istanbul ignore next */ - /* Jison generated parser */ - var handlebars = (function(){ - var parser = {trace: function trace() { }, - yy: {}, - symbols_: {"error":2,"root":3,"program":4,"EOF":5,"program_repetition0":6,"statement":7,"mustache":8,"block":9,"rawBlock":10,"partial":11,"CONTENT":12,"COMMENT":13,"openRawBlock":14,"END_RAW_BLOCK":15,"OPEN_RAW_BLOCK":16,"sexpr":17,"CLOSE_RAW_BLOCK":18,"openBlock":19,"block_option0":20,"closeBlock":21,"openInverse":22,"block_option1":23,"OPEN_BLOCK":24,"CLOSE":25,"OPEN_INVERSE":26,"inverseAndProgram":27,"INVERSE":28,"OPEN_ENDBLOCK":29,"path":30,"OPEN":31,"OPEN_UNESCAPED":32,"CLOSE_UNESCAPED":33,"OPEN_PARTIAL":34,"partialName":35,"param":36,"partial_option0":37,"partial_option1":38,"sexpr_repetition0":39,"sexpr_option0":40,"dataName":41,"STRING":42,"NUMBER":43,"BOOLEAN":44,"OPEN_SEXPR":45,"CLOSE_SEXPR":46,"hash":47,"hash_repetition_plus0":48,"hashSegment":49,"ID":50,"EQUALS":51,"DATA":52,"pathSegments":53,"SEP":54,"$accept":0,"$end":1}, - terminals_: {2:"error",5:"EOF",12:"CONTENT",13:"COMMENT",15:"END_RAW_BLOCK",16:"OPEN_RAW_BLOCK",18:"CLOSE_RAW_BLOCK",24:"OPEN_BLOCK",25:"CLOSE",26:"OPEN_INVERSE",28:"INVERSE",29:"OPEN_ENDBLOCK",31:"OPEN",32:"OPEN_UNESCAPED",33:"CLOSE_UNESCAPED",34:"OPEN_PARTIAL",42:"STRING",43:"NUMBER",44:"BOOLEAN",45:"OPEN_SEXPR",46:"CLOSE_SEXPR",50:"ID",51:"EQUALS",52:"DATA",54:"SEP"}, - productions_: [0,[3,2],[4,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[10,3],[14,3],[9,4],[9,4],[19,3],[22,3],[27,2],[21,3],[8,3],[8,3],[11,5],[11,4],[17,3],[17,1],[36,1],[36,1],[36,1],[36,1],[36,1],[36,3],[47,1],[49,3],[35,1],[35,1],[35,1],[41,2],[30,1],[53,3],[53,1],[6,0],[6,2],[20,0],[20,1],[23,0],[23,1],[37,0],[37,1],[38,0],[38,1],[39,0],[39,2],[40,0],[40,1],[48,1],[48,2]], - performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) { - - var $0 = $$.length - 1; - switch (yystate) { - case 1: yy.prepareProgram($$[$0-1].statements, true); return $$[$0-1]; - break; - case 2:this.$ = new yy.ProgramNode(yy.prepareProgram($$[$0]), {}, this._$); - break; - case 3:this.$ = $$[$0]; - break; - case 4:this.$ = $$[$0]; - break; - case 5:this.$ = $$[$0]; - break; - case 6:this.$ = $$[$0]; - break; - case 7:this.$ = new yy.ContentNode($$[$0], this._$); - break; - case 8:this.$ = new yy.CommentNode($$[$0], this._$); - break; - case 9:this.$ = new yy.RawBlockNode($$[$0-2], $$[$0-1], $$[$0], this._$); - break; - case 10:this.$ = new yy.MustacheNode($$[$0-1], null, '', '', this._$); - break; - case 11:this.$ = yy.prepareBlock($$[$0-3], $$[$0-2], $$[$0-1], $$[$0], false, this._$); - break; - case 12:this.$ = yy.prepareBlock($$[$0-3], $$[$0-2], $$[$0-1], $$[$0], true, this._$); - break; - case 13:this.$ = new yy.MustacheNode($$[$0-1], null, $$[$0-2], yy.stripFlags($$[$0-2], $$[$0]), this._$); - break; - case 14:this.$ = new yy.MustacheNode($$[$0-1], null, $$[$0-2], yy.stripFlags($$[$0-2], $$[$0]), this._$); - break; - case 15:this.$ = { strip: yy.stripFlags($$[$0-1], $$[$0-1]), program: $$[$0] }; - break; - case 16:this.$ = {path: $$[$0-1], strip: yy.stripFlags($$[$0-2], $$[$0])}; - break; - case 17:this.$ = new yy.MustacheNode($$[$0-1], null, $$[$0-2], yy.stripFlags($$[$0-2], $$[$0]), this._$); - break; - case 18:this.$ = new yy.MustacheNode($$[$0-1], null, $$[$0-2], yy.stripFlags($$[$0-2], $$[$0]), this._$); - break; - case 19:this.$ = new yy.PartialNode($$[$0-3], $$[$0-2], $$[$0-1], yy.stripFlags($$[$0-4], $$[$0]), this._$); - break; - case 20:this.$ = new yy.PartialNode($$[$0-2], undefined, $$[$0-1], yy.stripFlags($$[$0-3], $$[$0]), this._$); - break; - case 21:this.$ = new yy.SexprNode([$$[$0-2]].concat($$[$0-1]), $$[$0], this._$); - break; - case 22:this.$ = new yy.SexprNode([$$[$0]], null, this._$); - break; - case 23:this.$ = $$[$0]; - break; - case 24:this.$ = new yy.StringNode($$[$0], this._$); - break; - case 25:this.$ = new yy.NumberNode($$[$0], this._$); - break; - case 26:this.$ = new yy.BooleanNode($$[$0], this._$); - break; - case 27:this.$ = $$[$0]; - break; - case 28:$$[$0-1].isHelper = true; this.$ = $$[$0-1]; - break; - case 29:this.$ = new yy.HashNode($$[$0], this._$); - break; - case 30:this.$ = [$$[$0-2], $$[$0]]; - break; - case 31:this.$ = new yy.PartialNameNode($$[$0], this._$); - break; - case 32:this.$ = new yy.PartialNameNode(new yy.StringNode($$[$0], this._$), this._$); - break; - case 33:this.$ = new yy.PartialNameNode(new yy.NumberNode($$[$0], this._$)); - break; - case 34:this.$ = new yy.DataNode($$[$0], this._$); - break; - case 35:this.$ = new yy.IdNode($$[$0], this._$); - break; - case 36: $$[$0-2].push({part: $$[$0], separator: $$[$0-1]}); this.$ = $$[$0-2]; - break; - case 37:this.$ = [{part: $$[$0]}]; - break; - case 38:this.$ = []; - break; - case 39:$$[$0-1].push($$[$0]); - break; - case 48:this.$ = []; - break; - case 49:$$[$0-1].push($$[$0]); - break; - case 52:this.$ = [$$[$0]]; - break; - case 53:$$[$0-1].push($$[$0]); - break; - } - }, - table: [{3:1,4:2,5:[2,38],6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],31:[2,38],32:[2,38],34:[2,38]},{1:[3]},{5:[1,4]},{5:[2,2],7:5,8:6,9:7,10:8,11:9,12:[1,10],13:[1,11],14:16,16:[1,20],19:14,22:15,24:[1,18],26:[1,19],28:[2,2],29:[2,2],31:[1,12],32:[1,13],34:[1,17]},{1:[2,1]},{5:[2,39],12:[2,39],13:[2,39],16:[2,39],24:[2,39],26:[2,39],28:[2,39],29:[2,39],31:[2,39],32:[2,39],34:[2,39]},{5:[2,3],12:[2,3],13:[2,3],16:[2,3],24:[2,3],26:[2,3],28:[2,3],29:[2,3],31:[2,3],32:[2,3],34:[2,3]},{5:[2,4],12:[2,4],13:[2,4],16:[2,4],24:[2,4],26:[2,4],28:[2,4],29:[2,4],31:[2,4],32:[2,4],34:[2,4]},{5:[2,5],12:[2,5],13:[2,5],16:[2,5],24:[2,5],26:[2,5],28:[2,5],29:[2,5],31:[2,5],32:[2,5],34:[2,5]},{5:[2,6],12:[2,6],13:[2,6],16:[2,6],24:[2,6],26:[2,6],28:[2,6],29:[2,6],31:[2,6],32:[2,6],34:[2,6]},{5:[2,7],12:[2,7],13:[2,7],16:[2,7],24:[2,7],26:[2,7],28:[2,7],29:[2,7],31:[2,7],32:[2,7],34:[2,7]},{5:[2,8],12:[2,8],13:[2,8],16:[2,8],24:[2,8],26:[2,8],28:[2,8],29:[2,8],31:[2,8],32:[2,8],34:[2,8]},{17:21,30:22,41:23,50:[1,26],52:[1,25],53:24},{17:27,30:22,41:23,50:[1,26],52:[1,25],53:24},{4:28,6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],28:[2,38],29:[2,38],31:[2,38],32:[2,38],34:[2,38]},{4:29,6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],28:[2,38],29:[2,38],31:[2,38],32:[2,38],34:[2,38]},{12:[1,30]},{30:32,35:31,42:[1,33],43:[1,34],50:[1,26],53:24},{17:35,30:22,41:23,50:[1,26],52:[1,25],53:24},{17:36,30:22,41:23,50:[1,26],52:[1,25],53:24},{17:37,30:22,41:23,50:[1,26],52:[1,25],53:24},{25:[1,38]},{18:[2,48],25:[2,48],33:[2,48],39:39,42:[2,48],43:[2,48],44:[2,48],45:[2,48],46:[2,48],50:[2,48],52:[2,48]},{18:[2,22],25:[2,22],33:[2,22],46:[2,22]},{18:[2,35],25:[2,35],33:[2,35],42:[2,35],43:[2,35],44:[2,35],45:[2,35],46:[2,35],50:[2,35],52:[2,35],54:[1,40]},{30:41,50:[1,26],53:24},{18:[2,37],25:[2,37],33:[2,37],42:[2,37],43:[2,37],44:[2,37],45:[2,37],46:[2,37],50:[2,37],52:[2,37],54:[2,37]},{33:[1,42]},{20:43,27:44,28:[1,45],29:[2,40]},{23:46,27:47,28:[1,45],29:[2,42]},{15:[1,48]},{25:[2,46],30:51,36:49,38:50,41:55,42:[1,52],43:[1,53],44:[1,54],45:[1,56],47:57,48:58,49:60,50:[1,59],52:[1,25],53:24},{25:[2,31],42:[2,31],43:[2,31],44:[2,31],45:[2,31],50:[2,31],52:[2,31]},{25:[2,32],42:[2,32],43:[2,32],44:[2,32],45:[2,32],50:[2,32],52:[2,32]},{25:[2,33],42:[2,33],43:[2,33],44:[2,33],45:[2,33],50:[2,33],52:[2,33]},{25:[1,61]},{25:[1,62]},{18:[1,63]},{5:[2,17],12:[2,17],13:[2,17],16:[2,17],24:[2,17],26:[2,17],28:[2,17],29:[2,17],31:[2,17],32:[2,17],34:[2,17]},{18:[2,50],25:[2,50],30:51,33:[2,50],36:65,40:64,41:55,42:[1,52],43:[1,53],44:[1,54],45:[1,56],46:[2,50],47:66,48:58,49:60,50:[1,59],52:[1,25],53:24},{50:[1,67]},{18:[2,34],25:[2,34],33:[2,34],42:[2,34],43:[2,34],44:[2,34],45:[2,34],46:[2,34],50:[2,34],52:[2,34]},{5:[2,18],12:[2,18],13:[2,18],16:[2,18],24:[2,18],26:[2,18],28:[2,18],29:[2,18],31:[2,18],32:[2,18],34:[2,18]},{21:68,29:[1,69]},{29:[2,41]},{4:70,6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],29:[2,38],31:[2,38],32:[2,38],34:[2,38]},{21:71,29:[1,69]},{29:[2,43]},{5:[2,9],12:[2,9],13:[2,9],16:[2,9],24:[2,9],26:[2,9],28:[2,9],29:[2,9],31:[2,9],32:[2,9],34:[2,9]},{25:[2,44],37:72,47:73,48:58,49:60,50:[1,74]},{25:[1,75]},{18:[2,23],25:[2,23],33:[2,23],42:[2,23],43:[2,23],44:[2,23],45:[2,23],46:[2,23],50:[2,23],52:[2,23]},{18:[2,24],25:[2,24],33:[2,24],42:[2,24],43:[2,24],44:[2,24],45:[2,24],46:[2,24],50:[2,24],52:[2,24]},{18:[2,25],25:[2,25],33:[2,25],42:[2,25],43:[2,25],44:[2,25],45:[2,25],46:[2,25],50:[2,25],52:[2,25]},{18:[2,26],25:[2,26],33:[2,26],42:[2,26],43:[2,26],44:[2,26],45:[2,26],46:[2,26],50:[2,26],52:[2,26]},{18:[2,27],25:[2,27],33:[2,27],42:[2,27],43:[2,27],44:[2,27],45:[2,27],46:[2,27],50:[2,27],52:[2,27]},{17:76,30:22,41:23,50:[1,26],52:[1,25],53:24},{25:[2,47]},{18:[2,29],25:[2,29],33:[2,29],46:[2,29],49:77,50:[1,74]},{18:[2,37],25:[2,37],33:[2,37],42:[2,37],43:[2,37],44:[2,37],45:[2,37],46:[2,37],50:[2,37],51:[1,78],52:[2,37],54:[2,37]},{18:[2,52],25:[2,52],33:[2,52],46:[2,52],50:[2,52]},{12:[2,13],13:[2,13],16:[2,13],24:[2,13],26:[2,13],28:[2,13],29:[2,13],31:[2,13],32:[2,13],34:[2,13]},{12:[2,14],13:[2,14],16:[2,14],24:[2,14],26:[2,14],28:[2,14],29:[2,14],31:[2,14],32:[2,14],34:[2,14]},{12:[2,10]},{18:[2,21],25:[2,21],33:[2,21],46:[2,21]},{18:[2,49],25:[2,49],33:[2,49],42:[2,49],43:[2,49],44:[2,49],45:[2,49],46:[2,49],50:[2,49],52:[2,49]},{18:[2,51],25:[2,51],33:[2,51],46:[2,51]},{18:[2,36],25:[2,36],33:[2,36],42:[2,36],43:[2,36],44:[2,36],45:[2,36],46:[2,36],50:[2,36],52:[2,36],54:[2,36]},{5:[2,11],12:[2,11],13:[2,11],16:[2,11],24:[2,11],26:[2,11],28:[2,11],29:[2,11],31:[2,11],32:[2,11],34:[2,11]},{30:79,50:[1,26],53:24},{29:[2,15]},{5:[2,12],12:[2,12],13:[2,12],16:[2,12],24:[2,12],26:[2,12],28:[2,12],29:[2,12],31:[2,12],32:[2,12],34:[2,12]},{25:[1,80]},{25:[2,45]},{51:[1,78]},{5:[2,20],12:[2,20],13:[2,20],16:[2,20],24:[2,20],26:[2,20],28:[2,20],29:[2,20],31:[2,20],32:[2,20],34:[2,20]},{46:[1,81]},{18:[2,53],25:[2,53],33:[2,53],46:[2,53],50:[2,53]},{30:51,36:82,41:55,42:[1,52],43:[1,53],44:[1,54],45:[1,56],50:[1,26],52:[1,25],53:24},{25:[1,83]},{5:[2,19],12:[2,19],13:[2,19],16:[2,19],24:[2,19],26:[2,19],28:[2,19],29:[2,19],31:[2,19],32:[2,19],34:[2,19]},{18:[2,28],25:[2,28],33:[2,28],42:[2,28],43:[2,28],44:[2,28],45:[2,28],46:[2,28],50:[2,28],52:[2,28]},{18:[2,30],25:[2,30],33:[2,30],46:[2,30],50:[2,30]},{5:[2,16],12:[2,16],13:[2,16],16:[2,16],24:[2,16],26:[2,16],28:[2,16],29:[2,16],31:[2,16],32:[2,16],34:[2,16]}], - defaultActions: {4:[2,1],44:[2,41],47:[2,43],57:[2,47],63:[2,10],70:[2,15],73:[2,45]}, - parseError: function parseError(str, hash) { - throw new Error(str); - }, - parse: function parse(input) { - var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = "", yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; - this.lexer.setInput(input); - this.lexer.yy = this.yy; - this.yy.lexer = this.lexer; - this.yy.parser = this; - if (typeof this.lexer.yylloc == "undefined") - this.lexer.yylloc = {}; - var yyloc = this.lexer.yylloc; - lstack.push(yyloc); - var ranges = this.lexer.options && this.lexer.options.ranges; - if (typeof this.yy.parseError === "function") - this.parseError = this.yy.parseError; - function popStack(n) { - stack.length = stack.length - 2 * n; - vstack.length = vstack.length - n; - lstack.length = lstack.length - n; - } - function lex() { - var token; - token = self.lexer.lex() || 1; - if (typeof token !== "number") { - token = self.symbols_[token] || token; - } - return token; - } - var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; - while (true) { - state = stack[stack.length - 1]; - if (this.defaultActions[state]) { - action = this.defaultActions[state]; - } else { - if (symbol === null || typeof symbol == "undefined") { - symbol = lex(); - } - action = table[state] && table[state][symbol]; - } - if (typeof action === "undefined" || !action.length || !action[0]) { - var errStr = ""; - if (!recovering) { - expected = []; - for (p in table[state]) - if (this.terminals_[p] && p > 2) { - expected.push("'" + this.terminals_[p] + "'"); - } - if (this.lexer.showPosition) { - errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'"; - } else { - errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); - } - this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); - } - } - if (action[0] instanceof Array && action.length > 1) { - throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); - } - switch (action[0]) { - case 1: - stack.push(symbol); - vstack.push(this.lexer.yytext); - lstack.push(this.lexer.yylloc); - stack.push(action[1]); - symbol = null; - if (!preErrorSymbol) { - yyleng = this.lexer.yyleng; - yytext = this.lexer.yytext; - yylineno = this.lexer.yylineno; - yyloc = this.lexer.yylloc; - if (recovering > 0) - recovering--; - } else { - symbol = preErrorSymbol; - preErrorSymbol = null; - } - break; - case 2: - len = this.productions_[action[1]][1]; - yyval.$ = vstack[vstack.length - len]; - yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; - if (ranges) { - yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; - } - r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); - if (typeof r !== "undefined") { - return r; - } - if (len) { - stack = stack.slice(0, -1 * len * 2); - vstack = vstack.slice(0, -1 * len); - lstack = lstack.slice(0, -1 * len); - } - stack.push(this.productions_[action[1]][0]); - vstack.push(yyval.$); - lstack.push(yyval._$); - newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; - stack.push(newState); - break; - case 3: - return true; - } - } - return true; - } - }; - /* Jison generated lexer */ - var lexer = (function(){ - var lexer = ({EOF:1, - parseError:function parseError(str, hash) { - if (this.yy.parser) { - this.yy.parser.parseError(str, hash); - } else { - throw new Error(str); - } - }, - setInput:function (input) { - this._input = input; - this._more = this._less = this.done = false; - this.yylineno = this.yyleng = 0; - this.yytext = this.matched = this.match = ''; - this.conditionStack = ['INITIAL']; - this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; - if (this.options.ranges) this.yylloc.range = [0,0]; - this.offset = 0; - return this; - }, - input:function () { - var ch = this._input[0]; - this.yytext += ch; - this.yyleng++; - this.offset++; - this.match += ch; - this.matched += ch; - var lines = ch.match(/(?:\r\n?|\n).*/g); - if (lines) { - this.yylineno++; - this.yylloc.last_line++; - } else { - this.yylloc.last_column++; - } - if (this.options.ranges) this.yylloc.range[1]++; - - this._input = this._input.slice(1); - return ch; - }, - unput:function (ch) { - var len = ch.length; - var lines = ch.split(/(?:\r\n?|\n)/g); - - this._input = ch + this._input; - this.yytext = this.yytext.substr(0, this.yytext.length-len-1); - //this.yyleng -= len; - this.offset -= len; - var oldLines = this.match.split(/(?:\r\n?|\n)/g); - this.match = this.match.substr(0, this.match.length-1); - this.matched = this.matched.substr(0, this.matched.length-1); - - if (lines.length-1) this.yylineno -= lines.length-1; - var r = this.yylloc.range; - - this.yylloc = {first_line: this.yylloc.first_line, - last_line: this.yylineno+1, - first_column: this.yylloc.first_column, - last_column: lines ? - (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length: - this.yylloc.first_column - len - }; - - if (this.options.ranges) { - this.yylloc.range = [r[0], r[0] + this.yyleng - len]; - } - return this; - }, - more:function () { - this._more = true; - return this; - }, - less:function (n) { - this.unput(this.match.slice(n)); - }, - pastInput:function () { - var past = this.matched.substr(0, this.matched.length - this.match.length); - return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); - }, - upcomingInput:function () { - var next = this.match; - if (next.length < 20) { - next += this._input.substr(0, 20-next.length); - } - return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); - }, - showPosition:function () { - var pre = this.pastInput(); - var c = new Array(pre.length + 1).join("-"); - return pre + this.upcomingInput() + "\n" + c+"^"; - }, - next:function () { - if (this.done) { - return this.EOF; - } - if (!this._input) this.done = true; - - var token, - match, - tempMatch, - index, - col, - lines; - if (!this._more) { - this.yytext = ''; - this.match = ''; - } - var rules = this._currentRules(); - for (var i=0;i < rules.length; i++) { - tempMatch = this._input.match(this.rules[rules[i]]); - if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { - match = tempMatch; - index = i; - if (!this.options.flex) break; - } - } - if (match) { - lines = match[0].match(/(?:\r\n?|\n).*/g); - if (lines) this.yylineno += lines.length; - this.yylloc = {first_line: this.yylloc.last_line, - last_line: this.yylineno+1, - first_column: this.yylloc.last_column, - last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length}; - this.yytext += match[0]; - this.match += match[0]; - this.matches = match; - this.yyleng = this.yytext.length; - if (this.options.ranges) { - this.yylloc.range = [this.offset, this.offset += this.yyleng]; - } - this._more = false; - this._input = this._input.slice(match[0].length); - this.matched += match[0]; - token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]); - if (this.done && this._input) this.done = false; - if (token) return token; - else return; - } - if (this._input === "") { - return this.EOF; - } else { - return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), - {text: "", token: null, line: this.yylineno}); - } - }, - lex:function lex() { - var r = this.next(); - if (typeof r !== 'undefined') { - return r; - } else { - return this.lex(); - } - }, - begin:function begin(condition) { - this.conditionStack.push(condition); - }, - popState:function popState() { - return this.conditionStack.pop(); - }, - _currentRules:function _currentRules() { - return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; - }, - topState:function () { - return this.conditionStack[this.conditionStack.length-2]; - }, - pushState:function begin(condition) { - this.begin(condition); - }}); - lexer.options = {}; - lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { - - - function strip(start, end) { - return yy_.yytext = yy_.yytext.substr(start, yy_.yyleng-end); - } - - - var YYSTATE=YY_START - switch($avoiding_name_collisions) { - case 0: - if(yy_.yytext.slice(-2) === "\\\\") { - strip(0,1); - this.begin("mu"); - } else if(yy_.yytext.slice(-1) === "\\") { - strip(0,1); - this.begin("emu"); - } else { - this.begin("mu"); - } - if(yy_.yytext) return 12; - - break; - case 1:return 12; - break; - case 2: - this.popState(); - return 12; - - break; - case 3: - yy_.yytext = yy_.yytext.substr(5, yy_.yyleng-9); - this.popState(); - return 15; - - break; - case 4: return 12; - break; - case 5:strip(0,4); this.popState(); return 13; - break; - case 6:return 45; - break; - case 7:return 46; - break; - case 8: return 16; - break; - case 9: - this.popState(); - this.begin('raw'); - return 18; - - break; - case 10:return 34; - break; - case 11:return 24; - break; - case 12:return 29; - break; - case 13:this.popState(); return 28; - break; - case 14:this.popState(); return 28; - break; - case 15:return 26; - break; - case 16:return 26; - break; - case 17:return 32; - break; - case 18:return 31; - break; - case 19:this.popState(); this.begin('com'); - break; - case 20:strip(3,5); this.popState(); return 13; - break; - case 21:return 31; - break; - case 22:return 51; - break; - case 23:return 50; - break; - case 24:return 50; - break; - case 25:return 54; - break; - case 26:// ignore whitespace - break; - case 27:this.popState(); return 33; - break; - case 28:this.popState(); return 25; - break; - case 29:yy_.yytext = strip(1,2).replace(/\\"/g,'"'); return 42; - break; - case 30:yy_.yytext = strip(1,2).replace(/\\'/g,"'"); return 42; - break; - case 31:return 52; - break; - case 32:return 44; - break; - case 33:return 44; - break; - case 34:return 43; - break; - case 35:return 50; - break; - case 36:yy_.yytext = strip(1,2); return 50; - break; - case 37:return 'INVALID'; - break; - case 38:return 5; - break; - } - }; - lexer.rules = [/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|\\\{\{|\\\\\{\{|$)))/,/^(?:\{\{\{\{\/[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.])\}\}\}\})/,/^(?:[^\x00]*?(?=(\{\{\{\{\/)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\()/,/^(?:\))/,/^(?:\{\{\{\{)/,/^(?:\}\}\}\})/,/^(?:\{\{(~)?>)/,/^(?:\{\{(~)?#)/,/^(?:\{\{(~)?\/)/,/^(?:\{\{(~)?\^\s*(~)?\}\})/,/^(?:\{\{(~)?\s*else\s*(~)?\}\})/,/^(?:\{\{(~)?\^)/,/^(?:\{\{(~)?\s*else\b)/,/^(?:\{\{(~)?\{)/,/^(?:\{\{(~)?&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{(~)?)/,/^(?:=)/,/^(?:\.\.)/,/^(?:\.(?=([=~}\s\/.)])))/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}(~)?\}\})/,/^(?:(~)?\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=([~}\s)])))/,/^(?:false(?=([~}\s)])))/,/^(?:-?[0-9]+(?:\.[0-9]+)?(?=([~}\s)])))/,/^(?:([^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=([=~}\s\/.)]))))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/]; - lexer.conditions = {"mu":{"rules":[6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"com":{"rules":[5],"inclusive":false},"raw":{"rules":[3,4],"inclusive":false},"INITIAL":{"rules":[0,1,38],"inclusive":true}}; - return lexer;})() - parser.lexer = lexer; - function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser; - return new Parser; - })();__exports__ = handlebars; - /* jshint ignore:end */ - return __exports__; -})(); - -// handlebars/compiler/helpers.js -var __module10__ = (function(__dependency1__) { - "use strict"; - var __exports__ = {}; - var Exception = __dependency1__; - - function stripFlags(open, close) { - return { - left: open.charAt(2) === '~', - right: close.charAt(close.length-3) === '~' - }; - } - - __exports__.stripFlags = stripFlags; - function prepareBlock(mustache, program, inverseAndProgram, close, inverted, locInfo) { - /*jshint -W040 */ - if (mustache.sexpr.id.original !== close.path.original) { - throw new Exception(mustache.sexpr.id.original + ' doesn\'t match ' + close.path.original, mustache); - } - - var inverse = inverseAndProgram && inverseAndProgram.program; - - var strip = { - left: mustache.strip.left, - right: close.strip.right, - - // Determine the standalone candiacy. Basically flag our content as being possibly standalone - // so our parent can determine if we actually are standalone - openStandalone: isNextWhitespace(program.statements), - closeStandalone: isPrevWhitespace((inverse || program).statements) - }; - - if (mustache.strip.right) { - omitRight(program.statements, null, true); - } - - if (inverse) { - var inverseStrip = inverseAndProgram.strip; - - if (inverseStrip.left) { - omitLeft(program.statements, null, true); - } - if (inverseStrip.right) { - omitRight(inverse.statements, null, true); - } - if (close.strip.left) { - omitLeft(inverse.statements, null, true); - } - - // Find standalone else statments - if (isPrevWhitespace(program.statements) - && isNextWhitespace(inverse.statements)) { - - omitLeft(program.statements); - omitRight(inverse.statements); - } - } else { - if (close.strip.left) { - omitLeft(program.statements, null, true); - } - } - - if (inverted) { - return new this.BlockNode(mustache, inverse, program, strip, locInfo); - } else { - return new this.BlockNode(mustache, program, inverse, strip, locInfo); - } - } - - __exports__.prepareBlock = prepareBlock; - function prepareProgram(statements, isRoot) { - for (var i = 0, l = statements.length; i < l; i++) { - var current = statements[i], - strip = current.strip; - - if (!strip) { - continue; - } - - var _isPrevWhitespace = isPrevWhitespace(statements, i, isRoot, current.type === 'partial'), - _isNextWhitespace = isNextWhitespace(statements, i, isRoot), - - openStandalone = strip.openStandalone && _isPrevWhitespace, - closeStandalone = strip.closeStandalone && _isNextWhitespace, - inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace; - - if (strip.right) { - omitRight(statements, i, true); - } - if (strip.left) { - omitLeft(statements, i, true); - } - - if (inlineStandalone) { - omitRight(statements, i); - - if (omitLeft(statements, i)) { - // If we are on a standalone node, save the indent info for partials - if (current.type === 'partial') { - current.indent = (/([ \t]+$)/).exec(statements[i-1].original) ? RegExp.$1 : ''; - } - } - } - if (openStandalone) { - omitRight((current.program || current.inverse).statements); - - // Strip out the previous content node if it's whitespace only - omitLeft(statements, i); - } - if (closeStandalone) { - // Always strip the next node - omitRight(statements, i); - - omitLeft((current.inverse || current.program).statements); - } - } - - return statements; - } - - __exports__.prepareProgram = prepareProgram;function isPrevWhitespace(statements, i, isRoot) { - if (i === undefined) { - i = statements.length; - } - - // Nodes that end with newlines are considered whitespace (but are special - // cased for strip operations) - var prev = statements[i-1], - sibling = statements[i-2]; - if (!prev) { - return isRoot; - } - - if (prev.type === 'content') { - return (sibling || !isRoot ? (/\r?\n\s*?$/) : (/(^|\r?\n)\s*?$/)).test(prev.original); - } - } - function isNextWhitespace(statements, i, isRoot) { - if (i === undefined) { - i = -1; - } - - var next = statements[i+1], - sibling = statements[i+2]; - if (!next) { - return isRoot; - } - - if (next.type === 'content') { - return (sibling || !isRoot ? (/^\s*?\r?\n/) : (/^\s*?(\r?\n|$)/)).test(next.original); - } - } - - // Marks the node to the right of the position as omitted. - // I.e. {{foo}}' ' will mark the ' ' node as omitted. - // - // If i is undefined, then the first child will be marked as such. - // - // If mulitple is truthy then all whitespace will be stripped out until non-whitespace - // content is met. - function omitRight(statements, i, multiple) { - var current = statements[i == null ? 0 : i + 1]; - if (!current || current.type !== 'content' || (!multiple && current.rightStripped)) { - return; - } - - var original = current.string; - current.string = current.string.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), ''); - current.rightStripped = current.string !== original; - } - - // Marks the node to the left of the position as omitted. - // I.e. ' '{{foo}} will mark the ' ' node as omitted. - // - // If i is undefined then the last child will be marked as such. - // - // If mulitple is truthy then all whitespace will be stripped out until non-whitespace - // content is met. - function omitLeft(statements, i, multiple) { - var current = statements[i == null ? statements.length - 1 : i - 1]; - if (!current || current.type !== 'content' || (!multiple && current.leftStripped)) { - return; - } - - // We omit the last node if it's whitespace only and not preceeded by a non-content node. - var original = current.string; - current.string = current.string.replace(multiple ? (/\s+$/) : (/[ \t]+$/), ''); - current.leftStripped = current.string !== original; - return current.leftStripped; - } - return __exports__; -})(__module5__); - -// handlebars/compiler/base.js -var __module8__ = (function(__dependency1__, __dependency2__, __dependency3__, __dependency4__) { - "use strict"; - var __exports__ = {}; - var parser = __dependency1__; - var AST = __dependency2__; - var Helpers = __dependency3__; - var extend = __dependency4__.extend; - - __exports__.parser = parser; - - var yy = {}; - extend(yy, Helpers, AST); - - function parse(input) { - // Just return if an already-compile AST was passed in. - if (input.constructor === AST.ProgramNode) { return input; } - - parser.yy = yy; - - return parser.parse(input); - } - - __exports__.parse = parse; - return __exports__; -})(__module9__, __module7__, __module10__, __module3__); - -// handlebars/compiler/compiler.js -var __module11__ = (function(__dependency1__, __dependency2__) { - "use strict"; - var __exports__ = {}; - var Exception = __dependency1__; - var isArray = __dependency2__.isArray; - - var slice = [].slice; - - function Compiler() {} - - __exports__.Compiler = Compiler;// the foundHelper register will disambiguate helper lookup from finding a - // function in a context. This is necessary for mustache compatibility, which - // requires that context functions in blocks are evaluated by blockHelperMissing, - // and then proceed as if the resulting value was provided to blockHelperMissing. - - Compiler.prototype = { - compiler: Compiler, - - equals: function(other) { - var len = this.opcodes.length; - if (other.opcodes.length !== len) { - return false; - } - - for (var i = 0; i < len; i++) { - var opcode = this.opcodes[i], - otherOpcode = other.opcodes[i]; - if (opcode.opcode !== otherOpcode.opcode || !argEquals(opcode.args, otherOpcode.args)) { - return false; - } - } - - // We know that length is the same between the two arrays because they are directly tied - // to the opcode behavior above. - len = this.children.length; - for (i = 0; i < len; i++) { - if (!this.children[i].equals(other.children[i])) { - return false; - } - } - - return true; - }, - - guid: 0, - - compile: function(program, options) { - this.opcodes = []; - this.children = []; - this.depths = {list: []}; - this.options = options; - this.stringParams = options.stringParams; - this.trackIds = options.trackIds; - - // These changes will propagate to the other compiler components - var knownHelpers = this.options.knownHelpers; - this.options.knownHelpers = { - 'helperMissing': true, - 'blockHelperMissing': true, - 'each': true, - 'if': true, - 'unless': true, - 'with': true, - 'log': true, - 'lookup': true - }; - if (knownHelpers) { - for (var name in knownHelpers) { - this.options.knownHelpers[name] = knownHelpers[name]; - } - } - - return this.accept(program); - }, - - accept: function(node) { - return this[node.type](node); - }, - - program: function(program) { - var statements = program.statements; - - for(var i=0, l=statements.length; i 0) { - varDeclarations += ", " + locals.join(", "); - } - - // Generate minimizer alias mappings - for (var alias in this.aliases) { - if (this.aliases.hasOwnProperty(alias)) { - varDeclarations += ', ' + alias + '=' + this.aliases[alias]; - } - } - - var params = ["depth0", "helpers", "partials", "data"]; - - if (this.useDepths) { - params.push('depths'); - } - - // Perform a second pass over the output to merge content when possible - var source = this.mergeSource(varDeclarations); - - if (asObject) { - params.push(source); - - return Function.apply(this, params); - } else { - return 'function(' + params.join(',') + ') {\n ' + source + '}'; - } - }, - mergeSource: function(varDeclarations) { - var source = '', - buffer, - appendOnly = !this.forceBuffer, - appendFirst; - - for (var i = 0, len = this.source.length; i < len; i++) { - var line = this.source[i]; - if (line.appendToBuffer) { - if (buffer) { - buffer = buffer + '\n + ' + line.content; - } else { - buffer = line.content; - } - } else { - if (buffer) { - if (!source) { - appendFirst = true; - source = buffer + ';\n '; - } else { - source += 'buffer += ' + buffer + ';\n '; - } - buffer = undefined; - } - source += line + '\n '; - - if (!this.environment.isSimple) { - appendOnly = false; - } - } - } - - if (appendOnly) { - if (buffer || !source) { - source += 'return ' + (buffer || '""') + ';\n'; - } - } else { - varDeclarations += ", buffer = " + (appendFirst ? '' : this.initializeBuffer()); - if (buffer) { - source += 'return buffer + ' + buffer + ';\n'; - } else { - source += 'return buffer;\n'; - } - } - - if (varDeclarations) { - source = 'var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n ') + source; - } - - return source; - }, - - // [blockValue] - // - // On stack, before: hash, inverse, program, value - // On stack, after: return value of blockHelperMissing - // - // The purpose of this opcode is to take a block of the form - // `{{#this.foo}}...{{/this.foo}}`, resolve the value of `foo`, and - // replace it on the stack with the result of properly - // invoking blockHelperMissing. - blockValue: function(name) { - this.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; - - var params = [this.contextName(0)]; - this.setupParams(name, 0, params); - - var blockName = this.popStack(); - params.splice(1, 0, blockName); - - this.push('blockHelperMissing.call(' + params.join(', ') + ')'); - }, - - // [ambiguousBlockValue] - // - // On stack, before: hash, inverse, program, value - // Compiler value, before: lastHelper=value of last found helper, if any - // On stack, after, if no lastHelper: same as [blockValue] - // On stack, after, if lastHelper: value - ambiguousBlockValue: function() { - this.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; - - // We're being a bit cheeky and reusing the options value from the prior exec - var params = [this.contextName(0)]; - this.setupParams('', 0, params, true); - - this.flushInline(); - - var current = this.topStack(); - params.splice(1, 0, current); - - this.pushSource("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }"); - }, - - // [appendContent] - // - // On stack, before: ... - // On stack, after: ... - // - // Appends the string value of `content` to the current buffer - appendContent: function(content) { - if (this.pendingContent) { - content = this.pendingContent + content; - } - - this.pendingContent = content; - }, - - // [append] - // - // On stack, before: value, ... - // On stack, after: ... - // - // Coerces `value` to a String and appends it to the current buffer. - // - // If `value` is truthy, or 0, it is coerced into a string and appended - // Otherwise, the empty string is appended - append: function() { - // Force anything that is inlined onto the stack so we don't have duplication - // when we examine local - this.flushInline(); - var local = this.popStack(); - this.pushSource('if (' + local + ' != null) { ' + this.appendToBuffer(local) + ' }'); - if (this.environment.isSimple) { - this.pushSource("else { " + this.appendToBuffer("''") + " }"); - } - }, - - // [appendEscaped] - // - // On stack, before: value, ... - // On stack, after: ... - // - // Escape `value` and append it to the buffer - appendEscaped: function() { - this.aliases.escapeExpression = 'this.escapeExpression'; - - this.pushSource(this.appendToBuffer("escapeExpression(" + this.popStack() + ")")); - }, - - // [getContext] - // - // On stack, before: ... - // On stack, after: ... - // Compiler value, after: lastContext=depth - // - // Set the value of the `lastContext` compiler value to the depth - getContext: function(depth) { - this.lastContext = depth; - }, - - // [pushContext] - // - // On stack, before: ... - // On stack, after: currentContext, ... - // - // Pushes the value of the current context onto the stack. - pushContext: function() { - this.pushStackLiteral(this.contextName(this.lastContext)); - }, - - // [lookupOnContext] - // - // On stack, before: ... - // On stack, after: currentContext[name], ... - // - // Looks up the value of `name` on the current context and pushes - // it onto the stack. - lookupOnContext: function(parts, falsy, scoped) { - /*jshint -W083 */ - var i = 0, - len = parts.length; - - if (!scoped && this.options.compat && !this.lastContext) { - // The depthed query is expected to handle the undefined logic for the root level that - // is implemented below, so we evaluate that directly in compat mode - this.push(this.depthedLookup(parts[i++])); - } else { - this.pushContext(); - } - - for (; i < len; i++) { - this.replaceStack(function(current) { - var lookup = this.nameLookup(current, parts[i], 'context'); - // We want to ensure that zero and false are handled properly if the context (falsy flag) - // needs to have the special handling for these values. - if (!falsy) { - return ' != null ? ' + lookup + ' : ' + current; - } else { - // Otherwise we can use generic falsy handling - return ' && ' + lookup; - } - }); - } - }, - - // [lookupData] - // - // On stack, before: ... - // On stack, after: data, ... - // - // Push the data lookup operator - lookupData: function(depth, parts) { - /*jshint -W083 */ - if (!depth) { - this.pushStackLiteral('data'); - } else { - this.pushStackLiteral('this.data(data, ' + depth + ')'); - } - - var len = parts.length; - for (var i = 0; i < len; i++) { - this.replaceStack(function(current) { - return ' && ' + this.nameLookup(current, parts[i], 'data'); - }); - } - }, - - // [resolvePossibleLambda] - // - // On stack, before: value, ... - // On stack, after: resolved value, ... - // - // If the `value` is a lambda, replace it on the stack by - // the return value of the lambda - resolvePossibleLambda: function() { - this.aliases.lambda = 'this.lambda'; - - this.push('lambda(' + this.popStack() + ', ' + this.contextName(0) + ')'); - }, - - // [pushStringParam] - // - // On stack, before: ... - // On stack, after: string, currentContext, ... - // - // This opcode is designed for use in string mode, which - // provides the string value of a parameter along with its - // depth rather than resolving it immediately. - pushStringParam: function(string, type) { - this.pushContext(); - this.pushString(type); - - // If it's a subexpression, the string result - // will be pushed after this opcode. - if (type !== 'sexpr') { - if (typeof string === 'string') { - this.pushString(string); - } else { - this.pushStackLiteral(string); - } - } - }, - - emptyHash: function() { - this.pushStackLiteral('{}'); - - if (this.trackIds) { - this.push('{}'); // hashIds - } - if (this.stringParams) { - this.push('{}'); // hashContexts - this.push('{}'); // hashTypes - } - }, - pushHash: function() { - if (this.hash) { - this.hashes.push(this.hash); - } - this.hash = {values: [], types: [], contexts: [], ids: []}; - }, - popHash: function() { - var hash = this.hash; - this.hash = this.hashes.pop(); - - if (this.trackIds) { - this.push('{' + hash.ids.join(',') + '}'); - } - if (this.stringParams) { - this.push('{' + hash.contexts.join(',') + '}'); - this.push('{' + hash.types.join(',') + '}'); - } - - this.push('{\n ' + hash.values.join(',\n ') + '\n }'); - }, - - // [pushString] - // - // On stack, before: ... - // On stack, after: quotedString(string), ... - // - // Push a quoted version of `string` onto the stack - pushString: function(string) { - this.pushStackLiteral(this.quotedString(string)); - }, - - // [push] - // - // On stack, before: ... - // On stack, after: expr, ... - // - // Push an expression onto the stack - push: function(expr) { - this.inlineStack.push(expr); - return expr; - }, - - // [pushLiteral] - // - // On stack, before: ... - // On stack, after: value, ... - // - // Pushes a value onto the stack. This operation prevents - // the compiler from creating a temporary variable to hold - // it. - pushLiteral: function(value) { - this.pushStackLiteral(value); - }, - - // [pushProgram] - // - // On stack, before: ... - // On stack, after: program(guid), ... - // - // Push a program expression onto the stack. This takes - // a compile-time guid and converts it into a runtime-accessible - // expression. - pushProgram: function(guid) { - if (guid != null) { - this.pushStackLiteral(this.programExpression(guid)); - } else { - this.pushStackLiteral(null); - } - }, - - // [invokeHelper] - // - // On stack, before: hash, inverse, program, params..., ... - // On stack, after: result of helper invocation - // - // Pops off the helper's parameters, invokes the helper, - // and pushes the helper's return value onto the stack. - // - // If the helper is not found, `helperMissing` is called. - invokeHelper: function(paramSize, name, isSimple) { - this.aliases.helperMissing = 'helpers.helperMissing'; - - var nonHelper = this.popStack(); - var helper = this.setupHelper(paramSize, name); - - var lookup = (isSimple ? helper.name + ' || ' : '') + nonHelper + ' || helperMissing'; - this.push('((' + lookup + ').call(' + helper.callParams + '))'); - }, - - // [invokeKnownHelper] - // - // On stack, before: hash, inverse, program, params..., ... - // On stack, after: result of helper invocation - // - // This operation is used when the helper is known to exist, - // so a `helperMissing` fallback is not required. - invokeKnownHelper: function(paramSize, name) { - var helper = this.setupHelper(paramSize, name); - this.push(helper.name + ".call(" + helper.callParams + ")"); - }, - - // [invokeAmbiguous] - // - // On stack, before: hash, inverse, program, params..., ... - // On stack, after: result of disambiguation - // - // This operation is used when an expression like `{{foo}}` - // is provided, but we don't know at compile-time whether it - // is a helper or a path. - // - // This operation emits more code than the other options, - // and can be avoided by passing the `knownHelpers` and - // `knownHelpersOnly` flags at compile-time. - invokeAmbiguous: function(name, helperCall) { - this.aliases.functionType = '"function"'; - this.aliases.helperMissing = 'helpers.helperMissing'; - this.useRegister('helper'); - - var nonHelper = this.popStack(); - - this.emptyHash(); - var helper = this.setupHelper(0, name, helperCall); - - var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper'); - - this.push( - '((helper = (helper = ' + helperName + ' || ' + nonHelper + ') != null ? helper : helperMissing' - + (helper.paramsInit ? '),(' + helper.paramsInit : '') + '),' - + '(typeof helper === functionType ? helper.call(' + helper.callParams + ') : helper))'); - }, - - // [invokePartial] - // - // On stack, before: context, ... - // On stack after: result of partial invocation - // - // This operation pops off a context, invokes a partial with that context, - // and pushes the result of the invocation back. - invokePartial: function(name, indent) { - var params = [this.nameLookup('partials', name, 'partial'), "'" + indent + "'", "'" + name + "'", this.popStack(), this.popStack(), "helpers", "partials"]; - - if (this.options.data) { - params.push("data"); - } else if (this.options.compat) { - params.push('undefined'); - } - if (this.options.compat) { - params.push('depths'); - } - - this.push("this.invokePartial(" + params.join(", ") + ")"); - }, - - // [assignToHash] - // - // On stack, before: value, ..., hash, ... - // On stack, after: ..., hash, ... - // - // Pops a value off the stack and assigns it to the current hash - assignToHash: function(key) { - var value = this.popStack(), - context, - type, - id; - - if (this.trackIds) { - id = this.popStack(); - } - if (this.stringParams) { - type = this.popStack(); - context = this.popStack(); - } - - var hash = this.hash; - if (context) { - hash.contexts.push("'" + key + "': " + context); - } - if (type) { - hash.types.push("'" + key + "': " + type); - } - if (id) { - hash.ids.push("'" + key + "': " + id); - } - hash.values.push("'" + key + "': (" + value + ")"); - }, - - pushId: function(type, name) { - if (type === 'ID' || type === 'DATA') { - this.pushString(name); - } else if (type === 'sexpr') { - this.pushStackLiteral('true'); - } else { - this.pushStackLiteral('null'); - } - }, - - // HELPERS - - compiler: JavaScriptCompiler, - - compileChildren: function(environment, options) { - var children = environment.children, child, compiler; - - for(var i=0, l=children.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } - return this.topStackName(); - }, - topStackName: function() { - return "stack" + this.stackSlot; - }, - flushInline: function() { - var inlineStack = this.inlineStack; - if (inlineStack.length) { - this.inlineStack = []; - for (var i = 0, len = inlineStack.length; i < len; i++) { - var entry = inlineStack[i]; - if (entry instanceof Literal) { - this.compileStack.push(entry); - } else { - this.pushStack(entry); - } - } - } - }, - isInline: function() { - return this.inlineStack.length; - }, - - popStack: function(wrapped) { - var inline = this.isInline(), - item = (inline ? this.inlineStack : this.compileStack).pop(); - - if (!wrapped && (item instanceof Literal)) { - return item.value; - } else { - if (!inline) { - /* istanbul ignore next */ - if (!this.stackSlot) { - throw new Exception('Invalid stack pop'); - } - this.stackSlot--; - } - return item; - } - }, - - topStack: function() { - var stack = (this.isInline() ? this.inlineStack : this.compileStack), - item = stack[stack.length - 1]; - - if (item instanceof Literal) { - return item.value; - } else { - return item; - } - }, - - contextName: function(context) { - if (this.useDepths && context) { - return 'depths[' + context + ']'; - } else { - return 'depth' + context; - } - }, - - quotedString: function(str) { - return '"' + str - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 - .replace(/\u2029/g, '\\u2029') + '"'; - }, - - objectLiteral: function(obj) { - var pairs = []; - - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - pairs.push(this.quotedString(key) + ':' + obj[key]); - } - } - - return '{' + pairs.join(',') + '}'; - }, - - setupHelper: function(paramSize, name, blockHelper) { - var params = [], - paramsInit = this.setupParams(name, paramSize, params, blockHelper); - var foundHelper = this.nameLookup('helpers', name, 'helper'); - - return { - params: params, - paramsInit: paramsInit, - name: foundHelper, - callParams: [this.contextName(0)].concat(params).join(", ") - }; - }, - - setupOptions: function(helper, paramSize, params) { - var options = {}, contexts = [], types = [], ids = [], param, inverse, program; - - options.name = this.quotedString(helper); - options.hash = this.popStack(); - - if (this.trackIds) { - options.hashIds = this.popStack(); - } - if (this.stringParams) { - options.hashTypes = this.popStack(); - options.hashContexts = this.popStack(); - } - - inverse = this.popStack(); - program = this.popStack(); - - // Avoid setting fn and inverse if neither are set. This allows - // helpers to do a check for `if (options.fn)` - if (program || inverse) { - if (!program) { - program = 'this.noop'; - } - - if (!inverse) { - inverse = 'this.noop'; - } - - options.fn = program; - options.inverse = inverse; - } - - // The parameters go on to the stack in order (making sure that they are evaluated in order) - // so we need to pop them off the stack in reverse order - var i = paramSize; - while (i--) { - param = this.popStack(); - params[i] = param; - - if (this.trackIds) { - ids[i] = this.popStack(); - } - if (this.stringParams) { - types[i] = this.popStack(); - contexts[i] = this.popStack(); - } - } - - if (this.trackIds) { - options.ids = "[" + ids.join(",") + "]"; - } - if (this.stringParams) { - options.types = "[" + types.join(",") + "]"; - options.contexts = "[" + contexts.join(",") + "]"; - } - - if (this.options.data) { - options.data = "data"; - } - - return options; - }, - - // the params and contexts arguments are passed in arrays - // to fill in - setupParams: function(helperName, paramSize, params, useRegister) { - var options = this.objectLiteral(this.setupOptions(helperName, paramSize, params)); - - if (useRegister) { - this.useRegister('options'); - params.push('options'); - return 'options=' + options; - } else { - params.push(options); - return ''; - } - } - }; - - var reservedWords = ( - "break else new var" + - " case finally return void" + - " catch for switch while" + - " continue function this with" + - " default if throw" + - " delete in try" + - " do instanceof typeof" + - " abstract enum int short" + - " boolean export interface static" + - " byte extends long super" + - " char final native synchronized" + - " class float package throws" + - " const goto private transient" + - " debugger implements protected volatile" + - " double import public let yield" - ).split(" "); - - var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {}; - - for(var i=0, l=reservedWords.length; i - - - handlebars.js - 2.0.0 - handlebars.js Authors - https://github.com/wycats/handlebars.js/blob/master/LICENSE - https://github.com/wycats/handlebars.js/ - false - Extension of the Mustache logicless template language - - handlebars mustache template html - - - - - diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.min.js b/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.min.js deleted file mode 100644 index 53cf921d4f..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.min.js +++ /dev/null @@ -1,28 +0,0 @@ -/*! - - handlebars v2.0.0 - -Copyright (C) 2011-2014 by Yehuda Katz - -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. - -@license -*/ -!function(a,b){"function"==typeof define&&define.amd?define([],b):"object"==typeof exports?module.exports=b():a.Handlebars=a.Handlebars||b()}(this,function(){var a=function(){"use strict";function a(a){this.string=a}var b;return a.prototype.toString=function(){return""+this.string},b=a}(),b=function(a){"use strict";function b(a){return i[a]}function c(a){for(var b=1;b":">",'"':""","'":"'","`":"`"},j=/[&<>"'`]/g,k=/[&<>"'`]/;g.extend=c;var l=Object.prototype.toString;g.toString=l;var m=function(a){return"function"==typeof a};m(/x/)&&(m=function(a){return"function"==typeof a&&"[object Function]"===l.call(a)});var m;g.isFunction=m;var n=Array.isArray||function(a){return a&&"object"==typeof a?"[object Array]"===l.call(a):!1};return g.isArray=n,g.escapeExpression=d,g.isEmpty=e,g.appendContextPath=f,g}(a),c=function(){"use strict";function a(a,b){var d;b&&b.firstLine&&(d=b.firstLine,a+=" - "+d+":"+b.firstColumn);for(var e=Error.prototype.constructor.call(this,a),f=0;f0?(c.ids&&(c.ids=[c.name]),a.helpers.each(b,c)):d(this);if(c.data&&c.ids){var g=q(c.data);g.contextPath=f.appendContextPath(c.data.contextPath,c.name),c={data:g}}return e(b,c)}),a.registerHelper("each",function(a,b){if(!b)throw new g("Must pass iterator to #each");var c,d,e=b.fn,h=b.inverse,i=0,j="";if(b.data&&b.ids&&(d=f.appendContextPath(b.data.contextPath,b.ids[0])+"."),l(a)&&(a=a.call(this)),b.data&&(c=q(b.data)),a&&"object"==typeof a)if(k(a))for(var m=a.length;m>i;i++)c&&(c.index=i,c.first=0===i,c.last=i===a.length-1,d&&(c.contextPath=d+i)),j+=e(a[i],{data:c});else for(var n in a)a.hasOwnProperty(n)&&(c&&(c.key=n,c.index=i,c.first=0===i,d&&(c.contextPath=d+n)),j+=e(a[n],{data:c}),i++);return 0===i&&(j=h(this)),j}),a.registerHelper("if",function(a,b){return l(a)&&(a=a.call(this)),!b.hash.includeZero&&!a||f.isEmpty(a)?b.inverse(this):b.fn(this)}),a.registerHelper("unless",function(b,c){return a.helpers["if"].call(this,b,{fn:c.inverse,inverse:c.fn,hash:c.hash})}),a.registerHelper("with",function(a,b){l(a)&&(a=a.call(this));var c=b.fn;if(f.isEmpty(a))return b.inverse(this);if(b.data&&b.ids){var d=q(b.data);d.contextPath=f.appendContextPath(b.data.contextPath,b.ids[0]),b={data:d}}return c(a,b)}),a.registerHelper("log",function(b,c){var d=c.data&&null!=c.data.level?parseInt(c.data.level,10):1;a.log(d,b)}),a.registerHelper("lookup",function(a,b){return a&&a[b]})}var e={},f=a,g=b,h="2.0.0";e.VERSION=h;var i=6;e.COMPILER_REVISION=i;var j={1:"<= 1.0.rc.2",2:"== 1.0.0-rc.3",3:"== 1.0.0-rc.4",4:"== 1.x.x",5:"== 2.0.0-alpha.x",6:">= 2.0.0-beta.1"};e.REVISION_CHANGES=j;var k=f.isArray,l=f.isFunction,m=f.toString,n="[object Object]";e.HandlebarsEnvironment=c,c.prototype={constructor:c,logger:o,log:p,registerHelper:function(a,b){if(m.call(a)===n){if(b)throw new g("Arg not supported with multiple helpers");f.extend(this.helpers,a)}else this.helpers[a]=b},unregisterHelper:function(a){delete this.helpers[a]},registerPartial:function(a,b){m.call(a)===n?f.extend(this.partials,a):this.partials[a]=b},unregisterPartial:function(a){delete this.partials[a]}};var o={methodMap:{0:"debug",1:"info",2:"warn",3:"error"},DEBUG:0,INFO:1,WARN:2,ERROR:3,level:3,log:function(a,b){if(o.level<=a){var c=o.methodMap[a];"undefined"!=typeof console&&console[c]&&console[c].call(console,b)}}};e.logger=o;var p=o.log;e.log=p;var q=function(a){var b=f.extend({},a);return b._parent=a,b};return e.createFrame=q,e}(b,c),e=function(a,b,c){"use strict";function d(a){var b=a&&a[0]||1,c=m;if(b!==c){if(c>b){var d=n[c],e=n[b];throw new l("Template was precompiled with an older version of Handlebars than the current runtime. Please update your precompiler to a newer version ("+d+") or downgrade your runtime to an older version ("+e+").")}throw new l("Template was precompiled with a newer version of Handlebars than the current runtime. Please update your runtime to a newer version ("+a[1]+").")}}function e(a,b){if(!b)throw new l("No environment passed to template");if(!a||!a.main)throw new l("Unknown template object: "+typeof a);b.VM.checkRevision(a.compiler);var c=function(c,d,e,f,g,h,i,j,m){g&&(f=k.extend({},f,g));var n=b.VM.invokePartial.call(this,c,e,f,h,i,j,m);if(null==n&&b.compile){var o={helpers:h,partials:i,data:j,depths:m};i[e]=b.compile(c,{data:void 0!==j,compat:a.compat},b),n=i[e](f,o)}if(null!=n){if(d){for(var p=n.split("\n"),q=0,r=p.length;r>q&&(p[q]||q+1!==r);q++)p[q]=d+p[q];n=p.join("\n")}return n}throw new l("The partial "+e+" could not be compiled when running in runtime-only mode")},d={lookup:function(a,b){for(var c=a.length,d=0;c>d;d++)if(a[d]&&null!=a[d][b])return a[d][b]},lambda:function(a,b){return"function"==typeof a?a.call(b):a},escapeExpression:k.escapeExpression,invokePartial:c,fn:function(b){return a[b]},programs:[],program:function(a,b,c){var d=this.programs[a],e=this.fn(a);return b||c?d=f(this,a,e,b,c):d||(d=this.programs[a]=f(this,a,e)),d},data:function(a,b){for(;a&&b--;)a=a._parent;return a},merge:function(a,b){var c=a||b;return a&&b&&a!==b&&(c=k.extend({},b,a)),c},noop:b.VM.noop,compilerInfo:a.compiler},e=function(b,c){c=c||{};var f=c.data;e._setup(c),!c.partial&&a.useData&&(f=i(b,f));var g;return a.useDepths&&(g=c.depths?[b].concat(c.depths):[b]),a.main.call(d,b,d.helpers,d.partials,f,g)};return e.isTop=!0,e._setup=function(c){c.partial?(d.helpers=c.helpers,d.partials=c.partials):(d.helpers=d.merge(c.helpers,b.helpers),a.usePartial&&(d.partials=d.merge(c.partials,b.partials)))},e._child=function(b,c,e){if(a.useDepths&&!e)throw new l("must pass parent depths");return f(d,b,a[b],c,e)},e}function f(a,b,c,d,e){var f=function(b,f){return f=f||{},c.call(a,b,a.helpers,a.partials,f.data||d,e&&[b].concat(e))};return f.program=b,f.depth=e?e.length:0,f}function g(a,b,c,d,e,f,g){var h={partial:!0,helpers:d,partials:e,data:f,depths:g};if(void 0===a)throw new l("The partial "+b+" could not be found");return a instanceof Function?a(c,h):void 0}function h(){return""}function i(a,b){return b&&"root"in b||(b=b?o(b):{},b.root=a),b}var j={},k=a,l=b,m=c.COMPILER_REVISION,n=c.REVISION_CHANGES,o=c.createFrame;return j.checkRevision=d,j.template=e,j.program=f,j.invokePartial=g,j.noop=h,j}(b,c,d),f=function(a,b,c,d,e){"use strict";var f,g=a,h=b,i=c,j=d,k=e,l=function(){var a=new g.HandlebarsEnvironment;return j.extend(a,g),a.SafeString=h,a.Exception=i,a.Utils=j,a.escapeExpression=j.escapeExpression,a.VM=k,a.template=function(b){return k.template(b,a)},a},m=l();return m.create=l,m["default"]=m,f=m}(d,a,c,b,e),g=function(a){"use strict";function b(a){a=a||{},this.firstLine=a.first_line,this.firstColumn=a.first_column,this.lastColumn=a.last_column,this.lastLine=a.last_line}var c,d=a,e={ProgramNode:function(a,c,d){b.call(this,d),this.type="program",this.statements=a,this.strip=c},MustacheNode:function(a,c,d,f,g){if(b.call(this,g),this.type="mustache",this.strip=f,null!=d&&d.charAt){var h=d.charAt(3)||d.charAt(2);this.escaped="{"!==h&&"&"!==h}else this.escaped=!!d;this.sexpr=a instanceof e.SexprNode?a:new e.SexprNode(a,c),this.id=this.sexpr.id,this.params=this.sexpr.params,this.hash=this.sexpr.hash,this.eligibleHelper=this.sexpr.eligibleHelper,this.isHelper=this.sexpr.isHelper},SexprNode:function(a,c,d){b.call(this,d),this.type="sexpr",this.hash=c;var e=this.id=a[0],f=this.params=a.slice(1);this.isHelper=!(!f.length&&!c),this.eligibleHelper=this.isHelper||e.isSimple},PartialNode:function(a,c,d,e,f){b.call(this,f),this.type="partial",this.partialName=a,this.context=c,this.hash=d,this.strip=e,this.strip.inlineStandalone=!0},BlockNode:function(a,c,d,e,f){b.call(this,f),this.type="block",this.mustache=a,this.program=c,this.inverse=d,this.strip=e,d&&!c&&(this.isInverse=!0)},RawBlockNode:function(a,c,f,g){if(b.call(this,g),a.sexpr.id.original!==f)throw new d(a.sexpr.id.original+" doesn't match "+f,this);c=new e.ContentNode(c,g),this.type="block",this.mustache=a,this.program=new e.ProgramNode([c],{},g)},ContentNode:function(a,c){b.call(this,c),this.type="content",this.original=this.string=a},HashNode:function(a,c){b.call(this,c),this.type="hash",this.pairs=a},IdNode:function(a,c){b.call(this,c),this.type="ID";for(var e="",f=[],g=0,h="",i=0,j=a.length;j>i;i++){var k=a[i].part;if(e+=(a[i].separator||"")+k,".."===k||"."===k||"this"===k){if(f.length>0)throw new d("Invalid path: "+e,this);".."===k?(g++,h+="../"):this.isScoped=!0}else f.push(k)}this.original=e,this.parts=f,this.string=f.join("."),this.depth=g,this.idName=h+this.string,this.isSimple=1===a.length&&!this.isScoped&&0===g,this.stringModeValue=this.string},PartialNameNode:function(a,c){b.call(this,c),this.type="PARTIAL_NAME",this.name=a.original},DataNode:function(a,c){b.call(this,c),this.type="DATA",this.id=a,this.stringModeValue=a.stringModeValue,this.idName="@"+a.stringModeValue},StringNode:function(a,c){b.call(this,c),this.type="STRING",this.original=this.string=this.stringModeValue=a},NumberNode:function(a,c){b.call(this,c),this.type="NUMBER",this.original=this.number=a,this.stringModeValue=Number(a)},BooleanNode:function(a,c){b.call(this,c),this.type="BOOLEAN",this.bool=a,this.stringModeValue="true"===a},CommentNode:function(a,c){b.call(this,c),this.type="comment",this.comment=a,this.strip={inlineStandalone:!0}}};return c=e}(c),h=function(){"use strict";var a,b=function(){function a(){this.yy={}}var b={trace:function(){},yy:{},symbols_:{error:2,root:3,program:4,EOF:5,program_repetition0:6,statement:7,mustache:8,block:9,rawBlock:10,partial:11,CONTENT:12,COMMENT:13,openRawBlock:14,END_RAW_BLOCK:15,OPEN_RAW_BLOCK:16,sexpr:17,CLOSE_RAW_BLOCK:18,openBlock:19,block_option0:20,closeBlock:21,openInverse:22,block_option1:23,OPEN_BLOCK:24,CLOSE:25,OPEN_INVERSE:26,inverseAndProgram:27,INVERSE:28,OPEN_ENDBLOCK:29,path:30,OPEN:31,OPEN_UNESCAPED:32,CLOSE_UNESCAPED:33,OPEN_PARTIAL:34,partialName:35,param:36,partial_option0:37,partial_option1:38,sexpr_repetition0:39,sexpr_option0:40,dataName:41,STRING:42,NUMBER:43,BOOLEAN:44,OPEN_SEXPR:45,CLOSE_SEXPR:46,hash:47,hash_repetition_plus0:48,hashSegment:49,ID:50,EQUALS:51,DATA:52,pathSegments:53,SEP:54,$accept:0,$end:1},terminals_:{2:"error",5:"EOF",12:"CONTENT",13:"COMMENT",15:"END_RAW_BLOCK",16:"OPEN_RAW_BLOCK",18:"CLOSE_RAW_BLOCK",24:"OPEN_BLOCK",25:"CLOSE",26:"OPEN_INVERSE",28:"INVERSE",29:"OPEN_ENDBLOCK",31:"OPEN",32:"OPEN_UNESCAPED",33:"CLOSE_UNESCAPED",34:"OPEN_PARTIAL",42:"STRING",43:"NUMBER",44:"BOOLEAN",45:"OPEN_SEXPR",46:"CLOSE_SEXPR",50:"ID",51:"EQUALS",52:"DATA",54:"SEP"},productions_:[0,[3,2],[4,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[10,3],[14,3],[9,4],[9,4],[19,3],[22,3],[27,2],[21,3],[8,3],[8,3],[11,5],[11,4],[17,3],[17,1],[36,1],[36,1],[36,1],[36,1],[36,1],[36,3],[47,1],[49,3],[35,1],[35,1],[35,1],[41,2],[30,1],[53,3],[53,1],[6,0],[6,2],[20,0],[20,1],[23,0],[23,1],[37,0],[37,1],[38,0],[38,1],[39,0],[39,2],[40,0],[40,1],[48,1],[48,2]],performAction:function(a,b,c,d,e,f){var g=f.length-1;switch(e){case 1:return d.prepareProgram(f[g-1].statements,!0),f[g-1];case 2:this.$=new d.ProgramNode(d.prepareProgram(f[g]),{},this._$);break;case 3:this.$=f[g];break;case 4:this.$=f[g];break;case 5:this.$=f[g];break;case 6:this.$=f[g];break;case 7:this.$=new d.ContentNode(f[g],this._$);break;case 8:this.$=new d.CommentNode(f[g],this._$);break;case 9:this.$=new d.RawBlockNode(f[g-2],f[g-1],f[g],this._$);break;case 10:this.$=new d.MustacheNode(f[g-1],null,"","",this._$);break;case 11:this.$=d.prepareBlock(f[g-3],f[g-2],f[g-1],f[g],!1,this._$);break;case 12:this.$=d.prepareBlock(f[g-3],f[g-2],f[g-1],f[g],!0,this._$);break;case 13:this.$=new d.MustacheNode(f[g-1],null,f[g-2],d.stripFlags(f[g-2],f[g]),this._$);break;case 14:this.$=new d.MustacheNode(f[g-1],null,f[g-2],d.stripFlags(f[g-2],f[g]),this._$);break;case 15:this.$={strip:d.stripFlags(f[g-1],f[g-1]),program:f[g]};break;case 16:this.$={path:f[g-1],strip:d.stripFlags(f[g-2],f[g])};break;case 17:this.$=new d.MustacheNode(f[g-1],null,f[g-2],d.stripFlags(f[g-2],f[g]),this._$);break;case 18:this.$=new d.MustacheNode(f[g-1],null,f[g-2],d.stripFlags(f[g-2],f[g]),this._$);break;case 19:this.$=new d.PartialNode(f[g-3],f[g-2],f[g-1],d.stripFlags(f[g-4],f[g]),this._$);break;case 20:this.$=new d.PartialNode(f[g-2],void 0,f[g-1],d.stripFlags(f[g-3],f[g]),this._$);break;case 21:this.$=new d.SexprNode([f[g-2]].concat(f[g-1]),f[g],this._$);break;case 22:this.$=new d.SexprNode([f[g]],null,this._$);break;case 23:this.$=f[g];break;case 24:this.$=new d.StringNode(f[g],this._$);break;case 25:this.$=new d.NumberNode(f[g],this._$);break;case 26:this.$=new d.BooleanNode(f[g],this._$);break;case 27:this.$=f[g];break;case 28:f[g-1].isHelper=!0,this.$=f[g-1];break;case 29:this.$=new d.HashNode(f[g],this._$);break;case 30:this.$=[f[g-2],f[g]];break;case 31:this.$=new d.PartialNameNode(f[g],this._$);break;case 32:this.$=new d.PartialNameNode(new d.StringNode(f[g],this._$),this._$);break;case 33:this.$=new d.PartialNameNode(new d.NumberNode(f[g],this._$));break;case 34:this.$=new d.DataNode(f[g],this._$);break;case 35:this.$=new d.IdNode(f[g],this._$);break;case 36:f[g-2].push({part:f[g],separator:f[g-1]}),this.$=f[g-2];break;case 37:this.$=[{part:f[g]}];break;case 38:this.$=[];break;case 39:f[g-1].push(f[g]);break;case 48:this.$=[];break;case 49:f[g-1].push(f[g]);break;case 52:this.$=[f[g]];break;case 53:f[g-1].push(f[g])}},table:[{3:1,4:2,5:[2,38],6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],31:[2,38],32:[2,38],34:[2,38]},{1:[3]},{5:[1,4]},{5:[2,2],7:5,8:6,9:7,10:8,11:9,12:[1,10],13:[1,11],14:16,16:[1,20],19:14,22:15,24:[1,18],26:[1,19],28:[2,2],29:[2,2],31:[1,12],32:[1,13],34:[1,17]},{1:[2,1]},{5:[2,39],12:[2,39],13:[2,39],16:[2,39],24:[2,39],26:[2,39],28:[2,39],29:[2,39],31:[2,39],32:[2,39],34:[2,39]},{5:[2,3],12:[2,3],13:[2,3],16:[2,3],24:[2,3],26:[2,3],28:[2,3],29:[2,3],31:[2,3],32:[2,3],34:[2,3]},{5:[2,4],12:[2,4],13:[2,4],16:[2,4],24:[2,4],26:[2,4],28:[2,4],29:[2,4],31:[2,4],32:[2,4],34:[2,4]},{5:[2,5],12:[2,5],13:[2,5],16:[2,5],24:[2,5],26:[2,5],28:[2,5],29:[2,5],31:[2,5],32:[2,5],34:[2,5]},{5:[2,6],12:[2,6],13:[2,6],16:[2,6],24:[2,6],26:[2,6],28:[2,6],29:[2,6],31:[2,6],32:[2,6],34:[2,6]},{5:[2,7],12:[2,7],13:[2,7],16:[2,7],24:[2,7],26:[2,7],28:[2,7],29:[2,7],31:[2,7],32:[2,7],34:[2,7]},{5:[2,8],12:[2,8],13:[2,8],16:[2,8],24:[2,8],26:[2,8],28:[2,8],29:[2,8],31:[2,8],32:[2,8],34:[2,8]},{17:21,30:22,41:23,50:[1,26],52:[1,25],53:24},{17:27,30:22,41:23,50:[1,26],52:[1,25],53:24},{4:28,6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],28:[2,38],29:[2,38],31:[2,38],32:[2,38],34:[2,38]},{4:29,6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],28:[2,38],29:[2,38],31:[2,38],32:[2,38],34:[2,38]},{12:[1,30]},{30:32,35:31,42:[1,33],43:[1,34],50:[1,26],53:24},{17:35,30:22,41:23,50:[1,26],52:[1,25],53:24},{17:36,30:22,41:23,50:[1,26],52:[1,25],53:24},{17:37,30:22,41:23,50:[1,26],52:[1,25],53:24},{25:[1,38]},{18:[2,48],25:[2,48],33:[2,48],39:39,42:[2,48],43:[2,48],44:[2,48],45:[2,48],46:[2,48],50:[2,48],52:[2,48]},{18:[2,22],25:[2,22],33:[2,22],46:[2,22]},{18:[2,35],25:[2,35],33:[2,35],42:[2,35],43:[2,35],44:[2,35],45:[2,35],46:[2,35],50:[2,35],52:[2,35],54:[1,40]},{30:41,50:[1,26],53:24},{18:[2,37],25:[2,37],33:[2,37],42:[2,37],43:[2,37],44:[2,37],45:[2,37],46:[2,37],50:[2,37],52:[2,37],54:[2,37]},{33:[1,42]},{20:43,27:44,28:[1,45],29:[2,40]},{23:46,27:47,28:[1,45],29:[2,42]},{15:[1,48]},{25:[2,46],30:51,36:49,38:50,41:55,42:[1,52],43:[1,53],44:[1,54],45:[1,56],47:57,48:58,49:60,50:[1,59],52:[1,25],53:24},{25:[2,31],42:[2,31],43:[2,31],44:[2,31],45:[2,31],50:[2,31],52:[2,31]},{25:[2,32],42:[2,32],43:[2,32],44:[2,32],45:[2,32],50:[2,32],52:[2,32]},{25:[2,33],42:[2,33],43:[2,33],44:[2,33],45:[2,33],50:[2,33],52:[2,33]},{25:[1,61]},{25:[1,62]},{18:[1,63]},{5:[2,17],12:[2,17],13:[2,17],16:[2,17],24:[2,17],26:[2,17],28:[2,17],29:[2,17],31:[2,17],32:[2,17],34:[2,17]},{18:[2,50],25:[2,50],30:51,33:[2,50],36:65,40:64,41:55,42:[1,52],43:[1,53],44:[1,54],45:[1,56],46:[2,50],47:66,48:58,49:60,50:[1,59],52:[1,25],53:24},{50:[1,67]},{18:[2,34],25:[2,34],33:[2,34],42:[2,34],43:[2,34],44:[2,34],45:[2,34],46:[2,34],50:[2,34],52:[2,34]},{5:[2,18],12:[2,18],13:[2,18],16:[2,18],24:[2,18],26:[2,18],28:[2,18],29:[2,18],31:[2,18],32:[2,18],34:[2,18]},{21:68,29:[1,69]},{29:[2,41]},{4:70,6:3,12:[2,38],13:[2,38],16:[2,38],24:[2,38],26:[2,38],29:[2,38],31:[2,38],32:[2,38],34:[2,38]},{21:71,29:[1,69]},{29:[2,43]},{5:[2,9],12:[2,9],13:[2,9],16:[2,9],24:[2,9],26:[2,9],28:[2,9],29:[2,9],31:[2,9],32:[2,9],34:[2,9]},{25:[2,44],37:72,47:73,48:58,49:60,50:[1,74]},{25:[1,75]},{18:[2,23],25:[2,23],33:[2,23],42:[2,23],43:[2,23],44:[2,23],45:[2,23],46:[2,23],50:[2,23],52:[2,23]},{18:[2,24],25:[2,24],33:[2,24],42:[2,24],43:[2,24],44:[2,24],45:[2,24],46:[2,24],50:[2,24],52:[2,24]},{18:[2,25],25:[2,25],33:[2,25],42:[2,25],43:[2,25],44:[2,25],45:[2,25],46:[2,25],50:[2,25],52:[2,25]},{18:[2,26],25:[2,26],33:[2,26],42:[2,26],43:[2,26],44:[2,26],45:[2,26],46:[2,26],50:[2,26],52:[2,26]},{18:[2,27],25:[2,27],33:[2,27],42:[2,27],43:[2,27],44:[2,27],45:[2,27],46:[2,27],50:[2,27],52:[2,27]},{17:76,30:22,41:23,50:[1,26],52:[1,25],53:24},{25:[2,47]},{18:[2,29],25:[2,29],33:[2,29],46:[2,29],49:77,50:[1,74]},{18:[2,37],25:[2,37],33:[2,37],42:[2,37],43:[2,37],44:[2,37],45:[2,37],46:[2,37],50:[2,37],51:[1,78],52:[2,37],54:[2,37]},{18:[2,52],25:[2,52],33:[2,52],46:[2,52],50:[2,52]},{12:[2,13],13:[2,13],16:[2,13],24:[2,13],26:[2,13],28:[2,13],29:[2,13],31:[2,13],32:[2,13],34:[2,13]},{12:[2,14],13:[2,14],16:[2,14],24:[2,14],26:[2,14],28:[2,14],29:[2,14],31:[2,14],32:[2,14],34:[2,14]},{12:[2,10]},{18:[2,21],25:[2,21],33:[2,21],46:[2,21]},{18:[2,49],25:[2,49],33:[2,49],42:[2,49],43:[2,49],44:[2,49],45:[2,49],46:[2,49],50:[2,49],52:[2,49]},{18:[2,51],25:[2,51],33:[2,51],46:[2,51]},{18:[2,36],25:[2,36],33:[2,36],42:[2,36],43:[2,36],44:[2,36],45:[2,36],46:[2,36],50:[2,36],52:[2,36],54:[2,36]},{5:[2,11],12:[2,11],13:[2,11],16:[2,11],24:[2,11],26:[2,11],28:[2,11],29:[2,11],31:[2,11],32:[2,11],34:[2,11]},{30:79,50:[1,26],53:24},{29:[2,15]},{5:[2,12],12:[2,12],13:[2,12],16:[2,12],24:[2,12],26:[2,12],28:[2,12],29:[2,12],31:[2,12],32:[2,12],34:[2,12]},{25:[1,80]},{25:[2,45]},{51:[1,78]},{5:[2,20],12:[2,20],13:[2,20],16:[2,20],24:[2,20],26:[2,20],28:[2,20],29:[2,20],31:[2,20],32:[2,20],34:[2,20]},{46:[1,81]},{18:[2,53],25:[2,53],33:[2,53],46:[2,53],50:[2,53]},{30:51,36:82,41:55,42:[1,52],43:[1,53],44:[1,54],45:[1,56],50:[1,26],52:[1,25],53:24},{25:[1,83]},{5:[2,19],12:[2,19],13:[2,19],16:[2,19],24:[2,19],26:[2,19],28:[2,19],29:[2,19],31:[2,19],32:[2,19],34:[2,19]},{18:[2,28],25:[2,28],33:[2,28],42:[2,28],43:[2,28],44:[2,28],45:[2,28],46:[2,28],50:[2,28],52:[2,28]},{18:[2,30],25:[2,30],33:[2,30],46:[2,30],50:[2,30]},{5:[2,16],12:[2,16],13:[2,16],16:[2,16],24:[2,16],26:[2,16],28:[2,16],29:[2,16],31:[2,16],32:[2,16],34:[2,16]}],defaultActions:{4:[2,1],44:[2,41],47:[2,43],57:[2,47],63:[2,10],70:[2,15],73:[2,45]},parseError:function(a){throw new Error(a)},parse:function(a){function b(){var a;return a=c.lexer.lex()||1,"number"!=typeof a&&(a=c.symbols_[a]||a),a}var c=this,d=[0],e=[null],f=[],g=this.table,h="",i=0,j=0,k=0;this.lexer.setInput(a),this.lexer.yy=this.yy,this.yy.lexer=this.lexer,this.yy.parser=this,"undefined"==typeof this.lexer.yylloc&&(this.lexer.yylloc={});var l=this.lexer.yylloc;f.push(l);var m=this.lexer.options&&this.lexer.options.ranges;"function"==typeof this.yy.parseError&&(this.parseError=this.yy.parseError);for(var n,o,p,q,r,s,t,u,v,w={};;){if(p=d[d.length-1],this.defaultActions[p]?q=this.defaultActions[p]:((null===n||"undefined"==typeof n)&&(n=b()),q=g[p]&&g[p][n]),"undefined"==typeof q||!q.length||!q[0]){var x="";if(!k){v=[];for(s in g[p])this.terminals_[s]&&s>2&&v.push("'"+this.terminals_[s]+"'");x=this.lexer.showPosition?"Parse error on line "+(i+1)+":\n"+this.lexer.showPosition()+"\nExpecting "+v.join(", ")+", got '"+(this.terminals_[n]||n)+"'":"Parse error on line "+(i+1)+": Unexpected "+(1==n?"end of input":"'"+(this.terminals_[n]||n)+"'"),this.parseError(x,{text:this.lexer.match,token:this.terminals_[n]||n,line:this.lexer.yylineno,loc:l,expected:v})}}if(q[0]instanceof Array&&q.length>1)throw new Error("Parse Error: multiple actions possible at state: "+p+", token: "+n);switch(q[0]){case 1:d.push(n),e.push(this.lexer.yytext),f.push(this.lexer.yylloc),d.push(q[1]),n=null,o?(n=o,o=null):(j=this.lexer.yyleng,h=this.lexer.yytext,i=this.lexer.yylineno,l=this.lexer.yylloc,k>0&&k--);break;case 2:if(t=this.productions_[q[1]][1],w.$=e[e.length-t],w._$={first_line:f[f.length-(t||1)].first_line,last_line:f[f.length-1].last_line,first_column:f[f.length-(t||1)].first_column,last_column:f[f.length-1].last_column},m&&(w._$.range=[f[f.length-(t||1)].range[0],f[f.length-1].range[1]]),r=this.performAction.call(w,h,j,i,this.yy,q[1],e,f),"undefined"!=typeof r)return r;t&&(d=d.slice(0,-1*t*2),e=e.slice(0,-1*t),f=f.slice(0,-1*t)),d.push(this.productions_[q[1]][0]),e.push(w.$),f.push(w._$),u=g[d[d.length-2]][d[d.length-1]],d.push(u);break;case 3:return!0}}return!0}},c=function(){var a={EOF:1,parseError:function(a,b){if(!this.yy.parser)throw new Error(a);this.yy.parser.parseError(a,b)},setInput:function(a){return this._input=a,this._more=this._less=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var a=this._input[0];this.yytext+=a,this.yyleng++,this.offset++,this.match+=a,this.matched+=a;var b=a.match(/(?:\r\n?|\n).*/g);return b?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),a},unput:function(a){var b=a.length,c=a.split(/(?:\r\n?|\n)/g);this._input=a+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-b-1),this.offset-=b;var d=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),c.length-1&&(this.yylineno-=c.length-1);var e=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:c?(c.length===d.length?this.yylloc.first_column:0)+d[d.length-c.length].length-c[0].length:this.yylloc.first_column-b},this.options.ranges&&(this.yylloc.range=[e[0],e[0]+this.yyleng-b]),this},more:function(){return this._more=!0,this},less:function(a){this.unput(this.match.slice(a))},pastInput:function(){var a=this.matched.substr(0,this.matched.length-this.match.length);return(a.length>20?"...":"")+a.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var a=this.match;return a.length<20&&(a+=this._input.substr(0,20-a.length)),(a.substr(0,20)+(a.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var a=this.pastInput(),b=new Array(a.length+1).join("-");return a+this.upcomingInput()+"\n"+b+"^"},next:function(){if(this.done)return this.EOF;this._input||(this.done=!0);var a,b,c,d,e;this._more||(this.yytext="",this.match="");for(var f=this._currentRules(),g=0;gb[0].length)||(b=c,d=g,this.options.flex));g++);return b?(e=b[0].match(/(?:\r\n?|\n).*/g),e&&(this.yylineno+=e.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:e?e[e.length-1].length-e[e.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+b[0].length},this.yytext+=b[0],this.match+=b[0],this.matches=b,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._input=this._input.slice(b[0].length),this.matched+=b[0],a=this.performAction.call(this,this.yy,this,f[d],this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),a?a:void 0):""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){var a=this.next();return"undefined"!=typeof a?a:this.lex()},begin:function(a){this.conditionStack.push(a)},popState:function(){return this.conditionStack.pop()},_currentRules:function(){return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules},topState:function(){return this.conditionStack[this.conditionStack.length-2]},pushState:function(a){this.begin(a)}};return a.options={},a.performAction=function(a,b,c,d){function e(a,c){return b.yytext=b.yytext.substr(a,b.yyleng-c)}switch(c){case 0:if("\\\\"===b.yytext.slice(-2)?(e(0,1),this.begin("mu")):"\\"===b.yytext.slice(-1)?(e(0,1),this.begin("emu")):this.begin("mu"),b.yytext)return 12;break;case 1:return 12;case 2:return this.popState(),12;case 3:return b.yytext=b.yytext.substr(5,b.yyleng-9),this.popState(),15;case 4:return 12;case 5:return e(0,4),this.popState(),13;case 6:return 45;case 7:return 46;case 8:return 16;case 9:return this.popState(),this.begin("raw"),18;case 10:return 34;case 11:return 24;case 12:return 29;case 13:return this.popState(),28;case 14:return this.popState(),28;case 15:return 26;case 16:return 26;case 17:return 32;case 18:return 31;case 19:this.popState(),this.begin("com");break;case 20:return e(3,5),this.popState(),13;case 21:return 31;case 22:return 51;case 23:return 50;case 24:return 50;case 25:return 54;case 26:break;case 27:return this.popState(),33;case 28:return this.popState(),25;case 29:return b.yytext=e(1,2).replace(/\\"/g,'"'),42;case 30:return b.yytext=e(1,2).replace(/\\'/g,"'"),42;case 31:return 52;case 32:return 44;case 33:return 44;case 34:return 43;case 35:return 50;case 36:return b.yytext=e(1,2),50;case 37:return"INVALID";case 38:return 5}},a.rules=[/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|\\\{\{|\\\\\{\{|$)))/,/^(?:\{\{\{\{\/[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.])\}\}\}\})/,/^(?:[^\x00]*?(?=(\{\{\{\{\/)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\()/,/^(?:\))/,/^(?:\{\{\{\{)/,/^(?:\}\}\}\})/,/^(?:\{\{(~)?>)/,/^(?:\{\{(~)?#)/,/^(?:\{\{(~)?\/)/,/^(?:\{\{(~)?\^\s*(~)?\}\})/,/^(?:\{\{(~)?\s*else\s*(~)?\}\})/,/^(?:\{\{(~)?\^)/,/^(?:\{\{(~)?\s*else\b)/,/^(?:\{\{(~)?\{)/,/^(?:\{\{(~)?&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{(~)?)/,/^(?:=)/,/^(?:\.\.)/,/^(?:\.(?=([=~}\s\/.)])))/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}(~)?\}\})/,/^(?:(~)?\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=([~}\s)])))/,/^(?:false(?=([~}\s)])))/,/^(?:-?[0-9]+(?:\.[0-9]+)?(?=([~}\s)])))/,/^(?:([^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=([=~}\s\/.)]))))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/],a.conditions={mu:{rules:[6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],inclusive:!1},emu:{rules:[2],inclusive:!1},com:{rules:[5],inclusive:!1},raw:{rules:[3,4],inclusive:!1},INITIAL:{rules:[0,1,38],inclusive:!0}},a}();return b.lexer=c,a.prototype=b,b.Parser=a,new a}();return a=b}(),i=function(a){"use strict";function b(a,b){return{left:"~"===a.charAt(2),right:"~"===b.charAt(b.length-3)}}function c(a,b,c,d,i,k){if(a.sexpr.id.original!==d.path.original)throw new j(a.sexpr.id.original+" doesn't match "+d.path.original,a);var l=c&&c.program,m={left:a.strip.left,right:d.strip.right,openStandalone:f(b.statements),closeStandalone:e((l||b).statements)};if(a.strip.right&&g(b.statements,null,!0),l){var n=c.strip;n.left&&h(b.statements,null,!0),n.right&&g(l.statements,null,!0),d.strip.left&&h(l.statements,null,!0),e(b.statements)&&f(l.statements)&&(h(b.statements),g(l.statements))}else d.strip.left&&h(b.statements,null,!0);return i?new this.BlockNode(a,l,b,m,k):new this.BlockNode(a,b,l,m,k)}function d(a,b){for(var c=0,d=a.length;d>c;c++){var i=a[c],j=i.strip;if(j){var k=e(a,c,b,"partial"===i.type),l=f(a,c,b),m=j.openStandalone&&k,n=j.closeStandalone&&l,o=j.inlineStandalone&&k&&l;j.right&&g(a,c,!0),j.left&&h(a,c,!0),o&&(g(a,c),h(a,c)&&"partial"===i.type&&(i.indent=/([ \t]+$)/.exec(a[c-1].original)?RegExp.$1:"")),m&&(g((i.program||i.inverse).statements),h(a,c)),n&&(g(a,c),h((i.inverse||i.program).statements))}}return a}function e(a,b,c){void 0===b&&(b=a.length);var d=a[b-1],e=a[b-2];return d?"content"===d.type?(e||!c?/\r?\n\s*?$/:/(^|\r?\n)\s*?$/).test(d.original):void 0:c}function f(a,b,c){void 0===b&&(b=-1);var d=a[b+1],e=a[b+2];return d?"content"===d.type?(e||!c?/^\s*?\r?\n/:/^\s*?(\r?\n|$)/).test(d.original):void 0:c}function g(a,b,c){var d=a[null==b?0:b+1];if(d&&"content"===d.type&&(c||!d.rightStripped)){var e=d.string;d.string=d.string.replace(c?/^\s+/:/^[ \t]*\r?\n?/,""),d.rightStripped=d.string!==e}}function h(a,b,c){var d=a[null==b?a.length-1:b-1];if(d&&"content"===d.type&&(c||!d.leftStripped)){var e=d.string;return d.string=d.string.replace(c?/\s+$/:/[ \t]+$/,""),d.leftStripped=d.string!==e,d.leftStripped}}var i={},j=a;return i.stripFlags=b,i.prepareBlock=c,i.prepareProgram=d,i}(c),j=function(a,b,c,d){"use strict";function e(a){return a.constructor===h.ProgramNode?a:(g.yy=k,g.parse(a))}var f={},g=a,h=b,i=c,j=d.extend;f.parser=g;var k={};return j(k,i,h),f.parse=e,f}(h,g,i,b),k=function(a,b){"use strict";function c(){}function d(a,b,c){if(null==a||"string"!=typeof a&&a.constructor!==c.AST.ProgramNode)throw new h("You must pass a string or Handlebars AST to Handlebars.precompile. You passed "+a);b=b||{},"data"in b||(b.data=!0),b.compat&&(b.useDepths=!0);var d=c.parse(a),e=(new c.Compiler).compile(d,b);return(new c.JavaScriptCompiler).compile(e,b)}function e(a,b,c){function d(){var d=c.parse(a),e=(new c.Compiler).compile(d,b),f=(new c.JavaScriptCompiler).compile(e,b,void 0,!0);return c.template(f)}if(null==a||"string"!=typeof a&&a.constructor!==c.AST.ProgramNode)throw new h("You must pass a string or Handlebars AST to Handlebars.compile. You passed "+a);b=b||{},"data"in b||(b.data=!0),b.compat&&(b.useDepths=!0);var e,f=function(a,b){return e||(e=d()),e.call(this,a,b)};return f._setup=function(a){return e||(e=d()),e._setup(a)},f._child=function(a,b,c){return e||(e=d()),e._child(a,b,c)},f}function f(a,b){if(a===b)return!0;if(i(a)&&i(b)&&a.length===b.length){for(var c=0;cc;c++){var d=this.opcodes[c],e=a.opcodes[c];if(d.opcode!==e.opcode||!f(d.args,e.args))return!1}for(b=this.children.length,c=0;b>c;c++)if(!this.children[c].equals(a.children[c]))return!1;return!0},guid:0,compile:function(a,b){this.opcodes=[],this.children=[],this.depths={list:[]},this.options=b,this.stringParams=b.stringParams,this.trackIds=b.trackIds;var c=this.options.knownHelpers;if(this.options.knownHelpers={helperMissing:!0,blockHelperMissing:!0,each:!0,"if":!0,unless:!0,"with":!0,log:!0,lookup:!0},c)for(var d in c)this.options.knownHelpers[d]=c[d];return this.accept(a)},accept:function(a){return this[a.type](a)},program:function(a){for(var b=a.statements,c=0,d=b.length;d>c;c++)this.accept(b[c]);return this.isSimple=1===d,this.depths.list=this.depths.list.sort(function(a,b){return a-b}),this},compileProgram:function(a){var b,c=(new this.compiler).compile(a,this.options),d=this.guid++; -this.usePartial=this.usePartial||c.usePartial,this.children[d]=c;for(var e=0,f=c.depths.list.length;f>e;e++)b=c.depths.list[e],2>b||this.addDepth(b-1);return d},block:function(a){var b=a.mustache,c=a.program,d=a.inverse;c&&(c=this.compileProgram(c)),d&&(d=this.compileProgram(d));var e=b.sexpr,f=this.classifySexpr(e);"helper"===f?this.helperSexpr(e,c,d):"simple"===f?(this.simpleSexpr(e),this.opcode("pushProgram",c),this.opcode("pushProgram",d),this.opcode("emptyHash"),this.opcode("blockValue",e.id.original)):(this.ambiguousSexpr(e,c,d),this.opcode("pushProgram",c),this.opcode("pushProgram",d),this.opcode("emptyHash"),this.opcode("ambiguousBlockValue")),this.opcode("append")},hash:function(a){var b,c,d=a.pairs;for(this.opcode("pushHash"),b=0,c=d.length;c>b;b++)this.pushParam(d[b][1]);for(;b--;)this.opcode("assignToHash",d[b][0]);this.opcode("popHash")},partial:function(a){var b=a.partialName;this.usePartial=!0,a.hash?this.accept(a.hash):this.opcode("push","undefined"),a.context?this.accept(a.context):(this.opcode("getContext",0),this.opcode("pushContext")),this.opcode("invokePartial",b.name,a.indent||""),this.opcode("append")},content:function(a){a.string&&this.opcode("appendContent",a.string)},mustache:function(a){this.sexpr(a.sexpr),a.escaped&&!this.options.noEscape?this.opcode("appendEscaped"):this.opcode("append")},ambiguousSexpr:function(a,b,c){var d=a.id,e=d.parts[0],f=null!=b||null!=c;this.opcode("getContext",d.depth),this.opcode("pushProgram",b),this.opcode("pushProgram",c),this.ID(d),this.opcode("invokeAmbiguous",e,f)},simpleSexpr:function(a){var b=a.id;"DATA"===b.type?this.DATA(b):b.parts.length?this.ID(b):(this.addDepth(b.depth),this.opcode("getContext",b.depth),this.opcode("pushContext")),this.opcode("resolvePossibleLambda")},helperSexpr:function(a,b,c){var d=this.setupFullMustacheParams(a,b,c),e=a.id,f=e.parts[0];if(this.options.knownHelpers[f])this.opcode("invokeKnownHelper",d.length,f);else{if(this.options.knownHelpersOnly)throw new h("You specified knownHelpersOnly, but used the unknown helper "+f,a);e.falsy=!0,this.ID(e),this.opcode("invokeHelper",d.length,e.original,e.isSimple)}},sexpr:function(a){var b=this.classifySexpr(a);"simple"===b?this.simpleSexpr(a):"helper"===b?this.helperSexpr(a):this.ambiguousSexpr(a)},ID:function(a){this.addDepth(a.depth),this.opcode("getContext",a.depth);var b=a.parts[0];b?this.opcode("lookupOnContext",a.parts,a.falsy,a.isScoped):this.opcode("pushContext")},DATA:function(a){this.options.data=!0,this.opcode("lookupData",a.id.depth,a.id.parts)},STRING:function(a){this.opcode("pushString",a.string)},NUMBER:function(a){this.opcode("pushLiteral",a.number)},BOOLEAN:function(a){this.opcode("pushLiteral",a.bool)},comment:function(){},opcode:function(a){this.opcodes.push({opcode:a,args:j.call(arguments,1)})},addDepth:function(a){0!==a&&(this.depths[a]||(this.depths[a]=!0,this.depths.list.push(a)))},classifySexpr:function(a){var b=a.isHelper,c=a.eligibleHelper,d=this.options;if(c&&!b){var e=a.id.parts[0];d.knownHelpers[e]?b=!0:d.knownHelpersOnly&&(c=!1)}return b?"helper":c?"ambiguous":"simple"},pushParams:function(a){for(var b=0,c=a.length;c>b;b++)this.pushParam(a[b])},pushParam:function(a){this.stringParams?(a.depth&&this.addDepth(a.depth),this.opcode("getContext",a.depth||0),this.opcode("pushStringParam",a.stringModeValue,a.type),"sexpr"===a.type&&this.sexpr(a)):(this.trackIds&&this.opcode("pushId",a.type,a.idName||a.stringModeValue),this.accept(a))},setupFullMustacheParams:function(a,b,c){var d=a.params;return this.pushParams(d),this.opcode("pushProgram",b),this.opcode("pushProgram",c),a.hash?this.hash(a.hash):this.opcode("emptyHash"),d}},g.precompile=d,g.compile=e,g}(c,b),l=function(a,b){"use strict";function c(a){this.value=a}function d(){}var e,f=a.COMPILER_REVISION,g=a.REVISION_CHANGES,h=b;d.prototype={nameLookup:function(a,b){return d.isValidJavaScriptVariableName(b)?a+"."+b:a+"['"+b+"']"},depthedLookup:function(a){return this.aliases.lookup="this.lookup",'lookup(depths, "'+a+'")'},compilerInfo:function(){var a=f,b=g[a];return[a,b]},appendToBuffer:function(a){return this.environment.isSimple?"return "+a+";":{appendToBuffer:!0,content:a,toString:function(){return"buffer += "+a+";"}}},initializeBuffer:function(){return this.quotedString("")},namespace:"Handlebars",compile:function(a,b,c,d){this.environment=a,this.options=b,this.stringParams=this.options.stringParams,this.trackIds=this.options.trackIds,this.precompile=!d,this.name=this.environment.name,this.isChild=!!c,this.context=c||{programs:[],environments:[]},this.preamble(),this.stackSlot=0,this.stackVars=[],this.aliases={},this.registers={list:[]},this.hashes=[],this.compileStack=[],this.inlineStack=[],this.compileChildren(a,b),this.useDepths=this.useDepths||a.depths.list.length||this.options.compat;var e,f,g,i=a.opcodes;for(f=0,g=i.length;g>f;f++)e=i[f],this[e.opcode].apply(this,e.args);if(this.pushSource(""),this.stackSlot||this.inlineStack.length||this.compileStack.length)throw new h("Compile completed with content left on stack");var j=this.createFunctionContext(d);if(this.isChild)return j;var k={compiler:this.compilerInfo(),main:j},l=this.context.programs;for(f=0,g=l.length;g>f;f++)l[f]&&(k[f]=l[f]);return this.environment.usePartial&&(k.usePartial=!0),this.options.data&&(k.useData=!0),this.useDepths&&(k.useDepths=!0),this.options.compat&&(k.compat=!0),d||(k.compiler=JSON.stringify(k.compiler),k=this.objectLiteral(k)),k},preamble:function(){this.lastContext=0,this.source=[]},createFunctionContext:function(a){var b="",c=this.stackVars.concat(this.registers.list);c.length>0&&(b+=", "+c.join(", "));for(var d in this.aliases)this.aliases.hasOwnProperty(d)&&(b+=", "+d+"="+this.aliases[d]);var e=["depth0","helpers","partials","data"];this.useDepths&&e.push("depths");var f=this.mergeSource(b);return a?(e.push(f),Function.apply(this,e)):"function("+e.join(",")+") {\n "+f+"}"},mergeSource:function(a){for(var b,c,d="",e=!this.forceBuffer,f=0,g=this.source.length;g>f;f++){var h=this.source[f];h.appendToBuffer?b=b?b+"\n + "+h.content:h.content:(b&&(d?d+="buffer += "+b+";\n ":(c=!0,d=b+";\n "),b=void 0),d+=h+"\n ",this.environment.isSimple||(e=!1))}return e?(b||!d)&&(d+="return "+(b||'""')+";\n"):(a+=", buffer = "+(c?"":this.initializeBuffer()),d+=b?"return buffer + "+b+";\n":"return buffer;\n"),a&&(d="var "+a.substring(2)+(c?"":";\n ")+d),d},blockValue:function(a){this.aliases.blockHelperMissing="helpers.blockHelperMissing";var b=[this.contextName(0)];this.setupParams(a,0,b);var c=this.popStack();b.splice(1,0,c),this.push("blockHelperMissing.call("+b.join(", ")+")")},ambiguousBlockValue:function(){this.aliases.blockHelperMissing="helpers.blockHelperMissing";var a=[this.contextName(0)];this.setupParams("",0,a,!0),this.flushInline();var b=this.topStack();a.splice(1,0,b),this.pushSource("if (!"+this.lastHelper+") { "+b+" = blockHelperMissing.call("+a.join(", ")+"); }")},appendContent:function(a){this.pendingContent&&(a=this.pendingContent+a),this.pendingContent=a},append:function(){this.flushInline();var a=this.popStack();this.pushSource("if ("+a+" != null) { "+this.appendToBuffer(a)+" }"),this.environment.isSimple&&this.pushSource("else { "+this.appendToBuffer("''")+" }")},appendEscaped:function(){this.aliases.escapeExpression="this.escapeExpression",this.pushSource(this.appendToBuffer("escapeExpression("+this.popStack()+")"))},getContext:function(a){this.lastContext=a},pushContext:function(){this.pushStackLiteral(this.contextName(this.lastContext))},lookupOnContext:function(a,b,c){var d=0,e=a.length;for(c||!this.options.compat||this.lastContext?this.pushContext():this.push(this.depthedLookup(a[d++]));e>d;d++)this.replaceStack(function(c){var e=this.nameLookup(c,a[d],"context");return b?" && "+e:" != null ? "+e+" : "+c})},lookupData:function(a,b){a?this.pushStackLiteral("this.data(data, "+a+")"):this.pushStackLiteral("data");for(var c=b.length,d=0;c>d;d++)this.replaceStack(function(a){return" && "+this.nameLookup(a,b[d],"data")})},resolvePossibleLambda:function(){this.aliases.lambda="this.lambda",this.push("lambda("+this.popStack()+", "+this.contextName(0)+")")},pushStringParam:function(a,b){this.pushContext(),this.pushString(b),"sexpr"!==b&&("string"==typeof a?this.pushString(a):this.pushStackLiteral(a))},emptyHash:function(){this.pushStackLiteral("{}"),this.trackIds&&this.push("{}"),this.stringParams&&(this.push("{}"),this.push("{}"))},pushHash:function(){this.hash&&this.hashes.push(this.hash),this.hash={values:[],types:[],contexts:[],ids:[]}},popHash:function(){var a=this.hash;this.hash=this.hashes.pop(),this.trackIds&&this.push("{"+a.ids.join(",")+"}"),this.stringParams&&(this.push("{"+a.contexts.join(",")+"}"),this.push("{"+a.types.join(",")+"}")),this.push("{\n "+a.values.join(",\n ")+"\n }")},pushString:function(a){this.pushStackLiteral(this.quotedString(a))},push:function(a){return this.inlineStack.push(a),a},pushLiteral:function(a){this.pushStackLiteral(a)},pushProgram:function(a){null!=a?this.pushStackLiteral(this.programExpression(a)):this.pushStackLiteral(null)},invokeHelper:function(a,b,c){this.aliases.helperMissing="helpers.helperMissing";var d=this.popStack(),e=this.setupHelper(a,b),f=(c?e.name+" || ":"")+d+" || helperMissing";this.push("(("+f+").call("+e.callParams+"))")},invokeKnownHelper:function(a,b){var c=this.setupHelper(a,b);this.push(c.name+".call("+c.callParams+")")},invokeAmbiguous:function(a,b){this.aliases.functionType='"function"',this.aliases.helperMissing="helpers.helperMissing",this.useRegister("helper");var c=this.popStack();this.emptyHash();var d=this.setupHelper(0,a,b),e=this.lastHelper=this.nameLookup("helpers",a,"helper");this.push("((helper = (helper = "+e+" || "+c+") != null ? helper : helperMissing"+(d.paramsInit?"),("+d.paramsInit:"")+"),(typeof helper === functionType ? helper.call("+d.callParams+") : helper))")},invokePartial:function(a,b){var c=[this.nameLookup("partials",a,"partial"),"'"+b+"'","'"+a+"'",this.popStack(),this.popStack(),"helpers","partials"];this.options.data?c.push("data"):this.options.compat&&c.push("undefined"),this.options.compat&&c.push("depths"),this.push("this.invokePartial("+c.join(", ")+")")},assignToHash:function(a){var b,c,d,e=this.popStack();this.trackIds&&(d=this.popStack()),this.stringParams&&(c=this.popStack(),b=this.popStack());var f=this.hash;b&&f.contexts.push("'"+a+"': "+b),c&&f.types.push("'"+a+"': "+c),d&&f.ids.push("'"+a+"': "+d),f.values.push("'"+a+"': ("+e+")")},pushId:function(a,b){"ID"===a||"DATA"===a?this.pushString(b):"sexpr"===a?this.pushStackLiteral("true"):this.pushStackLiteral("null")},compiler:d,compileChildren:function(a,b){for(var c,d,e=a.children,f=0,g=e.length;g>f;f++){c=e[f],d=new this.compiler;var h=this.matchExistingProgram(c);null==h?(this.context.programs.push(""),h=this.context.programs.length,c.index=h,c.name="program"+h,this.context.programs[h]=d.compile(c,b,this.context,!this.precompile),this.context.environments[h]=c,this.useDepths=this.useDepths||d.useDepths):(c.index=h,c.name="program"+h)}},matchExistingProgram:function(a){for(var b=0,c=this.context.environments.length;c>b;b++){var d=this.context.environments[b];if(d&&d.equals(a))return b}},programExpression:function(a){var b=this.environment.children[a],c=(b.depths.list,this.useDepths),d=[b.index,"data"];return c&&d.push("depths"),"this.program("+d.join(", ")+")"},useRegister:function(a){this.registers[a]||(this.registers[a]=!0,this.registers.list.push(a))},pushStackLiteral:function(a){return this.push(new c(a))},pushSource:function(a){this.pendingContent&&(this.source.push(this.appendToBuffer(this.quotedString(this.pendingContent))),this.pendingContent=void 0),a&&this.source.push(a)},pushStack:function(a){this.flushInline();var b=this.incrStack();return this.pushSource(b+" = "+a+";"),this.compileStack.push(b),b},replaceStack:function(a){{var b,d,e,f="";this.isInline()}if(!this.isInline())throw new h("replaceStack on non-inline");var g=this.popStack(!0);if(g instanceof c)f=b=g.value,e=!0;else{d=!this.stackSlot;var i=d?this.incrStack():this.topStackName();f="("+this.push(i)+" = "+g+")",b=this.topStack()}var j=a.call(this,b);e||this.popStack(),d&&this.stackSlot--,this.push("("+f+j+")")},incrStack:function(){return this.stackSlot++,this.stackSlot>this.stackVars.length&&this.stackVars.push("stack"+this.stackSlot),this.topStackName()},topStackName:function(){return"stack"+this.stackSlot},flushInline:function(){var a=this.inlineStack;if(a.length){this.inlineStack=[];for(var b=0,d=a.length;d>b;b++){var e=a[b];e instanceof c?this.compileStack.push(e):this.pushStack(e)}}},isInline:function(){return this.inlineStack.length},popStack:function(a){var b=this.isInline(),d=(b?this.inlineStack:this.compileStack).pop();if(!a&&d instanceof c)return d.value;if(!b){if(!this.stackSlot)throw new h("Invalid stack pop");this.stackSlot--}return d},topStack:function(){var a=this.isInline()?this.inlineStack:this.compileStack,b=a[a.length-1];return b instanceof c?b.value:b},contextName:function(a){return this.useDepths&&a?"depths["+a+"]":"depth"+a},quotedString:function(a){return'"'+a.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029")+'"'},objectLiteral:function(a){var b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(this.quotedString(c)+":"+a[c]);return"{"+b.join(",")+"}"},setupHelper:function(a,b,c){var d=[],e=this.setupParams(b,a,d,c),f=this.nameLookup("helpers",b,"helper");return{params:d,paramsInit:e,name:f,callParams:[this.contextName(0)].concat(d).join(", ")}},setupOptions:function(a,b,c){var d,e,f,g={},h=[],i=[],j=[];g.name=this.quotedString(a),g.hash=this.popStack(),this.trackIds&&(g.hashIds=this.popStack()),this.stringParams&&(g.hashTypes=this.popStack(),g.hashContexts=this.popStack()),e=this.popStack(),f=this.popStack(),(f||e)&&(f||(f="this.noop"),e||(e="this.noop"),g.fn=f,g.inverse=e);for(var k=b;k--;)d=this.popStack(),c[k]=d,this.trackIds&&(j[k]=this.popStack()),this.stringParams&&(i[k]=this.popStack(),h[k]=this.popStack());return this.trackIds&&(g.ids="["+j.join(",")+"]"),this.stringParams&&(g.types="["+i.join(",")+"]",g.contexts="["+h.join(",")+"]"),this.options.data&&(g.data="data"),g},setupParams:function(a,b,c,d){var e=this.objectLiteral(this.setupOptions(a,b,c));return d?(this.useRegister("options"),c.push("options"),"options="+e):(c.push(e),"")}};for(var i="break else new var case finally return void catch for switch while continue function this with default if throw delete in try do instanceof typeof abstract enum int short boolean export interface static byte extends long super char final native synchronized class float package throws const goto private transient debugger implements protected volatile double import public let yield".split(" "),j=d.RESERVED_WORDS={},k=0,l=i.length;l>k;k++)j[i[k]]=!0;return d.isValidJavaScriptVariableName=function(a){return!d.RESERVED_WORDS[a]&&/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(a)},e=d}(d,c),m=function(a,b,c,d,e){"use strict";var f,g=a,h=b,i=c.parser,j=c.parse,k=d.Compiler,l=d.compile,m=d.precompile,n=e,o=g.create,p=function(){var a=o();return a.compile=function(b,c){return l(b,c,a)},a.precompile=function(b,c){return m(b,c,a)},a.AST=h,a.Compiler=k,a.JavaScriptCompiler=n,a.Parser=i,a.parse=j,a};return g=p(),g.create=p,g["default"]=g,f=g}(f,g,j,k,l);return m}); \ No newline at end of file diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.runtime.amd.js b/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.runtime.amd.js deleted file mode 100644 index d805f82e2a..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.runtime.amd.js +++ /dev/null @@ -1,637 +0,0 @@ -/*! - - handlebars v2.0.0 - -Copyright (C) 2011-2014 by Yehuda Katz - -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. - -@license -*/ - -define( - 'handlebars/safe-string',["exports"], - function(__exports__) { - - // Build out our basic SafeString type - function SafeString(string) { - this.string = string; - } - - SafeString.prototype.toString = function() { - return "" + this.string; - }; - - __exports__["default"] = SafeString; - }); -define( - 'handlebars/utils',["./safe-string","exports"], - function(__dependency1__, __exports__) { - - /*jshint -W004 */ - var SafeString = __dependency1__["default"]; - - var escape = { - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" - }; - - var badChars = /[&<>"'`]/g; - var possible = /[&<>"'`]/; - - function escapeChar(chr) { - return escape[chr]; - } - - function extend(obj /* , ...source */) { - for (var i = 1; i < arguments.length; i++) { - for (var key in arguments[i]) { - if (Object.prototype.hasOwnProperty.call(arguments[i], key)) { - obj[key] = arguments[i][key]; - } - } - } - - return obj; - } - - __exports__.extend = extend;var toString = Object.prototype.toString; - __exports__.toString = toString; - // Sourced from lodash - // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt - var isFunction = function(value) { - return typeof value === 'function'; - }; - // fallback for older versions of Chrome and Safari - /* istanbul ignore next */ - if (isFunction(/x/)) { - isFunction = function(value) { - return typeof value === 'function' && toString.call(value) === '[object Function]'; - }; - } - var isFunction; - __exports__.isFunction = isFunction; - /* istanbul ignore next */ - var isArray = Array.isArray || function(value) { - return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; - }; - __exports__.isArray = isArray; - - function escapeExpression(string) { - // don't escape SafeStrings, since they're already safe - if (string instanceof SafeString) { - return string.toString(); - } else if (string == null) { - return ""; - } else if (!string) { - return string + ''; - } - - // Force a string conversion as this will be done by the append regardless and - // the regex test will do this transparently behind the scenes, causing issues if - // an object's to string has escaped characters in it. - string = "" + string; - - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); - } - - __exports__.escapeExpression = escapeExpression;function isEmpty(value) { - if (!value && value !== 0) { - return true; - } else if (isArray(value) && value.length === 0) { - return true; - } else { - return false; - } - } - - __exports__.isEmpty = isEmpty;function appendContextPath(contextPath, id) { - return (contextPath ? contextPath + '.' : '') + id; - } - - __exports__.appendContextPath = appendContextPath; - }); -define( - 'handlebars/exception',["exports"], - function(__exports__) { - - - var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; - - function Exception(message, node) { - var line; - if (node && node.firstLine) { - line = node.firstLine; - - message += ' - ' + line + ':' + node.firstColumn; - } - - var tmp = Error.prototype.constructor.call(this, message); - - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (var idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } - - if (line) { - this.lineNumber = line; - this.column = node.firstColumn; - } - } - - Exception.prototype = new Error(); - - __exports__["default"] = Exception; - }); -define( - 'handlebars/base',["./utils","./exception","exports"], - function(__dependency1__, __dependency2__, __exports__) { - - var Utils = __dependency1__; - var Exception = __dependency2__["default"]; - - var VERSION = "2.0.0"; - __exports__.VERSION = VERSION;var COMPILER_REVISION = 6; - __exports__.COMPILER_REVISION = COMPILER_REVISION; - var REVISION_CHANGES = { - 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it - 2: '== 1.0.0-rc.3', - 3: '== 1.0.0-rc.4', - 4: '== 1.x.x', - 5: '== 2.0.0-alpha.x', - 6: '>= 2.0.0-beta.1' - }; - __exports__.REVISION_CHANGES = REVISION_CHANGES; - var isArray = Utils.isArray, - isFunction = Utils.isFunction, - toString = Utils.toString, - objectType = '[object Object]'; - - function HandlebarsEnvironment(helpers, partials) { - this.helpers = helpers || {}; - this.partials = partials || {}; - - registerDefaultHelpers(this); - } - - __exports__.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = { - constructor: HandlebarsEnvironment, - - logger: logger, - log: log, - - registerHelper: function(name, fn) { - if (toString.call(name) === objectType) { - if (fn) { throw new Exception('Arg not supported with multiple helpers'); } - Utils.extend(this.helpers, name); - } else { - this.helpers[name] = fn; - } - }, - unregisterHelper: function(name) { - delete this.helpers[name]; - }, - - registerPartial: function(name, partial) { - if (toString.call(name) === objectType) { - Utils.extend(this.partials, name); - } else { - this.partials[name] = partial; - } - }, - unregisterPartial: function(name) { - delete this.partials[name]; - } - }; - - function registerDefaultHelpers(instance) { - instance.registerHelper('helperMissing', function(/* [args, ]options */) { - if(arguments.length === 1) { - // A missing field in a {{foo}} constuct. - return undefined; - } else { - // Someone is actually trying to call something, blow up. - throw new Exception("Missing helper: '" + arguments[arguments.length-1].name + "'"); - } - }); - - instance.registerHelper('blockHelperMissing', function(context, options) { - var inverse = options.inverse, - fn = options.fn; - - if(context === true) { - return fn(this); - } else if(context === false || context == null) { - return inverse(this); - } else if (isArray(context)) { - if(context.length > 0) { - if (options.ids) { - options.ids = [options.name]; - } - - return instance.helpers.each(context, options); - } else { - return inverse(this); - } - } else { - if (options.data && options.ids) { - var data = createFrame(options.data); - data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name); - options = {data: data}; - } - - return fn(context, options); - } - }); - - instance.registerHelper('each', function(context, options) { - if (!options) { - throw new Exception('Must pass iterator to #each'); - } - - var fn = options.fn, inverse = options.inverse; - var i = 0, ret = "", data; - - var contextPath; - if (options.data && options.ids) { - contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.'; - } - - if (isFunction(context)) { context = context.call(this); } - - if (options.data) { - data = createFrame(options.data); - } - - if(context && typeof context === 'object') { - if (isArray(context)) { - for(var j = context.length; i":">",'"':""","'":"'","`":"`"},j=/[&<>"'`]/g,k=/[&<>"'`]/;b.extend=d;var l=Object.prototype.toString;b.toString=l;var m=function(a){return"function"==typeof a};m(/x/)&&(m=function(a){return"function"==typeof a&&"[object Function]"===l.call(a)});var m;b.isFunction=m;var n=Array.isArray||function(a){return a&&"object"==typeof a?"[object Array]"===l.call(a):!1};b.isArray=n,b.escapeExpression=e,b.isEmpty=f,b.appendContextPath=g}),define("handlebars/exception",["exports"],function(a){function b(a,b){var d;b&&b.firstLine&&(d=b.firstLine,a+=" - "+d+":"+b.firstColumn);for(var e=Error.prototype.constructor.call(this,a),f=0;f0?(c.ids&&(c.ids=[c.name]),a.helpers.each(b,c)):d(this);if(c.data&&c.ids){var g=q(c.data);g.contextPath=f.appendContextPath(c.data.contextPath,c.name),c={data:g}}return e(b,c)}),a.registerHelper("each",function(a,b){if(!b)throw new g("Must pass iterator to #each");var c,d,e=b.fn,h=b.inverse,i=0,j="";if(b.data&&b.ids&&(d=f.appendContextPath(b.data.contextPath,b.ids[0])+"."),l(a)&&(a=a.call(this)),b.data&&(c=q(b.data)),a&&"object"==typeof a)if(k(a))for(var m=a.length;m>i;i++)c&&(c.index=i,c.first=0===i,c.last=i===a.length-1,d&&(c.contextPath=d+i)),j+=e(a[i],{data:c});else for(var n in a)a.hasOwnProperty(n)&&(c&&(c.key=n,c.index=i,c.first=0===i,d&&(c.contextPath=d+n)),j+=e(a[n],{data:c}),i++);return 0===i&&(j=h(this)),j}),a.registerHelper("if",function(a,b){return l(a)&&(a=a.call(this)),!b.hash.includeZero&&!a||f.isEmpty(a)?b.inverse(this):b.fn(this)}),a.registerHelper("unless",function(b,c){return a.helpers["if"].call(this,b,{fn:c.inverse,inverse:c.fn,hash:c.hash})}),a.registerHelper("with",function(a,b){l(a)&&(a=a.call(this));var c=b.fn;if(f.isEmpty(a))return b.inverse(this);if(b.data&&b.ids){var d=q(b.data);d.contextPath=f.appendContextPath(b.data.contextPath,b.ids[0]),b={data:d}}return c(a,b)}),a.registerHelper("log",function(b,c){var d=c.data&&null!=c.data.level?parseInt(c.data.level,10):1;a.log(d,b)}),a.registerHelper("lookup",function(a,b){return a&&a[b]})}var f=a,g=b["default"],h="2.0.0";c.VERSION=h;var i=6;c.COMPILER_REVISION=i;var j={1:"<= 1.0.rc.2",2:"== 1.0.0-rc.3",3:"== 1.0.0-rc.4",4:"== 1.x.x",5:"== 2.0.0-alpha.x",6:">= 2.0.0-beta.1"};c.REVISION_CHANGES=j;var k=f.isArray,l=f.isFunction,m=f.toString,n="[object Object]";c.HandlebarsEnvironment=d,d.prototype={constructor:d,logger:o,log:p,registerHelper:function(a,b){if(m.call(a)===n){if(b)throw new g("Arg not supported with multiple helpers");f.extend(this.helpers,a)}else this.helpers[a]=b},unregisterHelper:function(a){delete this.helpers[a]},registerPartial:function(a,b){m.call(a)===n?f.extend(this.partials,a):this.partials[a]=b},unregisterPartial:function(a){delete this.partials[a]}};var o={methodMap:{0:"debug",1:"info",2:"warn",3:"error"},DEBUG:0,INFO:1,WARN:2,ERROR:3,level:3,log:function(a,b){if(o.level<=a){var c=o.methodMap[a];"undefined"!=typeof console&&console[c]&&console[c].call(console,b)}}};c.logger=o;var p=o.log;c.log=p;var q=function(a){var b=f.extend({},a);return b._parent=a,b};c.createFrame=q}),define("handlebars/runtime",["./utils","./exception","./base","exports"],function(a,b,c,d){function e(a){var b=a&&a[0]||1,c=m;if(b!==c){if(c>b){var d=n[c],e=n[b];throw new l("Template was precompiled with an older version of Handlebars than the current runtime. Please update your precompiler to a newer version ("+d+") or downgrade your runtime to an older version ("+e+").")}throw new l("Template was precompiled with a newer version of Handlebars than the current runtime. Please update your runtime to a newer version ("+a[1]+").")}}function f(a,b){if(!b)throw new l("No environment passed to template");if(!a||!a.main)throw new l("Unknown template object: "+typeof a);b.VM.checkRevision(a.compiler);var c=function(c,d,e,f,g,h,i,j,m){g&&(f=k.extend({},f,g));var n=b.VM.invokePartial.call(this,c,e,f,h,i,j,m);if(null==n&&b.compile){var o={helpers:h,partials:i,data:j,depths:m};i[e]=b.compile(c,{data:void 0!==j,compat:a.compat},b),n=i[e](f,o)}if(null!=n){if(d){for(var p=n.split("\n"),q=0,r=p.length;r>q&&(p[q]||q+1!==r);q++)p[q]=d+p[q];n=p.join("\n")}return n}throw new l("The partial "+e+" could not be compiled when running in runtime-only mode")},d={lookup:function(a,b){for(var c=a.length,d=0;c>d;d++)if(a[d]&&null!=a[d][b])return a[d][b]},lambda:function(a,b){return"function"==typeof a?a.call(b):a},escapeExpression:k.escapeExpression,invokePartial:c,fn:function(b){return a[b]},programs:[],program:function(a,b,c){var d=this.programs[a],e=this.fn(a);return b||c?d=g(this,a,e,b,c):d||(d=this.programs[a]=g(this,a,e)),d},data:function(a,b){for(;a&&b--;)a=a._parent;return a},merge:function(a,b){var c=a||b;return a&&b&&a!==b&&(c=k.extend({},b,a)),c},noop:b.VM.noop,compilerInfo:a.compiler},e=function(b,c){c=c||{};var f=c.data;e._setup(c),!c.partial&&a.useData&&(f=j(b,f));var g;return a.useDepths&&(g=c.depths?[b].concat(c.depths):[b]),a.main.call(d,b,d.helpers,d.partials,f,g)};return e.isTop=!0,e._setup=function(c){c.partial?(d.helpers=c.helpers,d.partials=c.partials):(d.helpers=d.merge(c.helpers,b.helpers),a.usePartial&&(d.partials=d.merge(c.partials,b.partials)))},e._child=function(b,c,e){if(a.useDepths&&!e)throw new l("must pass parent depths");return g(d,b,a[b],c,e)},e}function g(a,b,c,d,e){var f=function(b,f){return f=f||{},c.call(a,b,a.helpers,a.partials,f.data||d,e&&[b].concat(e))};return f.program=b,f.depth=e?e.length:0,f}function h(a,b,c,d,e,f,g){var h={partial:!0,helpers:d,partials:e,data:f,depths:g};if(void 0===a)throw new l("The partial "+b+" could not be found");return a instanceof Function?a(c,h):void 0}function i(){return""}function j(a,b){return b&&"root"in b||(b=b?o(b):{},b.root=a),b}var k=a,l=b["default"],m=c.COMPILER_REVISION,n=c.REVISION_CHANGES,o=c.createFrame;d.checkRevision=e,d.template=f,d.program=g,d.invokePartial=h,d.noop=i}),define("handlebars.runtime",["./handlebars/base","./handlebars/safe-string","./handlebars/exception","./handlebars/utils","./handlebars/runtime","exports"],function(a,b,c,d,e,f){var g=a,h=b["default"],i=c["default"],j=d,k=e,l=function(){var a=new g.HandlebarsEnvironment;return j.extend(a,g),a.SafeString=h,a.Exception=i,a.Utils=j,a.escapeExpression=j.escapeExpression,a.VM=k,a.template=function(b){return k.template(b,a)},a},m=l();m.create=l,m["default"]=m,f["default"]=m}); \ No newline at end of file diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.runtime.js b/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.runtime.js deleted file mode 100644 index 932fb7aea0..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/handlebars.runtime.js +++ /dev/null @@ -1,660 +0,0 @@ -/*! - - handlebars v2.0.0 - -Copyright (C) 2011-2014 by Yehuda Katz - -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. - -@license -*/ -/* exported Handlebars */ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - define([], factory); - } else if (typeof exports === 'object') { - module.exports = factory(); - } else { - root.Handlebars = root.Handlebars || factory(); - } -}(this, function () { -// handlebars/safe-string.js -var __module3__ = (function() { - "use strict"; - var __exports__; - // Build out our basic SafeString type - function SafeString(string) { - this.string = string; - } - - SafeString.prototype.toString = function() { - return "" + this.string; - }; - - __exports__ = SafeString; - return __exports__; -})(); - -// handlebars/utils.js -var __module2__ = (function(__dependency1__) { - "use strict"; - var __exports__ = {}; - /*jshint -W004 */ - var SafeString = __dependency1__; - - var escape = { - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" - }; - - var badChars = /[&<>"'`]/g; - var possible = /[&<>"'`]/; - - function escapeChar(chr) { - return escape[chr]; - } - - function extend(obj /* , ...source */) { - for (var i = 1; i < arguments.length; i++) { - for (var key in arguments[i]) { - if (Object.prototype.hasOwnProperty.call(arguments[i], key)) { - obj[key] = arguments[i][key]; - } - } - } - - return obj; - } - - __exports__.extend = extend;var toString = Object.prototype.toString; - __exports__.toString = toString; - // Sourced from lodash - // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt - var isFunction = function(value) { - return typeof value === 'function'; - }; - // fallback for older versions of Chrome and Safari - /* istanbul ignore next */ - if (isFunction(/x/)) { - isFunction = function(value) { - return typeof value === 'function' && toString.call(value) === '[object Function]'; - }; - } - var isFunction; - __exports__.isFunction = isFunction; - /* istanbul ignore next */ - var isArray = Array.isArray || function(value) { - return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; - }; - __exports__.isArray = isArray; - - function escapeExpression(string) { - // don't escape SafeStrings, since they're already safe - if (string instanceof SafeString) { - return string.toString(); - } else if (string == null) { - return ""; - } else if (!string) { - return string + ''; - } - - // Force a string conversion as this will be done by the append regardless and - // the regex test will do this transparently behind the scenes, causing issues if - // an object's to string has escaped characters in it. - string = "" + string; - - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); - } - - __exports__.escapeExpression = escapeExpression;function isEmpty(value) { - if (!value && value !== 0) { - return true; - } else if (isArray(value) && value.length === 0) { - return true; - } else { - return false; - } - } - - __exports__.isEmpty = isEmpty;function appendContextPath(contextPath, id) { - return (contextPath ? contextPath + '.' : '') + id; - } - - __exports__.appendContextPath = appendContextPath; - return __exports__; -})(__module3__); - -// handlebars/exception.js -var __module4__ = (function() { - "use strict"; - var __exports__; - - var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; - - function Exception(message, node) { - var line; - if (node && node.firstLine) { - line = node.firstLine; - - message += ' - ' + line + ':' + node.firstColumn; - } - - var tmp = Error.prototype.constructor.call(this, message); - - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (var idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } - - if (line) { - this.lineNumber = line; - this.column = node.firstColumn; - } - } - - Exception.prototype = new Error(); - - __exports__ = Exception; - return __exports__; -})(); - -// handlebars/base.js -var __module1__ = (function(__dependency1__, __dependency2__) { - "use strict"; - var __exports__ = {}; - var Utils = __dependency1__; - var Exception = __dependency2__; - - var VERSION = "2.0.0"; - __exports__.VERSION = VERSION;var COMPILER_REVISION = 6; - __exports__.COMPILER_REVISION = COMPILER_REVISION; - var REVISION_CHANGES = { - 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it - 2: '== 1.0.0-rc.3', - 3: '== 1.0.0-rc.4', - 4: '== 1.x.x', - 5: '== 2.0.0-alpha.x', - 6: '>= 2.0.0-beta.1' - }; - __exports__.REVISION_CHANGES = REVISION_CHANGES; - var isArray = Utils.isArray, - isFunction = Utils.isFunction, - toString = Utils.toString, - objectType = '[object Object]'; - - function HandlebarsEnvironment(helpers, partials) { - this.helpers = helpers || {}; - this.partials = partials || {}; - - registerDefaultHelpers(this); - } - - __exports__.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = { - constructor: HandlebarsEnvironment, - - logger: logger, - log: log, - - registerHelper: function(name, fn) { - if (toString.call(name) === objectType) { - if (fn) { throw new Exception('Arg not supported with multiple helpers'); } - Utils.extend(this.helpers, name); - } else { - this.helpers[name] = fn; - } - }, - unregisterHelper: function(name) { - delete this.helpers[name]; - }, - - registerPartial: function(name, partial) { - if (toString.call(name) === objectType) { - Utils.extend(this.partials, name); - } else { - this.partials[name] = partial; - } - }, - unregisterPartial: function(name) { - delete this.partials[name]; - } - }; - - function registerDefaultHelpers(instance) { - instance.registerHelper('helperMissing', function(/* [args, ]options */) { - if(arguments.length === 1) { - // A missing field in a {{foo}} constuct. - return undefined; - } else { - // Someone is actually trying to call something, blow up. - throw new Exception("Missing helper: '" + arguments[arguments.length-1].name + "'"); - } - }); - - instance.registerHelper('blockHelperMissing', function(context, options) { - var inverse = options.inverse, - fn = options.fn; - - if(context === true) { - return fn(this); - } else if(context === false || context == null) { - return inverse(this); - } else if (isArray(context)) { - if(context.length > 0) { - if (options.ids) { - options.ids = [options.name]; - } - - return instance.helpers.each(context, options); - } else { - return inverse(this); - } - } else { - if (options.data && options.ids) { - var data = createFrame(options.data); - data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name); - options = {data: data}; - } - - return fn(context, options); - } - }); - - instance.registerHelper('each', function(context, options) { - if (!options) { - throw new Exception('Must pass iterator to #each'); - } - - var fn = options.fn, inverse = options.inverse; - var i = 0, ret = "", data; - - var contextPath; - if (options.data && options.ids) { - contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.'; - } - - if (isFunction(context)) { context = context.call(this); } - - if (options.data) { - data = createFrame(options.data); - } - - if(context && typeof context === 'object') { - if (isArray(context)) { - for(var j = context.length; i":">",'"':""","'":"'","`":"`"},j=/[&<>"'`]/g,k=/[&<>"'`]/;g.extend=c;var l=Object.prototype.toString;g.toString=l;var m=function(a){return"function"==typeof a};m(/x/)&&(m=function(a){return"function"==typeof a&&"[object Function]"===l.call(a)});var m;g.isFunction=m;var n=Array.isArray||function(a){return a&&"object"==typeof a?"[object Array]"===l.call(a):!1};return g.isArray=n,g.escapeExpression=d,g.isEmpty=e,g.appendContextPath=f,g}(a),c=function(){"use strict";function a(a,b){var d;b&&b.firstLine&&(d=b.firstLine,a+=" - "+d+":"+b.firstColumn);for(var e=Error.prototype.constructor.call(this,a),f=0;f0?(c.ids&&(c.ids=[c.name]),a.helpers.each(b,c)):d(this);if(c.data&&c.ids){var g=q(c.data);g.contextPath=f.appendContextPath(c.data.contextPath,c.name),c={data:g}}return e(b,c)}),a.registerHelper("each",function(a,b){if(!b)throw new g("Must pass iterator to #each");var c,d,e=b.fn,h=b.inverse,i=0,j="";if(b.data&&b.ids&&(d=f.appendContextPath(b.data.contextPath,b.ids[0])+"."),l(a)&&(a=a.call(this)),b.data&&(c=q(b.data)),a&&"object"==typeof a)if(k(a))for(var m=a.length;m>i;i++)c&&(c.index=i,c.first=0===i,c.last=i===a.length-1,d&&(c.contextPath=d+i)),j+=e(a[i],{data:c});else for(var n in a)a.hasOwnProperty(n)&&(c&&(c.key=n,c.index=i,c.first=0===i,d&&(c.contextPath=d+n)),j+=e(a[n],{data:c}),i++);return 0===i&&(j=h(this)),j}),a.registerHelper("if",function(a,b){return l(a)&&(a=a.call(this)),!b.hash.includeZero&&!a||f.isEmpty(a)?b.inverse(this):b.fn(this)}),a.registerHelper("unless",function(b,c){return a.helpers["if"].call(this,b,{fn:c.inverse,inverse:c.fn,hash:c.hash})}),a.registerHelper("with",function(a,b){l(a)&&(a=a.call(this));var c=b.fn;if(f.isEmpty(a))return b.inverse(this);if(b.data&&b.ids){var d=q(b.data);d.contextPath=f.appendContextPath(b.data.contextPath,b.ids[0]),b={data:d}}return c(a,b)}),a.registerHelper("log",function(b,c){var d=c.data&&null!=c.data.level?parseInt(c.data.level,10):1;a.log(d,b)}),a.registerHelper("lookup",function(a,b){return a&&a[b]})}var e={},f=a,g=b,h="2.0.0";e.VERSION=h;var i=6;e.COMPILER_REVISION=i;var j={1:"<= 1.0.rc.2",2:"== 1.0.0-rc.3",3:"== 1.0.0-rc.4",4:"== 1.x.x",5:"== 2.0.0-alpha.x",6:">= 2.0.0-beta.1"};e.REVISION_CHANGES=j;var k=f.isArray,l=f.isFunction,m=f.toString,n="[object Object]";e.HandlebarsEnvironment=c,c.prototype={constructor:c,logger:o,log:p,registerHelper:function(a,b){if(m.call(a)===n){if(b)throw new g("Arg not supported with multiple helpers");f.extend(this.helpers,a)}else this.helpers[a]=b},unregisterHelper:function(a){delete this.helpers[a]},registerPartial:function(a,b){m.call(a)===n?f.extend(this.partials,a):this.partials[a]=b},unregisterPartial:function(a){delete this.partials[a]}};var o={methodMap:{0:"debug",1:"info",2:"warn",3:"error"},DEBUG:0,INFO:1,WARN:2,ERROR:3,level:3,log:function(a,b){if(o.level<=a){var c=o.methodMap[a];"undefined"!=typeof console&&console[c]&&console[c].call(console,b)}}};e.logger=o;var p=o.log;e.log=p;var q=function(a){var b=f.extend({},a);return b._parent=a,b};return e.createFrame=q,e}(b,c),e=function(a,b,c){"use strict";function d(a){var b=a&&a[0]||1,c=m;if(b!==c){if(c>b){var d=n[c],e=n[b];throw new l("Template was precompiled with an older version of Handlebars than the current runtime. Please update your precompiler to a newer version ("+d+") or downgrade your runtime to an older version ("+e+").")}throw new l("Template was precompiled with a newer version of Handlebars than the current runtime. Please update your runtime to a newer version ("+a[1]+").")}}function e(a,b){if(!b)throw new l("No environment passed to template");if(!a||!a.main)throw new l("Unknown template object: "+typeof a);b.VM.checkRevision(a.compiler);var c=function(c,d,e,f,g,h,i,j,m){g&&(f=k.extend({},f,g));var n=b.VM.invokePartial.call(this,c,e,f,h,i,j,m);if(null==n&&b.compile){var o={helpers:h,partials:i,data:j,depths:m};i[e]=b.compile(c,{data:void 0!==j,compat:a.compat},b),n=i[e](f,o)}if(null!=n){if(d){for(var p=n.split("\n"),q=0,r=p.length;r>q&&(p[q]||q+1!==r);q++)p[q]=d+p[q];n=p.join("\n")}return n}throw new l("The partial "+e+" could not be compiled when running in runtime-only mode")},d={lookup:function(a,b){for(var c=a.length,d=0;c>d;d++)if(a[d]&&null!=a[d][b])return a[d][b]},lambda:function(a,b){return"function"==typeof a?a.call(b):a},escapeExpression:k.escapeExpression,invokePartial:c,fn:function(b){return a[b]},programs:[],program:function(a,b,c){var d=this.programs[a],e=this.fn(a);return b||c?d=f(this,a,e,b,c):d||(d=this.programs[a]=f(this,a,e)),d},data:function(a,b){for(;a&&b--;)a=a._parent;return a},merge:function(a,b){var c=a||b;return a&&b&&a!==b&&(c=k.extend({},b,a)),c},noop:b.VM.noop,compilerInfo:a.compiler},e=function(b,c){c=c||{};var f=c.data;e._setup(c),!c.partial&&a.useData&&(f=i(b,f));var g;return a.useDepths&&(g=c.depths?[b].concat(c.depths):[b]),a.main.call(d,b,d.helpers,d.partials,f,g)};return e.isTop=!0,e._setup=function(c){c.partial?(d.helpers=c.helpers,d.partials=c.partials):(d.helpers=d.merge(c.helpers,b.helpers),a.usePartial&&(d.partials=d.merge(c.partials,b.partials)))},e._child=function(b,c,e){if(a.useDepths&&!e)throw new l("must pass parent depths");return f(d,b,a[b],c,e)},e}function f(a,b,c,d,e){var f=function(b,f){return f=f||{},c.call(a,b,a.helpers,a.partials,f.data||d,e&&[b].concat(e))};return f.program=b,f.depth=e?e.length:0,f}function g(a,b,c,d,e,f,g){var h={partial:!0,helpers:d,partials:e,data:f,depths:g};if(void 0===a)throw new l("The partial "+b+" could not be found");return a instanceof Function?a(c,h):void 0}function h(){return""}function i(a,b){return b&&"root"in b||(b=b?o(b):{},b.root=a),b}var j={},k=a,l=b,m=c.COMPILER_REVISION,n=c.REVISION_CHANGES,o=c.createFrame;return j.checkRevision=d,j.template=e,j.program=f,j.invokePartial=g,j.noop=h,j}(b,c,d),f=function(a,b,c,d,e){"use strict";var f,g=a,h=b,i=c,j=d,k=e,l=function(){var a=new g.HandlebarsEnvironment;return j.extend(a,g),a.SafeString=h,a.Exception=i,a.Utils=j,a.escapeExpression=j.escapeExpression,a.VM=k,a.template=function(b){return k.template(b,a)},a},m=l();return m.create=l,m["default"]=m,f=m}(d,a,c,b,e);return f}); \ No newline at end of file diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/lib/handlebars/source.rb b/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/lib/handlebars/source.rb deleted file mode 100644 index 4f3f8bfcee..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/handlebars/lib/handlebars/source.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Handlebars - module Source - def self.bundled_path - File.expand_path("../../../handlebars.js", __FILE__) - end - - def self.runtime_bundled_path - File.expand_path("../../../handlebars.runtime.js", __FILE__) - end - end -end diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/jquery/.bower.json b/src/main/resources/net/rptools/maptool/webapp/bower_components/jquery/.bower.json deleted file mode 100644 index e6324300d9..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/jquery/.bower.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "jquery", - "version": "2.1.1", - "main": "dist/jquery.js", - "license": "MIT", - "ignore": [ - "**/.*", - "build", - "speed", - "test", - "*.md", - "AUTHORS.txt", - "Gruntfile.js", - "package.json" - ], - "devDependencies": { - "sizzle": "1.10.19", - "requirejs": "2.1.10", - "qunit": "1.14.0", - "sinon": "1.8.1" - }, - "keywords": [ - "jquery", - "javascript", - "library" - ], - "homepage": "https://github.com/jquery/jquery", - "_release": "2.1.1", - "_resolution": { - "type": "version", - "tag": "2.1.1", - "commit": "4dec426aa2a6cbabb1b064319ba7c272d594a688" - }, - "_source": "git://github.com/jquery/jquery.git", - "_target": "~2.1.1", - "_originalSource": "jquery", - "_direct": true -} \ No newline at end of file diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/jquery/MIT-LICENSE.txt b/src/main/resources/net/rptools/maptool/webapp/bower_components/jquery/MIT-LICENSE.txt deleted file mode 100644 index cdd31b5c71..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/jquery/MIT-LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -Copyright 2014 jQuery Foundation and other contributors -http://jquery.com/ - -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. diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/jquery/bower.json b/src/main/resources/net/rptools/maptool/webapp/bower_components/jquery/bower.json deleted file mode 100644 index c66a7506b4..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/jquery/bower.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "jquery", - "version": "2.1.1", - "main": "dist/jquery.js", - "license": "MIT", - "ignore": [ - "**/.*", - "build", - "speed", - "test", - "*.md", - "AUTHORS.txt", - "Gruntfile.js", - "package.json" - ], - "devDependencies": { - "sizzle": "1.10.19", - "requirejs": "2.1.10", - "qunit": "1.14.0", - "sinon": "1.8.1" - }, - "keywords": [ - "jquery", - "javascript", - "library" - ] -} diff --git a/src/main/resources/net/rptools/maptool/webapp/bower_components/jquery/dist/jquery.js b/src/main/resources/net/rptools/maptool/webapp/bower_components/jquery/dist/jquery.js deleted file mode 100644 index 9f7b3d3895..0000000000 --- a/src/main/resources/net/rptools/maptool/webapp/bower_components/jquery/dist/jquery.js +++ /dev/null @@ -1,9190 +0,0 @@ -/*! - * jQuery JavaScript Library v2.1.1 - * http://jquery.com/ - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * - * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2014-05-01T17:11Z - */ - -(function( global, factory ) { - - if ( typeof module === "object" && typeof module.exports === "object" ) { - // For CommonJS and CommonJS-like environments where a proper window is present, - // execute the factory and get jQuery - // For environments that do not inherently posses a window with a document - // (such as Node.js), expose a jQuery-making factory as module.exports - // This accentuates the need for the creation of a real window - // e.g. var jQuery = require("jquery")(window); - // See ticket #14549 for more info - module.exports = global.document ? - factory( global, true ) : - function( w ) { - if ( !w.document ) { - throw new Error( "jQuery requires a window with a document" ); - } - return factory( w ); - }; - } else { - factory( global ); - } - -// Pass this if window is not defined yet -}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { - -// Can't do this because several apps including ASP.NET trace -// the stack via arguments.caller.callee and Firefox dies if -// you try to trace through "use strict" call chains. (#13335) -// Support: Firefox 18+ -// - -var arr = []; - -var slice = arr.slice; - -var concat = arr.concat; - -var push = arr.push; - -var indexOf = arr.indexOf; - -var class2type = {}; - -var toString = class2type.toString; - -var hasOwn = class2type.hasOwnProperty; - -var support = {}; - - - -var - // Use the correct document accordingly with window argument (sandbox) - document = window.document, - - version = "2.1.1", - - // Define a local copy of jQuery - jQuery = function( selector, context ) { - // The jQuery object is actually just the init constructor 'enhanced' - // Need init if jQuery is called (just allow error to be thrown if not included) - return new jQuery.fn.init( selector, context ); - }, - - // Support: Android<4.1 - // Make sure we trim BOM and NBSP - rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, - - // Matches dashed string for camelizing - rmsPrefix = /^-ms-/, - rdashAlpha = /-([\da-z])/gi, - - // Used by jQuery.camelCase as callback to replace() - fcamelCase = function( all, letter ) { - return letter.toUpperCase(); - }; - -jQuery.fn = jQuery.prototype = { - // The current version of jQuery being used - jquery: version, - - constructor: jQuery, - - // Start with an empty selector - selector: "", - - // The default length of a jQuery object is 0 - length: 0, - - toArray: function() { - return slice.call( this ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - return num != null ? - - // Return just the one element from the set - ( num < 0 ? this[ num + this.length ] : this[ num ] ) : - - // Return all the elements in a clean array - slice.call( this ); - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems ) { - - // Build a new jQuery matched element set - var ret = jQuery.merge( this.constructor(), elems ); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - ret.context = this.context; - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { - return callback.call( elem, i, elem ); - })); - }, - - slice: function() { - return this.pushStack( slice.apply( this, arguments ) ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - eq: function( i ) { - var len = this.length, - j = +i + ( i < 0 ? len : 0 ); - return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); - }, - - end: function() { - return this.prevObject || this.constructor(null); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: push, - sort: arr.sort, - splice: arr.splice -}; - -jQuery.extend = jQuery.fn.extend = function() { - var options, name, src, copy, copyIsArray, clone, - target = arguments[0] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - - // skip the boolean and the target - target = arguments[ i ] || {}; - i++; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) { - target = {}; - } - - // extend jQuery itself if only one argument is passed - if ( i === length ) { - target = this; - i--; - } - - for ( ; i < length; i++ ) { - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) { - // Extend the base object - for ( name in options ) { - src = target[ name ]; - copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { - if ( copyIsArray ) { - copyIsArray = false; - clone = src && jQuery.isArray(src) ? src : []; - - } else { - clone = src && jQuery.isPlainObject(src) ? src : {}; - } - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend({ - // Unique for each copy of jQuery on the page - expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), - - // Assume jQuery is ready without the ready module - isReady: true, - - error: function( msg ) { - throw new Error( msg ); - }, - - noop: function() {}, - - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). - isFunction: function( obj ) { - return jQuery.type(obj) === "function"; - }, - - isArray: Array.isArray, - - isWindow: function( obj ) { - return obj != null && obj === obj.window; - }, - - isNumeric: function( obj ) { - // parseFloat NaNs numeric-cast false positives (null|true|false|"") - // ...but misinterprets leading-number strings, particularly hex literals ("0x...") - // subtraction forces infinities to NaN - return !jQuery.isArray( obj ) && obj - parseFloat( obj ) >= 0; - }, - - isPlainObject: function( obj ) { - // Not plain objects: - // - Any object or value whose internal [[Class]] property is not "[object Object]" - // - DOM nodes - // - window - if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { - return false; - } - - if ( obj.constructor && - !hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) { - return false; - } - - // If the function hasn't returned already, we're confident that - // |obj| is a plain object, created by {} or constructed with new Object - return true; - }, - - isEmptyObject: function( obj ) { - var name; - for ( name in obj ) { - return false; - } - return true; - }, - - type: function( obj ) { - if ( obj == null ) { - return obj + ""; - } - // Support: Android < 4.0, iOS < 6 (functionish RegExp) - return typeof obj === "object" || typeof obj === "function" ? - class2type[ toString.call(obj) ] || "object" : - typeof obj; - }, - - // Evaluates a script in a global context - globalEval: function( code ) { - var script, - indirect = eval; - - code = jQuery.trim( code ); - - if ( code ) { - // If the code includes a valid, prologue position - // strict mode pragma, execute code by injecting a - // script tag into the document. - if ( code.indexOf("use strict") === 1 ) { - script = document.createElement("script"); - script.text = code; - document.head.appendChild( script ).parentNode.removeChild( script ); - } else { - // Otherwise, avoid the DOM node creation, insertion - // and removal by using an indirect global eval - indirect( code ); - } - } - }, - - // Convert dashed to camelCase; used by the css and data modules - // Microsoft forgot to hump their vendor prefix (#9572) - camelCase: function( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); - }, - - nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); - }, - - // args is for internal usage only - each: function( obj, callback, args ) { - var value, - i = 0, - length = obj.length, - isArray = isArraylike( obj ); - - if ( args ) { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } - } - } - - // A special, fast, case for the most common use of each - } else { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } - } - } - } - - return obj; - }, - - // Support: Android<4.1 - trim: function( text ) { - return text == null ? - "" : - ( text + "" ).replace( rtrim, "" ); - }, - - // results is for internal usage only - makeArray: function( arr, results ) { - var ret = results || []; - - if ( arr != null ) { - if ( isArraylike( Object(arr) ) ) { - jQuery.merge( ret, - typeof arr === "string" ? - [ arr ] : arr - ); - } else { - push.call( ret, arr ); - } - } - - return ret; - }, - - inArray: function( elem, arr, i ) { - return arr == null ? -1 : indexOf.call( arr, elem, i ); - }, - - merge: function( first, second ) { - var len = +second.length, - j = 0, - i = first.length; - - for ( ; j < len; j++ ) { - first[ i++ ] = second[ j ]; - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, invert ) { - var callbackInverse, - matches = [], - i = 0, - length = elems.length, - callbackExpect = !invert; - - // Go through the array, only saving the items - // that pass the validator function - for ( ; i < length; i++ ) { - callbackInverse = !callback( elems[ i ], i ); - if ( callbackInverse !== callbackExpect ) { - matches.push( elems[ i ] ); - } - } - - return matches; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var value, - i = 0, - length = elems.length, - isArray = isArraylike( elems ), - ret = []; - - // Go through the array, translating each of the items to their new values - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - - // Go through every key on the object, - } else { - for ( i in elems ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - } - - // Flatten any nested arrays - return concat.apply( [], ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // Bind a function to a context, optionally partially applying any - // arguments. - proxy: function( fn, context ) { - var tmp, args, proxy; - - if ( typeof context === "string" ) { - tmp = fn[ context ]; - context = fn; - fn = tmp; - } - - // Quick check to determine if target is callable, in the spec - // this throws a TypeError, but we will just return undefined. - if ( !jQuery.isFunction( fn ) ) { - return undefined; - } - - // Simulated bind - args = slice.call( arguments, 2 ); - proxy = function() { - return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); - }; - - // Set the guid of unique handler to the same of original handler, so it can be removed - proxy.guid = fn.guid = fn.guid || jQuery.guid++; - - return proxy; - }, - - now: Date.now, - - // jQuery.support is not used in Core but other projects attach their - // properties to it so it needs to exist. - support: support -}); - -// Populate the class2type map -jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -}); - -function isArraylike( obj ) { - var length = obj.length, - type = jQuery.type( obj ); - - if ( type === "function" || jQuery.isWindow( obj ) ) { - return false; - } - - if ( obj.nodeType === 1 && length ) { - return true; - } - - return type === "array" || length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj; -} -var Sizzle = -/*! - * Sizzle CSS Selector Engine v1.10.19 - * http://sizzlejs.com/ - * - * Copyright 2013 jQuery Foundation, Inc. and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2014-04-18 - */ -(function( window ) { - -var i, - support, - Expr, - getText, - isXML, - tokenize, - compile, - select, - outermostContext, - sortInput, - hasDuplicate, - - // Local document vars - setDocument, - document, - docElem, - documentIsHTML, - rbuggyQSA, - rbuggyMatches, - matches, - contains, - - // Instance-specific data - expando = "sizzle" + -(new Date()), - preferredDoc = window.document, - dirruns = 0, - done = 0, - classCache = createCache(), - tokenCache = createCache(), - compilerCache = createCache(), - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - } - return 0; - }, - - // General-purpose constants - strundefined = typeof undefined, - MAX_NEGATIVE = 1 << 31, - - // Instance methods - hasOwn = ({}).hasOwnProperty, - arr = [], - pop = arr.pop, - push_native = arr.push, - push = arr.push, - slice = arr.slice, - // Use a stripped-down indexOf if we can't use a native one - indexOf = arr.indexOf || function( elem ) { - var i = 0, - len = this.length; - for ( ; i < len; i++ ) { - if ( this[i] === elem ) { - return i; - } - } - return -1; - }, - - booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", - - // Regular expressions - - // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace - whitespace = "[\\x20\\t\\r\\n\\f]", - // http://www.w3.org/TR/css3-syntax/#characters - characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", - - // Loosely modeled on CSS identifier characters - // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors - // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier - identifier = characterEncoding.replace( "w", "w#" ), - - // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + characterEncoding + ")(?:" + whitespace + - // Operator (capture 2) - "*([*^$|!~]?=)" + whitespace + - // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" - "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + - "*\\]", - - pseudos = ":(" + characterEncoding + ")(?:\\((" + - // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: - // 1. quoted (capture 3; capture 4 or capture 5) - "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + - // 2. simple (capture 6) - "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + - // 3. anything else (capture 2) - ".*" + - ")\\)|)", - - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter - rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), - - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), - - rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), - - rpseudo = new RegExp( pseudos ), - ridentifier = new RegExp( "^" + identifier + "$" ), - - matchExpr = { - "ID": new RegExp( "^#(" + characterEncoding + ")" ), - "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), - "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), - "ATTR": new RegExp( "^" + attributes ), - "PSEUDO": new RegExp( "^" + pseudos ), - "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + - "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + - "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), - "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), - // For use in libraries implementing .is() - // We use this for POS matching in `select` - "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + - whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, - - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - - rnative = /^[^{]+\{\s*\[native \w/, - - // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - rsibling = /[+~]/, - rescape = /'|\\/g, - - // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), - funescape = function( _, escaped, escapedWhitespace ) { - var high = "0x" + escaped - 0x10000; - // NaN means non-codepoint - // Support: Firefox<24 - // Workaround erroneous numeric interpretation of +"0x" - return high !== high || escapedWhitespace ? - escaped : - high < 0 ? - // BMP codepoint - String.fromCharCode( high + 0x10000 ) : - // Supplemental Plane codepoint (surrogate pair) - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }; - -// Optimize for push.apply( _, NodeList ) -try { - push.apply( - (arr = slice.call( preferredDoc.childNodes )), - preferredDoc.childNodes - ); - // Support: Android<4.0 - // Detect silently failing push.apply - arr[ preferredDoc.childNodes.length ].nodeType; -} catch ( e ) { - push = { apply: arr.length ? - - // Leverage slice if possible - function( target, els ) { - push_native.apply( target, slice.call(els) ); - } : - - // Support: IE<9 - // Otherwise append directly - function( target, els ) { - var j = target.length, - i = 0; - // Can't trust NodeList.length - while ( (target[j++] = els[i++]) ) {} - target.length = j - 1; - } - }; -} - -function Sizzle( selector, context, results, seed ) { - var match, elem, m, nodeType, - // QSA vars - i, groups, old, nid, newContext, newSelector; - - if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { - setDocument( context ); - } - - context = context || document; - results = results || []; - - if ( !selector || typeof selector !== "string" ) { - return results; - } - - if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { - return []; - } - - if ( documentIsHTML && !seed ) { - - // Shortcuts - if ( (match = rquickExpr.exec( selector )) ) { - // Speed-up: Sizzle("#ID") - if ( (m = match[1]) ) { - if ( nodeType === 9 ) { - elem = context.getElementById( m ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document (jQuery #6963) - if ( elem && elem.parentNode ) { - // Handle the case where IE, Opera, and Webkit return items - // by name instead of ID - if ( elem.id === m ) { - results.push( elem ); - return results; - } - } else { - return results; - } - } else { - // Context is not a document - if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && - contains( context, elem ) && elem.id === m ) { - results.push( elem ); - return results; - } - } - - // Speed-up: Sizzle("TAG") - } else if ( match[2] ) { - push.apply( results, context.getElementsByTagName( selector ) ); - return results; - - // Speed-up: Sizzle(".CLASS") - } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { - push.apply( results, context.getElementsByClassName( m ) ); - return results; - } - } - - // QSA path - if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { - nid = old = expando; - newContext = context; - newSelector = nodeType === 9 && selector; - - // qSA works strangely on Element-rooted queries - // We can work around this by specifying an extra ID on the root - // and working up from there (Thanks to Andrew Dupont for the technique) - // IE 8 doesn't work on object elements - if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - groups = tokenize( selector ); - - if ( (old = context.getAttribute("id")) ) { - nid = old.replace( rescape, "\\$&" ); - } else { - context.setAttribute( "id", nid ); - } - nid = "[id='" + nid + "'] "; - - i = groups.length; - while ( i-- ) { - groups[i] = nid + toSelector( groups[i] ); - } - newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; - newSelector = groups.join(","); - } - - if ( newSelector ) { - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); - return results; - } catch(qsaError) { - } finally { - if ( !old ) { - context.removeAttribute("id"); - } - } - } - } - } - - // All others - return select( selector.replace( rtrim, "$1" ), context, results, seed ); -} - -/** - * Create key-value caches of limited size - * @returns {Function(string, Object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var keys = []; - - function cache( key, value ) { - // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) - if ( keys.push( key + " " ) > Expr.cacheLength ) { - // Only keep the most recent entries - delete cache[ keys.shift() ]; - } - return (cache[ key + " " ] = value); - } - return cache; -} - -/** - * Mark a function for special use by Sizzle - * @param {Function} fn The function to mark - */ -function markFunction( fn ) { - fn[ expando ] = true; - return fn; -} - -/** - * Support testing using an element - * @param {Function} fn Passed the created div and expects a boolean result - */ -function assert( fn ) { - var div = document.createElement("div"); - - try { - return !!fn( div ); - } catch (e) { - return false; - } finally { - // Remove from its parent by default - if ( div.parentNode ) { - div.parentNode.removeChild( div ); - } - // release memory in IE - div = null; - } -} - -/** - * Adds the same handler for all of the specified attrs - * @param {String} attrs Pipe-separated list of attributes - * @param {Function} handler The method that will be applied - */ -function addHandle( attrs, handler ) { - var arr = attrs.split("|"), - i = attrs.length; - - while ( i-- ) { - Expr.attrHandle[ arr[i] ] = handler; - } -} - -/** - * Checks document order of two siblings - * @param {Element} a - * @param {Element} b - * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b - */ -function siblingCheck( a, b ) { - var cur = b && a, - diff = cur && a.nodeType === 1 && b.nodeType === 1 && - ( ~b.sourceIndex || MAX_NEGATIVE ) - - ( ~a.sourceIndex || MAX_NEGATIVE ); - - // Use IE sourceIndex if available on both nodes - if ( diff ) { - return diff; - } - - // Check if b follows a - if ( cur ) { - while ( (cur = cur.nextSibling) ) { - if ( cur === b ) { - return -1; - } - } - } - - return a ? 1 : -1; -} - -/** - * Returns a function to use in pseudos for input types - * @param {String} type - */ -function createInputPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for buttons - * @param {String} type - */ -function createButtonPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for positionals - * @param {Function} fn - */ -function createPositionalPseudo( fn ) { - return markFunction(function( argument ) { - argument = +argument; - return markFunction(function( seed, matches ) { - var j, - matchIndexes = fn( [], seed.length, argument ), - i = matchIndexes.length; - - // Match elements found at the specified indexes - while ( i-- ) { - if ( seed[ (j = matchIndexes[i]) ] ) { - seed[j] = !(matches[j] = seed[j]); - } - } - }); - }); -} - -/** - * Checks a node for validity as a Sizzle context - * @param {Element|Object=} context - * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value - */ -function testContext( context ) { - return context && typeof context.getElementsByTagName !== strundefined && context; -} - -// Expose support vars for convenience -support = Sizzle.support = {}; - -/** - * Detects XML nodes - * @param {Element|Object} elem An element or a document - * @returns {Boolean} True iff elem is a non-HTML XML node - */ -isXML = Sizzle.isXML = function( elem ) { - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = elem && (elem.ownerDocument || elem).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; - -/** - * Sets document-related variables once based on the current document - * @param {Element|Object} [doc] An element or document object to use to set the document - * @returns {Object} Returns the current document - */ -setDocument = Sizzle.setDocument = function( node ) { - var hasCompare, - doc = node ? node.ownerDocument || node : preferredDoc, - parent = doc.defaultView; - - // If no document and documentElement is available, return - if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { - return document; - } - - // Set our document - document = doc; - docElem = doc.documentElement; - - // Support tests - documentIsHTML = !isXML( doc ); - - // Support: IE>8 - // If iframe document is assigned to "document" variable and if iframe has been reloaded, - // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 - // IE6-8 do not support the defaultView property so parent will be undefined - if ( parent && parent !== parent.top ) { - // IE11 does not have attachEvent, so all must suffer - if ( parent.addEventListener ) { - parent.addEventListener( "unload", function() { - setDocument(); - }, false ); - } else if ( parent.attachEvent ) { - parent.attachEvent( "onunload", function() { - setDocument(); - }); - } - } - - /* Attributes - ---------------------------------------------------------------------- */ - - // Support: IE<8 - // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans) - support.attributes = assert(function( div ) { - div.className = "i"; - return !div.getAttribute("className"); - }); - - /* getElement(s)By* - ---------------------------------------------------------------------- */ - - // Check if getElementsByTagName("*") returns only elements - support.getElementsByTagName = assert(function( div ) { - div.appendChild( doc.createComment("") ); - return !div.getElementsByTagName("*").length; - }); - - // Check if getElementsByClassName can be trusted - support.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) { - div.innerHTML = "
"; - - // Support: Safari<4 - // Catch class over-caching - div.firstChild.className = "i"; - // Support: Opera<10 - // Catch gEBCN failure to find non-leading classes - return div.getElementsByClassName("i").length === 2; - }); - - // Support: IE<10 - // Check if getElementById returns elements by name - // The broken getElementById methods don't pick up programatically-set names, - // so use a roundabout getElementsByName test - support.getById = assert(function( div ) { - docElem.appendChild( div ).id = expando; - return !doc.getElementsByName || !doc.getElementsByName( expando ).length; - }); - - // ID find and filter - if ( support.getById ) { - Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== strundefined && documentIsHTML ) { - var m = context.getElementById( id ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - return m && m.parentNode ? [ m ] : []; - } - }; - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - return elem.getAttribute("id") === attrId; - }; - }; - } else { - // Support: IE6/7 - // getElementById is not reliable as a find shortcut - delete Expr.find["ID"]; - - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); - return node && node.value === attrId; - }; - }; - } - - // Tag - Expr.find["TAG"] = support.getElementsByTagName ? - function( tag, context ) { - if ( typeof context.getElementsByTagName !== strundefined ) { - return context.getElementsByTagName( tag ); - } - } : - function( tag, context ) { - var elem, - tmp = [], - i = 0, - results = context.getElementsByTagName( tag ); - - // Filter out possible comments - if ( tag === "*" ) { - while ( (elem = results[i++]) ) { - if ( elem.nodeType === 1 ) { - tmp.push( elem ); - } - } - - return tmp; - } - return results; - }; - - // Class - Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { - if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) { - return context.getElementsByClassName( className ); - } - }; - - /* QSA/matchesSelector - ---------------------------------------------------------------------- */ - - // QSA and matchesSelector support - - // matchesSelector(:active) reports false when true (IE9/Opera 11.5) - rbuggyMatches = []; - - // qSa(:focus) reports false when true (Chrome 21) - // We allow this because of a bug in IE8/9 that throws an error - // whenever `document.activeElement` is accessed on an iframe - // So, we allow :focus to pass through QSA all the time to avoid the IE error - // See http://bugs.jquery.com/ticket/13378 - rbuggyQSA = []; - - if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) { - // Build QSA regex - // Regex strategy adopted from Diego Perini - assert(function( div ) { - // Select is set to empty string on purpose - // This is to test IE's treatment of not explicitly - // setting a boolean content attribute, - // since its presence should be enough - // http://bugs.jquery.com/ticket/12359 - div.innerHTML = ""; - - // Support: IE8, Opera 11-12.16 - // Nothing should be selected when empty strings follow ^= or $= or *= - // The test attribute must be unknown in Opera but "safe" for WinRT - // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section - if ( div.querySelectorAll("[msallowclip^='']").length ) { - rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); - } - - // Support: IE8 - // Boolean attributes and "value" are not treated correctly - if ( !div.querySelectorAll("[selected]").length ) { - rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); - } - - // Webkit/Opera - :checked should return selected option elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - // IE8 throws error here and will not see later tests - if ( !div.querySelectorAll(":checked").length ) { - rbuggyQSA.push(":checked"); - } - }); - - assert(function( div ) { - // Support: Windows 8 Native Apps - // The type and name attributes are restricted during .innerHTML assignment - var input = doc.createElement("input"); - input.setAttribute( "type", "hidden" ); - div.appendChild( input ).setAttribute( "name", "D" ); - - // Support: IE8 - // Enforce case-sensitivity of name attribute - if ( div.querySelectorAll("[name=d]").length ) { - rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); - } - - // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) - // IE8 throws error here and will not see later tests - if ( !div.querySelectorAll(":enabled").length ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Opera 10-11 does not throw on post-comma invalid pseudos - div.querySelectorAll("*,:x"); - rbuggyQSA.push(",.*:"); - }); - } - - if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || - docElem.webkitMatchesSelector || - docElem.mozMatchesSelector || - docElem.oMatchesSelector || - docElem.msMatchesSelector) )) ) { - - assert(function( div ) { - // Check to see if it's possible to do matchesSelector - // on a disconnected node (IE 9) - support.disconnectedMatch = matches.call( div, "div" ); - - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( div, "[s!='']:x" ); - rbuggyMatches.push( "!=", pseudos ); - }); - } - - rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); - rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); - - /* Contains - ---------------------------------------------------------------------- */ - hasCompare = rnative.test( docElem.compareDocumentPosition ); - - // Element contains another - // Purposefully does not implement inclusive descendent - // As in, an element does not contain itself - contains = hasCompare || rnative.test( docElem.contains ) ? - function( a, b ) { - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - /* Sorting - ---------------------------------------------------------------------- */ - - // Document order sorting - sortOrder = hasCompare ? - function( a, b ) { - - // Flag for duplicate removal - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - // Sort on method existence if only one input has compareDocumentPosition - var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; - if ( compare ) { - return compare; - } - - // Calculate position if both inputs belong to the same document - compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? - a.compareDocumentPosition( b ) : - - // Otherwise we know they are disconnected - 1; - - // Disconnected nodes - if ( compare & 1 || - (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { - - // Choose the first element that is related to our preferred document - if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { - return -1; - } - if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { - return 1; - } - - // Maintain original order - return sortInput ? - ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : - 0; - } - - return compare & 4 ? -1 : 1; - } : - function( a, b ) { - // Exit early if the nodes are identical - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - var cur, - i = 0, - aup = a.parentNode, - bup = b.parentNode, - ap = [ a ], - bp = [ b ]; - - // Parentless nodes are either documents or disconnected - if ( !aup || !bup ) { - return a === doc ? -1 : - b === doc ? 1 : - aup ? -1 : - bup ? 1 : - sortInput ? - ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : - 0; - - // If the nodes are siblings, we can do a quick check - } else if ( aup === bup ) { - return siblingCheck( a, b ); - } - - // Otherwise we need full lists of their ancestors for comparison - cur = a; - while ( (cur = cur.parentNode) ) { - ap.unshift( cur ); - } - cur = b; - while ( (cur = cur.parentNode) ) { - bp.unshift( cur ); - } - - // Walk down the tree looking for a discrepancy - while ( ap[i] === bp[i] ) { - i++; - } - - return i ? - // Do a sibling check if the nodes have a common ancestor - siblingCheck( ap[i], bp[i] ) : - - // Otherwise nodes in our document sort first - ap[i] === preferredDoc ? -1 : - bp[i] === preferredDoc ? 1 : - 0; - }; - - return doc; -}; - -Sizzle.matches = function( expr, elements ) { - return Sizzle( expr, null, null, elements ); -}; - -Sizzle.matchesSelector = function( elem, expr ) { - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - // Make sure that attribute selectors are quoted - expr = expr.replace( rattributeQuotes, "='$1']" ); - - if ( support.matchesSelector && documentIsHTML && - ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && - ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { - - try { - var ret = matches.call( elem, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || support.disconnectedMatch || - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11 ) { - return ret; - } - } catch(e) {} - } - - return Sizzle( expr, document, null, [ elem ] ).length > 0; -}; - -Sizzle.contains = function( context, elem ) { - // Set document vars if needed - if ( ( context.ownerDocument || context ) !== document ) { - setDocument( context ); - } - return contains( context, elem ); -}; - -Sizzle.attr = function( elem, name ) { - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - var fn = Expr.attrHandle[ name.toLowerCase() ], - // Don't get fooled by Object.prototype properties (jQuery #13807) - val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? - fn( elem, name, !documentIsHTML ) : - undefined; - - return val !== undefined ? - val : - support.attributes || !documentIsHTML ? - elem.getAttribute( name ) : - (val = elem.getAttributeNode(name)) && val.specified ? - val.value : - null; -}; - -Sizzle.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -/** - * Document sorting and removing duplicates - * @param {ArrayLike} results - */ -Sizzle.uniqueSort = function( results ) { - var elem, - duplicates = [], - j = 0, - i = 0; - - // Unless we *know* we can detect duplicates, assume their presence - hasDuplicate = !support.detectDuplicates; - sortInput = !support.sortStable && results.slice( 0 ); - results.sort( sortOrder ); - - if ( hasDuplicate ) { - while ( (elem = results[i++]) ) { - if ( elem === results[ i ] ) { - j = duplicates.push( i ); - } - } - while ( j-- ) { - results.splice( duplicates[ j ], 1 ); - } - } - - // Clear input after sorting to release objects - // See https://github.com/jquery/sizzle/pull/225 - sortInput = null; - - return results; -}; - -/** - * Utility function for retrieving the text value of an array of DOM nodes - * @param {Array|Element} elem - */ -getText = Sizzle.getText = function( elem ) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; - - if ( !nodeType ) { - // If no nodeType, this is expected to be an array - while ( (node = elem[i++]) ) { - // Do not traverse comment nodes - ret += getText( node ); - } - } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - // Use textContent for elements - // innerText usage removed for consistency of new lines (jQuery #11153) - if ( typeof elem.textContent === "string" ) { - return elem.textContent; - } else { - // Traverse its children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - ret += getText( elem ); - } - } - } else if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - // Do not include comment or processing instruction nodes - - return ret; -}; - -Expr = Sizzle.selectors = { - - // Can be adjusted by the user - cacheLength: 50, - - createPseudo: markFunction, - - match: matchExpr, - - attrHandle: {}, - - find: {}, - - relative: { - ">": { dir: "parentNode", first: true }, - " ": { dir: "parentNode" }, - "+": { dir: "previousSibling", first: true }, - "~": { dir: "previousSibling" } - }, - - preFilter: { - "ATTR": function( match ) { - match[1] = match[1].replace( runescape, funescape ); - - // Move the given value to match[3] whether quoted or unquoted - match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); - - if ( match[2] === "~=" ) { - match[3] = " " + match[3] + " "; - } - - return match.slice( 0, 4 ); - }, - - "CHILD": function( match ) { - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[1] = match[1].toLowerCase(); - - if ( match[1].slice( 0, 3 ) === "nth" ) { - // nth-* requires argument - if ( !match[3] ) { - Sizzle.error( match[0] ); - } - - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); - match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); - - // other types prohibit arguments - } else if ( match[3] ) { - Sizzle.error( match[0] ); - } - - return match; - }, - - "PSEUDO": function( match ) { - var excess, - unquoted = !match[6] && match[2]; - - if ( matchExpr["CHILD"].test( match[0] ) ) { - return null; - } - - // Accept quoted arguments as-is - if ( match[3] ) { - match[2] = match[4] || match[5] || ""; - - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - // Get excess from tokenize (recursively) - (excess = tokenize( unquoted, true )) && - // advance to the next closing parenthesis - (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { - - // excess is a negative index - match[0] = match[0].slice( 0, excess ); - match[2] = unquoted.slice( 0, excess ); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); - } - }, - - filter: { - - "TAG": function( nodeNameSelector ) { - var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); - return nodeNameSelector === "*" ? - function() { return true; } : - function( elem ) { - return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; - }; - }, - - "CLASS": function( className ) { - var pattern = classCache[ className + " " ]; - - return pattern || - (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && - classCache( className, function( elem ) { - return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" ); - }); - }, - - "ATTR": function( name, operator, check ) { - return function( elem ) { - var result = Sizzle.attr( elem, name ); - - if ( result == null ) { - return operator === "!="; - } - if ( !operator ) { - return true; - } - - result += ""; - - return operator === "=" ? result === check : - operator === "!=" ? result !== check : - operator === "^=" ? check && result.indexOf( check ) === 0 : - operator === "*=" ? check && result.indexOf( check ) > -1 : - operator === "$=" ? check && result.slice( -check.length ) === check : - operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : - operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : - false; - }; - }, - - "CHILD": function( type, what, argument, first, last ) { - var simple = type.slice( 0, 3 ) !== "nth", - forward = type.slice( -4 ) !== "last", - ofType = what === "of-type"; - - return first === 1 && last === 0 ? - - // Shortcut for :nth-*(n) - function( elem ) { - return !!elem.parentNode; - } : - - function( elem, context, xml ) { - var cache, outerCache, node, diff, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType; - - if ( parent ) { - - // :(first|last|only)-(child|of-type) - if ( simple ) { - while ( dir ) { - node = elem; - while ( (node = node[ dir ]) ) { - if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { - return false; - } - } - // Reverse direction for :only-* (if we haven't yet done so) - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } - - start = [ forward ? parent.firstChild : parent.lastChild ]; - - // non-xml :nth-child(...) stores cache data on `parent` - if ( forward && useCache ) { - // Seek `elem` from a previously-cached index - outerCache = parent[ expando ] || (parent[ expando ] = {}); - cache = outerCache[ type ] || []; - nodeIndex = cache[0] === dirruns && cache[1]; - diff = cache[0] === dirruns && cache[2]; - node = nodeIndex && parent.childNodes[ nodeIndex ]; - - while ( (node = ++nodeIndex && node && node[ dir ] || - - // Fallback to seeking `elem` from the start - (diff = nodeIndex = 0) || start.pop()) ) { - - // When found, cache indexes on `parent` and break - if ( node.nodeType === 1 && ++diff && node === elem ) { - outerCache[ type ] = [ dirruns, nodeIndex, diff ]; - break; - } - } - - // Use previously-cached element index if available - } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { - diff = cache[1]; - - // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) - } else { - // Use the same loop as above to seek `elem` from the start - while ( (node = ++nodeIndex && node && node[ dir ] || - (diff = nodeIndex = 0) || start.pop()) ) { - - if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { - // Cache the index of each encountered element - if ( useCache ) { - (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; - } - - if ( node === elem ) { - break; - } - } - } - } - - // Incorporate the offset, then check against cycle size - diff -= last; - return diff === first || ( diff % first === 0 && diff / first >= 0 ); - } - }; - }, - - "PSEUDO": function( pseudo, argument ) { - // pseudo-class names are case-insensitive - // http://www.w3.org/TR/selectors/#pseudo-classes - // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters - // Remember that setFilters inherits from pseudos - var args, - fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || - Sizzle.error( "unsupported pseudo: " + pseudo ); - - // The user may use createPseudo to indicate that - // arguments are needed to create the filter function - // just as Sizzle does - if ( fn[ expando ] ) { - return fn( argument ); - } - - // But maintain support for old signatures - if ( fn.length > 1 ) { - args = [ pseudo, pseudo, "", argument ]; - return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction(function( seed, matches ) { - var idx, - matched = fn( seed, argument ), - i = matched.length; - while ( i-- ) { - idx = indexOf.call( seed, matched[i] ); - seed[ idx ] = !( matches[ idx ] = matched[i] ); - } - }) : - function( elem ) { - return fn( elem, 0, args ); - }; - } - - return fn; - } - }, - - pseudos: { - // Potentially complex pseudos - "not": markFunction(function( selector ) { - // Trim the selector passed to compile - // to avoid treating leading and trailing - // spaces as combinators - var input = [], - results = [], - matcher = compile( selector.replace( rtrim, "$1" ) ); - - return matcher[ expando ] ? - markFunction(function( seed, matches, context, xml ) { - var elem, - unmatched = matcher( seed, null, xml, [] ), - i = seed.length; - - // Match elements unmatched by `matcher` - while ( i-- ) { - if ( (elem = unmatched[i]) ) { - seed[i] = !(matches[i] = elem); - } - } - }) : - function( elem, context, xml ) { - input[0] = elem; - matcher( input, null, xml, results ); - return !results.pop(); - }; - }), - - "has": markFunction(function( selector ) { - return function( elem ) { - return Sizzle( selector, elem ).length > 0; - }; - }), - - "contains": markFunction(function( text ) { - return function( elem ) { - return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; - }; - }), - - // "Whether an element is represented by a :lang() selector - // is based solely on the element's language value - // being equal to the identifier C, - // or beginning with the identifier C immediately followed by "-". - // The matching of C against the element's language value is performed case-insensitively. - // The identifier C does not have to be a valid language name." - // http://www.w3.org/TR/selectors/#lang-pseudo - "lang": markFunction( function( lang ) { - // lang value must be a valid identifier - if ( !ridentifier.test(lang || "") ) { - Sizzle.error( "unsupported lang: " + lang ); - } - lang = lang.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - var elemLang; - do { - if ( (elemLang = documentIsHTML ? - elem.lang : - elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { - - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; - } - } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); - return false; - }; - }), - - // Miscellaneous - "target": function( elem ) { - var hash = window.location && window.location.hash; - return hash && hash.slice( 1 ) === elem.id; - }, - - "root": function( elem ) { - return elem === docElem; - }, - - "focus": function( elem ) { - return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); - }, - - // Boolean properties - "enabled": function( elem ) { - return elem.disabled === false; - }, - - "disabled": function( elem ) { - return elem.disabled === true; - }, - - "checked": function( elem ) { - // In CSS3, :checked should return both checked and selected elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - var nodeName = elem.nodeName.toLowerCase(); - return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); - }, - - "selected": function( elem ) { - // Accessing this property makes selected-by-default - // options in Safari work properly - if ( elem.parentNode ) { - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - // Contents - "empty": function( elem ) { - // http://www.w3.org/TR/selectors/#empty-pseudo - // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), - // but not by others (comment: 8; processing instruction: 7; etc.) - // nodeType < 6 works because attributes (2) do not appear as children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeType < 6 ) { - return false; - } - } - return true; - }, - - "parent": function( elem ) { - return !Expr.pseudos["empty"]( elem ); - }, - - // Element/input types - "header": function( elem ) { - return rheader.test( elem.nodeName ); - }, - - "input": function( elem ) { - return rinputs.test( elem.nodeName ); - }, - - "button": function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === "button" || name === "button"; - }, - - "text": function( elem ) { - var attr; - return elem.nodeName.toLowerCase() === "input" && - elem.type === "text" && - - // Support: IE<8 - // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" - ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); - }, - - // Position-in-collection - "first": createPositionalPseudo(function() { - return [ 0 ]; - }), - - "last": createPositionalPseudo(function( matchIndexes, length ) { - return [ length - 1 ]; - }), - - "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { - return [ argument < 0 ? argument + length : argument ]; - }), - - "even": createPositionalPseudo(function( matchIndexes, length ) { - var i = 0; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "odd": createPositionalPseudo(function( matchIndexes, length ) { - var i = 1; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; --i >= 0; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; ++i < length; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }) - } -}; - -Expr.pseudos["nth"] = Expr.pseudos["eq"]; - -// Add button/input type pseudos -for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); -} -for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); -} - -// Easy API for creating new setFilters -function setFilters() {} -setFilters.prototype = Expr.filters = Expr.pseudos; -Expr.setFilters = new setFilters(); - -tokenize = Sizzle.tokenize = function( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while ( soFar ) { - - // Comma and first run - if ( !matched || (match = rcomma.exec( soFar )) ) { - if ( match ) { - // Don't consume trailing commas as valid - soFar = soFar.slice( match[0].length ) || soFar; - } - groups.push( (tokens = []) ); - } - - matched = false; - - // Combinators - if ( (match = rcombinators.exec( soFar )) ) { - matched = match.shift(); - tokens.push({ - value: matched, - // Cast descendant combinators to space - type: match[0].replace( rtrim, " " ) - }); - soFar = soFar.slice( matched.length ); - } - - // Filters - for ( type in Expr.filter ) { - if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || - (match = preFilters[ type ]( match ))) ) { - matched = match.shift(); - tokens.push({ - value: matched, - type: type, - matches: match - }); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - return parseOnly ? - soFar.length : - soFar ? - Sizzle.error( selector ) : - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -}; - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[i].value; - } - return selector; -} - -function addCombinator( matcher, combinator, base ) { - var dir = combinator.dir, - checkNonElements = base && dir === "parentNode", - doneName = done++; - - return combinator.first ? - // Check against closest ancestor/preceding element - function( elem, context, xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - return matcher( elem, context, xml ); - } - } - } : - - // Check against all ancestor/preceding elements - function( elem, context, xml ) { - var oldCache, outerCache, - newCache = [ dirruns, doneName ]; - - // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching - if ( xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - if ( matcher( elem, context, xml ) ) { - return true; - } - } - } - } else { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || (elem[ expando ] = {}); - if ( (oldCache = outerCache[ dir ]) && - oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { - - // Assign to newCache so results back-propagate to previous elements - return (newCache[ 2 ] = oldCache[ 2 ]); - } else { - // Reuse newcache so results back-propagate to previous elements - outerCache[ dir ] = newCache; - - // A match means we're done; a fail means we have to keep checking - if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { - return true; - } - } - } - } - } - }; -} - -function elementMatcher( matchers ) { - return matchers.length > 1 ? - function( elem, context, xml ) { - var i = matchers.length; - while ( i-- ) { - if ( !matchers[i]( elem, context, xml ) ) { - return false; - } - } - return true; - } : - matchers[0]; -} - -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - Sizzle( selector, contexts[i], results ); - } - return results; -} - -function condense( unmatched, map, filter, context, xml ) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; - - for ( ; i < len; i++ ) { - if ( (elem = unmatched[i]) ) { - if ( !filter || filter( elem, context, xml ) ) { - newUnmatched.push( elem ); - if ( mapped ) { - map.push( i ); - } - } - } - } - - return newUnmatched; -} - -function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { - postFilter = setMatcher( postFilter ); - } - if ( postFinder && !postFinder[ expando ] ) { - postFinder = setMatcher( postFinder, postSelector ); - } - return markFunction(function( seed, results, context, xml ) { - var temp, i, elem, - preMap = [], - postMap = [], - preexisting = results.length, - - // Get initial elements from seed or context - elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), - - // Prefilter to get matcher input, preserving a map for seed-results synchronization - matcherIn = preFilter && ( seed || !selector ) ? - condense( elems, preMap, preFilter, context, xml ) : - elems, - - matcherOut = matcher ? - // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, - postFinder || ( seed ? preFilter : preexisting || postFilter ) ? - - // ...intermediate processing is necessary - [] : - - // ...otherwise use results directly - results : - matcherIn; - - // Find primary matches - if ( matcher ) { - matcher( matcherIn, matcherOut, context, xml ); - } - - // Apply postFilter - if ( postFilter ) { - temp = condense( matcherOut, postMap ); - postFilter( temp, [], context, xml ); - - // Un-match failing elements by moving them back to matcherIn - i = temp.length; - while ( i-- ) { - if ( (elem = temp[i]) ) { - matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); - } - } - } - - if ( seed ) { - if ( postFinder || preFilter ) { - if ( postFinder ) { - // Get the final matcherOut by condensing this intermediate into postFinder contexts - temp = []; - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) ) { - // Restore matcherIn since elem is not yet a final match - temp.push( (matcherIn[i] = elem) ); - } - } - postFinder( null, (matcherOut = []), temp, xml ); - } - - // Move matched elements from seed to results to keep them synchronized - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) && - (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { - - seed[temp] = !(results[temp] = elem); - } - } - } - - // Add elements to results, through postFinder if defined - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice( preexisting, matcherOut.length ) : - matcherOut - ); - if ( postFinder ) { - postFinder( null, results, matcherOut, xml ); - } else { - push.apply( results, matcherOut ); - } - } - }); -} - -function matcherFromTokens( tokens ) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = Expr.relative[ tokens[0].type ], - implicitRelative = leadingRelative || Expr.relative[" "], - i = leadingRelative ? 1 : 0, - - // The foundational matcher ensures that elements are reachable from top-level context(s) - matchContext = addCombinator( function( elem ) { - return elem === checkContext; - }, implicitRelative, true ), - matchAnyContext = addCombinator( function( elem ) { - return indexOf.call( checkContext, elem ) > -1; - }, implicitRelative, true ), - matchers = [ function( elem, context, xml ) { - return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( - (checkContext = context).nodeType ? - matchContext( elem, context, xml ) : - matchAnyContext( elem, context, xml ) ); - } ]; - - for ( ; i < len; i++ ) { - if ( (matcher = Expr.relative[ tokens[i].type ]) ) { - matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; - } else { - matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); - - // Return special upon seeing a positional matcher - if ( matcher[ expando ] ) { - // Find the next relative operator (if any) for proper handling - j = ++i; - for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[j].type ] ) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher( matchers ), - i > 1 && toSelector( - // If the preceding token was a descendant combinator, insert an implicit any-element `*` - tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) - ).replace( rtrim, "$1" ), - matcher, - i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), - j < len && toSelector( tokens ) - ); - } - matchers.push( matcher ); - } - } - - return elementMatcher( matchers ); -} - -function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - var bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, outermost ) { - var elem, j, matcher, - matchedCount = 0, - i = "0", - unmatched = seed && [], - setMatched = [], - contextBackup = outermostContext, - // We must always have either seed elements or outermost context - elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), - // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), - len = elems.length; - - if ( outermost ) { - outermostContext = context !== document && context; - } - - // Add elements passing elementMatchers directly to results - // Keep `i` a string if there are no elements so `matchedCount` will be "00" below - // Support: IE<9, Safari - // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id - for ( ; i !== len && (elem = elems[i]) != null; i++ ) { - if ( byElement && elem ) { - j = 0; - while ( (matcher = elementMatchers[j++]) ) { - if ( matcher( elem, context, xml ) ) { - results.push( elem ); - break; - } - } - if ( outermost ) { - dirruns = dirrunsUnique; - } - } - - // Track unmatched elements for set filters - if ( bySet ) { - // They will have gone through all possible matchers - if ( (elem = !matcher && elem) ) { - matchedCount--; - } - - // Lengthen the array for every element, matched or not - if ( seed ) { - unmatched.push( elem ); - } - } - } - - // Apply set filters to unmatched elements - matchedCount += i; - if ( bySet && i !== matchedCount ) { - j = 0; - while ( (matcher = setMatchers[j++]) ) { - matcher( unmatched, setMatched, context, xml ); - } - - if ( seed ) { - // Reintegrate element matches to eliminate the need for sorting - if ( matchedCount > 0 ) { - while ( i-- ) { - if ( !(unmatched[i] || setMatched[i]) ) { - setMatched[i] = pop.call( results ); - } - } - } - - // Discard index placeholder values to get only actual matches - setMatched = condense( setMatched ); - } - - // Add matches to results - push.apply( results, setMatched ); - - // Seedless set matches succeeding multiple successful matchers stipulate sorting - if ( outermost && !seed && setMatched.length > 0 && - ( matchedCount + setMatchers.length ) > 1 ) { - - Sizzle.uniqueSort( results ); - } - } - - // Override manipulation of globals by nested matchers - if ( outermost ) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; - } - - return unmatched; - }; - - return bySet ? - markFunction( superMatcher ) : - superMatcher; -} - -compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[ selector + " " ]; - - if ( !cached ) { - // Generate a function of recursive functions that can be used to check each element - if ( !match ) { - match = tokenize( selector ); - } - i = match.length; - while ( i-- ) { - cached = matcherFromTokens( match[i] ); - if ( cached[ expando ] ) { - setMatchers.push( cached ); - } else { - elementMatchers.push( cached ); - } - } - - // Cache the compiled function - cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); - - // Save selector and tokenization - cached.selector = selector; - } - return cached; -}; - -/** - * A low-level selection function that works with Sizzle's compiled - * selector functions - * @param {String|Function} selector A selector or a pre-compiled - * selector function built with Sizzle.compile - * @param {Element} context - * @param {Array} [results] - * @param {Array} [seed] A set of elements to match against - */ -select = Sizzle.select = function( selector, context, results, seed ) { - var i, tokens, token, type, find, - compiled = typeof selector === "function" && selector, - match = !seed && tokenize( (selector = compiled.selector || selector) ); - - results = results || []; - - // Try to minimize operations if there is no seed and only one group - if ( match.length === 1 ) { - - // Take a shortcut and set the context if the root selector is an ID - tokens = match[0] = match[0].slice( 0 ); - if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && - support.getById && context.nodeType === 9 && documentIsHTML && - Expr.relative[ tokens[1].type ] ) { - - context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; - if ( !context ) { - return results; - - // Precompiled matchers will still verify ancestry, so step up a level - } else if ( compiled ) { - context = context.parentNode; - } - - selector = selector.slice( tokens.shift().value.length ); - } - - // Fetch a seed set for right-to-left matching - i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[i]; - - // Abort if we hit a combinator - if ( Expr.relative[ (type = token.type) ] ) { - break; - } - if ( (find = Expr.find[ type ]) ) { - // Search, expanding context for leading sibling combinators - if ( (seed = find( - token.matches[0].replace( runescape, funescape ), - rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context - )) ) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, seed ); - return results; - } - - break; - } - } - } - } - - // Compile and execute a filtering function if one is not provided - // Provide `match` to avoid retokenization if we modified the selector above - ( compiled || compile( selector, match ) )( - seed, - context, - !documentIsHTML, - results, - rsibling.test( selector ) && testContext( context.parentNode ) || context - ); - return results; -}; - -// One-time assignments - -// Sort stability -support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; - -// Support: Chrome<14 -// Always assume duplicates if they aren't passed to the comparison function -support.detectDuplicates = !!hasDuplicate; - -// Initialize against the default document -setDocument(); - -// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) -// Detached nodes confoundingly follow *each other* -support.sortDetached = assert(function( div1 ) { - // Should return 1, but returns 4 (following) - return div1.compareDocumentPosition( document.createElement("div") ) & 1; -}); - -// Support: IE<8 -// Prevent attribute/property "interpolation" -// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !assert(function( div ) { - div.innerHTML = ""; - return div.firstChild.getAttribute("href") === "#" ; -}) ) { - addHandle( "type|href|height|width", function( elem, name, isXML ) { - if ( !isXML ) { - return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); - } - }); -} - -// Support: IE<9 -// Use defaultValue in place of getAttribute("value") -if ( !support.attributes || !assert(function( div ) { - div.innerHTML = ""; - div.firstChild.setAttribute( "value", "" ); - return div.firstChild.getAttribute( "value" ) === ""; -}) ) { - addHandle( "value", function( elem, name, isXML ) { - if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { - return elem.defaultValue; - } - }); -} - -// Support: IE<9 -// Use getAttributeNode to fetch booleans when getAttribute lies -if ( !assert(function( div ) { - return div.getAttribute("disabled") == null; -}) ) { - addHandle( booleans, function( elem, name, isXML ) { - var val; - if ( !isXML ) { - return elem[ name ] === true ? name.toLowerCase() : - (val = elem.getAttributeNode( name )) && val.specified ? - val.value : - null; - } - }); -} - -return Sizzle; - -})( window ); - - - -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.pseudos; -jQuery.unique = Sizzle.uniqueSort; -jQuery.text = Sizzle.getText; -jQuery.isXMLDoc = Sizzle.isXML; -jQuery.contains = Sizzle.contains; - - - -var rneedsContext = jQuery.expr.match.needsContext; - -var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/); - - - -var risSimple = /^.[^:#\[\.,]*$/; - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, not ) { - if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep( elements, function( elem, i ) { - /* jshint -W018 */ - return !!qualifier.call( elem, i, elem ) !== not; - }); - - } - - if ( qualifier.nodeType ) { - return jQuery.grep( elements, function( elem ) { - return ( elem === qualifier ) !== not; - }); - - } - - if ( typeof qualifier === "string" ) { - if ( risSimple.test( qualifier ) ) { - return jQuery.filter( qualifier, elements, not ); - } - - qualifier = jQuery.filter( qualifier, elements ); - } - - return jQuery.grep( elements, function( elem ) { - return ( indexOf.call( qualifier, elem ) >= 0 ) !== not; - }); -} - -jQuery.filter = function( expr, elems, not ) { - var elem = elems[ 0 ]; - - if ( not ) { - expr = ":not(" + expr + ")"; - } - - return elems.length === 1 && elem.nodeType === 1 ? - jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : - jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { - return elem.nodeType === 1; - })); -}; - -jQuery.fn.extend({ - find: function( selector ) { - var i, - len = this.length, - ret = [], - self = this; - - if ( typeof selector !== "string" ) { - return this.pushStack( jQuery( selector ).filter(function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - }) ); - } - - for ( i = 0; i < len; i++ ) { - jQuery.find( selector, self[ i ], ret ); - } - - // Needed because $( selector, context ) becomes $( context ).find( selector ) - ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); - ret.selector = this.selector ? this.selector + " " + selector : selector; - return ret; - }, - filter: function( selector ) { - return this.pushStack( winnow(this, selector || [], false) ); - }, - not: function( selector ) { - return this.pushStack( winnow(this, selector || [], true) ); - }, - is: function( selector ) { - return !!winnow( - this, - - // If this is a positional/relative selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - typeof selector === "string" && rneedsContext.test( selector ) ? - jQuery( selector ) : - selector || [], - false - ).length; - } -}); - - -// Initialize a jQuery object - - -// A central reference to the root jQuery(document) -var rootjQuery, - - // A simple way to check for HTML strings - // Prioritize #id over to avoid XSS via location.hash (#9521) - // Strict HTML recognition (#11290: must start with <) - rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, - - init = jQuery.fn.init = function( selector, context ) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if ( !selector ) { - return this; - } - - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector[0] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) { - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = rquickExpr.exec( selector ); - } - - // Match html or make sure no context is specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) { - context = context instanceof jQuery ? context[0] : context; - - // scripts is true for back-compat - // Intentionally let the error be thrown if parseHTML is not present - jQuery.merge( this, jQuery.parseHTML( - match[1], - context && context.nodeType ? context.ownerDocument || context : document, - true - ) ); - - // HANDLE: $(html, props) - if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { - for ( match in context ) { - // Properties of context are called as methods if possible - if ( jQuery.isFunction( this[ match ] ) ) { - this[ match ]( context[ match ] ); - - // ...and otherwise set as attributes - } else { - this.attr( match, context[ match ] ); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById( match[2] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || rootjQuery ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { - return typeof rootjQuery.ready !== "undefined" ? - rootjQuery.ready( selector ) : - // Execute immediately if ready is not present - selector( jQuery ); - } - - if ( selector.selector !== undefined ) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray( selector, this ); - }; - -// Give the init function the jQuery prototype for later instantiation -init.prototype = jQuery.fn; - -// Initialize central reference -rootjQuery = jQuery( document ); - - -var rparentsprev = /^(?:parents|prev(?:Until|All))/, - // methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.extend({ - dir: function( elem, dir, until ) { - var matched = [], - truncate = until !== undefined; - - while ( (elem = elem[ dir ]) && elem.nodeType !== 9 ) { - if ( elem.nodeType === 1 ) { - if ( truncate && jQuery( elem ).is( until ) ) { - break; - } - matched.push( elem ); - } - } - return matched; - }, - - sibling: function( n, elem ) { - var matched = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - matched.push( n ); - } - } - - return matched; - } -}); - -jQuery.fn.extend({ - has: function( target ) { - var targets = jQuery( target, this ), - l = targets.length; - - return this.filter(function() { - var i = 0; - for ( ; i < l; i++ ) { - if ( jQuery.contains( this, targets[i] ) ) { - return true; - } - } - }); - }, - - closest: function( selectors, context ) { - var cur, - i = 0, - l = this.length, - matched = [], - pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? - jQuery( selectors, context || this.context ) : - 0; - - for ( ; i < l; i++ ) { - for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { - // Always skip document fragments - if ( cur.nodeType < 11 && (pos ? - pos.index(cur) > -1 : - - // Don't pass non-elements to Sizzle - cur.nodeType === 1 && - jQuery.find.matchesSelector(cur, selectors)) ) { - - matched.push( cur ); - break; - } - } - } - - return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched ); - }, - - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { - - // No argument, return index in parent - if ( !elem ) { - return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; - } - - // index in selector - if ( typeof elem === "string" ) { - return indexOf.call( jQuery( elem ), this[ 0 ] ); - } - - // Locate the position of the desired element - return indexOf.call( this, - - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[ 0 ] : elem - ); - }, - - add: function( selector, context ) { - return this.pushStack( - jQuery.unique( - jQuery.merge( this.get(), jQuery( selector, context ) ) - ) - ); - }, - - addBack: function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter(selector) - ); - } -}); - -function sibling( cur, dir ) { - while ( (cur = cur[dir]) && cur.nodeType !== 1 ) {} - return cur; -} - -jQuery.each({ - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return jQuery.dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, i, until ) { - return jQuery.dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return sibling( elem, "nextSibling" ); - }, - prev: function( elem ) { - return sibling( elem, "previousSibling" ); - }, - nextAll: function( elem ) { - return jQuery.dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return jQuery.dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, i, until ) { - return jQuery.dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, i, until ) { - return jQuery.dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); - }, - children: function( elem ) { - return jQuery.sibling( elem.firstChild ); - }, - contents: function( elem ) { - return elem.contentDocument || jQuery.merge( [], elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var matched = jQuery.map( this, fn, until ); - - if ( name.slice( -5 ) !== "Until" ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - matched = jQuery.filter( selector, matched ); - } - - if ( this.length > 1 ) { - // Remove duplicates - if ( !guaranteedUnique[ name ] ) { - jQuery.unique( matched ); - } - - // Reverse order for parents* and prev-derivatives - if ( rparentsprev.test( name ) ) { - matched.reverse(); - } - } - - return this.pushStack( matched ); - }; -}); -var rnotwhite = (/\S+/g); - - - -// String to Object options format cache -var optionsCache = {}; - -// Convert String-formatted options into Object-formatted ones and store in cache -function createOptions( options ) { - var object = optionsCache[ options ] = {}; - jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { - object[ flag ] = true; - }); - return object; -} - -/* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( options ) { - - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - ( optionsCache[ options ] || createOptions( options ) ) : - jQuery.extend( {}, options ); - - var // Last fire value (for non-forgettable lists) - memory, - // Flag to know if list was already fired - fired, - // Flag to know if list is currently firing - firing, - // First callback to fire (used internally by add and fireWith) - firingStart, - // End of the loop when firing - firingLength, - // Index of currently firing callback (modified by remove if needed) - firingIndex, - // Actual callback list - list = [], - // Stack of fire calls for repeatable lists - stack = !options.once && [], - // Fire callbacks - fire = function( data ) { - memory = options.memory && data; - fired = true; - firingIndex = firingStart || 0; - firingStart = 0; - firingLength = list.length; - firing = true; - for ( ; list && firingIndex < firingLength; firingIndex++ ) { - if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { - memory = false; // To prevent further calls using add - break; - } - } - firing = false; - if ( list ) { - if ( stack ) { - if ( stack.length ) { - fire( stack.shift() ); - } - } else if ( memory ) { - list = []; - } else { - self.disable(); - } - } - }, - // Actual Callbacks object - self = { - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - // First, we save the current length - var start = list.length; - (function add( args ) { - jQuery.each( args, function( _, arg ) { - var type = jQuery.type( arg ); - if ( type === "function" ) { - if ( !options.unique || !self.has( arg ) ) { - list.push( arg ); - } - } else if ( arg && arg.length && type !== "string" ) { - // Inspect recursively - add( arg ); - } - }); - })( arguments ); - // Do we need to add the callbacks to the - // current firing batch? - if ( firing ) { - firingLength = list.length; - // With memory, if we're not firing then - // we should call right away - } else if ( memory ) { - firingStart = start; - fire( memory ); - } - } - return this; - }, - // Remove a callback from the list - remove: function() { - if ( list ) { - jQuery.each( arguments, function( _, arg ) { - var index; - while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - // Handle firing indexes - if ( firing ) { - if ( index <= firingLength ) { - firingLength--; - } - if ( index <= firingIndex ) { - firingIndex--; - } - } - } - }); - } - return this; - }, - // Check if a given callback is in the list. - // If no argument is given, return whether or not list has callbacks attached. - has: function( fn ) { - return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); - }, - // Remove all callbacks from the list - empty: function() { - list = []; - firingLength = 0; - return this; - }, - // Have the list do nothing anymore - disable: function() { - list = stack = memory = undefined; - return this; - }, - // Is it disabled? - disabled: function() { - return !list; - }, - // Lock the list in its current state - lock: function() { - stack = undefined; - if ( !memory ) { - self.disable(); - } - return this; - }, - // Is it locked? - locked: function() { - return !stack; - }, - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - if ( list && ( !fired || stack ) ) { - args = args || []; - args = [ context, args.slice ? args.slice() : args ]; - if ( firing ) { - stack.push( args ); - } else { - fire( args ); - } - } - return this; - }, - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; - - return self; -}; - - -jQuery.extend({ - - Deferred: function( func ) { - var tuples = [ - // action, add listener, listener list, final state - [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], - [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], - [ "notify", "progress", jQuery.Callbacks("memory") ] - ], - state = "pending", - promise = { - state: function() { - return state; - }, - always: function() { - deferred.done( arguments ).fail( arguments ); - return this; - }, - then: function( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - return jQuery.Deferred(function( newDefer ) { - jQuery.each( tuples, function( i, tuple ) { - var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; - // deferred[ done | fail | progress ] for forwarding actions to newDefer - deferred[ tuple[1] ](function() { - var returned = fn && fn.apply( this, arguments ); - if ( returned && jQuery.isFunction( returned.promise ) ) { - returned.promise() - .done( newDefer.resolve ) - .fail( newDefer.reject ) - .progress( newDefer.notify ); - } else { - newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); - } - }); - }); - fns = null; - }).promise(); - }, - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - return obj != null ? jQuery.extend( obj, promise ) : promise; - } - }, - deferred = {}; - - // Keep pipe for back-compat - promise.pipe = promise.then; - - // Add list-specific methods - jQuery.each( tuples, function( i, tuple ) { - var list = tuple[ 2 ], - stateString = tuple[ 3 ]; - - // promise[ done | fail | progress ] = list.add - promise[ tuple[1] ] = list.add; - - // Handle state - if ( stateString ) { - list.add(function() { - // state = [ resolved | rejected ] - state = stateString; - - // [ reject_list | resolve_list ].disable; progress_list.lock - }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); - } - - // deferred[ resolve | reject | notify ] - deferred[ tuple[0] ] = function() { - deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); - return this; - }; - deferred[ tuple[0] + "With" ] = list.fireWith; - }); - - // Make the deferred a promise - promise.promise( deferred ); - - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function( subordinate /* , ..., subordinateN */ ) { - var i = 0, - resolveValues = slice.call( arguments ), - length = resolveValues.length, - - // the count of uncompleted subordinates - remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, - - // the master Deferred. If resolveValues consist of only a single Deferred, just use that. - deferred = remaining === 1 ? subordinate : jQuery.Deferred(), - - // Update function for both resolve and progress values - updateFunc = function( i, contexts, values ) { - return function( value ) { - contexts[ i ] = this; - values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; - if ( values === progressValues ) { - deferred.notifyWith( contexts, values ); - } else if ( !( --remaining ) ) { - deferred.resolveWith( contexts, values ); - } - }; - }, - - progressValues, progressContexts, resolveContexts; - - // add listeners to Deferred subordinates; treat others as resolved - if ( length > 1 ) { - progressValues = new Array( length ); - progressContexts = new Array( length ); - resolveContexts = new Array( length ); - for ( ; i < length; i++ ) { - if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { - resolveValues[ i ].promise() - .done( updateFunc( i, resolveContexts, resolveValues ) ) - .fail( deferred.reject ) - .progress( updateFunc( i, progressContexts, progressValues ) ); - } else { - --remaining; - } - } - } - - // if we're not waiting on anything, resolve the master - if ( !remaining ) { - deferred.resolveWith( resolveContexts, resolveValues ); - } - - return deferred.promise(); - } -}); - - -// The deferred used on DOM ready -var readyList; - -jQuery.fn.ready = function( fn ) { - // Add the callback - jQuery.ready.promise().done( fn ); - - return this; -}; - -jQuery.extend({ - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Hold (or release) the ready event - holdReady: function( hold ) { - if ( hold ) { - jQuery.readyWait++; - } else { - jQuery.ready( true ); - } - }, - - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - - // Trigger any bound ready events - if ( jQuery.fn.triggerHandler ) { - jQuery( document ).triggerHandler( "ready" ); - jQuery( document ).off( "ready" ); - } - } -}); - -/** - * The ready event handler and self cleanup method - */ -function completed() { - document.removeEventListener( "DOMContentLoaded", completed, false ); - window.removeEventListener( "load", completed, false ); - jQuery.ready(); -} - -jQuery.ready.promise = function( obj ) { - if ( !readyList ) { - - readyList = jQuery.Deferred(); - - // Catch cases where $(document).ready() is called after the browser event has already occurred. - // we once tried to use readyState "interactive" here, but it caused issues like the one - // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 - if ( document.readyState === "complete" ) { - // Handle it asynchronously to allow scripts the opportunity to delay ready - setTimeout( jQuery.ready ); - - } else { - - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed, false ); - } - } - return readyList.promise( obj ); -}; - -// Kick off the DOM ready check even if the user does not -jQuery.ready.promise(); - - - - -// Multifunctional method to get and set values of a collection -// The value/s can optionally be executed if it's a function -var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - len = elems.length, - bulk = key == null; - - // Sets many values - if ( jQuery.type( key ) === "object" ) { - chainable = true; - for ( i in key ) { - jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); - } - - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !jQuery.isFunction( value ) ) { - raw = true; - } - - if ( bulk ) { - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } - - if ( fn ) { - for ( ; i < len; i++ ) { - fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); - } - } - } - - return chainable ? - elems : - - // Gets - bulk ? - fn.call( elems ) : - len ? fn( elems[0], key ) : emptyGet; -}; - - -/** - * Determines whether an object can have data - */ -jQuery.acceptData = function( owner ) { - // Accepts only: - // - Node - // - Node.ELEMENT_NODE - // - Node.DOCUMENT_NODE - // - Object - // - Any - /* jshint -W018 */ - return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); -}; - - -function Data() { - // Support: Android < 4, - // Old WebKit does not have Object.preventExtensions/freeze method, - // return new empty object instead with no [[set]] accessor - Object.defineProperty( this.cache = {}, 0, { - get: function() { - return {}; - } - }); - - this.expando = jQuery.expando + Math.random(); -} - -Data.uid = 1; -Data.accepts = jQuery.acceptData; - -Data.prototype = { - key: function( owner ) { - // We can accept data for non-element nodes in modern browsers, - // but we should not, see #8335. - // Always return the key for a frozen object. - if ( !Data.accepts( owner ) ) { - return 0; - } - - var descriptor = {}, - // Check if the owner object already has a cache key - unlock = owner[ this.expando ]; - - // If not, create one - if ( !unlock ) { - unlock = Data.uid++; - - // Secure it in a non-enumerable, non-writable property - try { - descriptor[ this.expando ] = { value: unlock }; - Object.defineProperties( owner, descriptor ); - - // Support: Android < 4 - // Fallback to a less secure definition - } catch ( e ) { - descriptor[ this.expando ] = unlock; - jQuery.extend( owner, descriptor ); - } - } - - // Ensure the cache object - if ( !this.cache[ unlock ] ) { - this.cache[ unlock ] = {}; - } - - return unlock; - }, - set: function( owner, data, value ) { - var prop, - // There may be an unlock assigned to this node, - // if there is no entry for this "owner", create one inline - // and set the unlock as though an owner entry had always existed - unlock = this.key( owner ), - cache = this.cache[ unlock ]; - - // Handle: [ owner, key, value ] args - if ( typeof data === "string" ) { - cache[ data ] = value; - - // Handle: [ owner, { properties } ] args - } else { - // Fresh assignments by object are shallow copied - if ( jQuery.isEmptyObject( cache ) ) { - jQuery.extend( this.cache[ unlock ], data ); - // Otherwise, copy the properties one-by-one to the cache object - } else { - for ( prop in data ) { - cache[ prop ] = data[ prop ]; - } - } - } - return cache; - }, - get: function( owner, key ) { - // Either a valid cache is found, or will be created. - // New caches will be created and the unlock returned, - // allowing direct access to the newly created - // empty data object. A valid owner object must be provided. - var cache = this.cache[ this.key( owner ) ]; - - return key === undefined ? - cache : cache[ key ]; - }, - access: function( owner, key, value ) { - var stored; - // In cases where either: - // - // 1. No key was specified - // 2. A string key was specified, but no value provided - // - // Take the "read" path and allow the get method to determine - // which value to return, respectively either: - // - // 1. The entire cache object - // 2. The data stored at the key - // - if ( key === undefined || - ((key && typeof key === "string") && value === undefined) ) { - - stored = this.get( owner, key ); - - return stored !== undefined ? - stored : this.get( owner, jQuery.camelCase(key) ); - } - - // [*]When the key is not a string, or both a key and value - // are specified, set or extend (existing objects) with either: - // - // 1. An object of properties - // 2. A key and value - // - this.set( owner, key, value ); - - // Since the "set" path can have two possible entry points - // return the expected data based on which path was taken[*] - return value !== undefined ? value : key; - }, - remove: function( owner, key ) { - var i, name, camel, - unlock = this.key( owner ), - cache = this.cache[ unlock ]; - - if ( key === undefined ) { - this.cache[ unlock ] = {}; - - } else { - // Support array or space separated string of keys - if ( jQuery.isArray( key ) ) { - // If "name" is an array of keys... - // When data is initially created, via ("key", "val") signature, - // keys will be converted to camelCase. - // Since there is no way to tell _how_ a key was added, remove - // both plain key and camelCase key. #12786 - // This will only penalize the array argument path. - name = key.concat( key.map( jQuery.camelCase ) ); - } else { - camel = jQuery.camelCase( key ); - // Try the string as a key before any manipulation - if ( key in cache ) { - name = [ key, camel ]; - } else { - // If a key with the spaces exists, use it. - // Otherwise, create an array by matching non-whitespace - name = camel; - name = name in cache ? - [ name ] : ( name.match( rnotwhite ) || [] ); - } - } - - i = name.length; - while ( i-- ) { - delete cache[ name[ i ] ]; - } - } - }, - hasData: function( owner ) { - return !jQuery.isEmptyObject( - this.cache[ owner[ this.expando ] ] || {} - ); - }, - discard: function( owner ) { - if ( owner[ this.expando ] ) { - delete this.cache[ owner[ this.expando ] ]; - } - } -}; -var data_priv = new Data(); - -var data_user = new Data(); - - - -/* - Implementation Summary - - 1. Enforce API surface and semantic compatibility with 1.9.x branch - 2. Improve the module's maintainability by reducing the storage - paths to a single mechanism. - 3. Use the same single mechanism to support "private" and "user" data. - 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) - 5. Avoid exposing implementation details on user objects (eg. expando properties) - 6. Provide a clear path for implementation upgrade to WeakMap in 2014 -*/ -var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, - rmultiDash = /([A-Z])/g; - -function dataAttr( elem, key, data ) { - var name; - - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - // Only convert to a number if it doesn't change the string - +data + "" === data ? +data : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} - - // Make sure we set the data so it isn't changed later - data_user.set( elem, key, data ); - } else { - data = undefined; - } - } - return data; -} - -jQuery.extend({ - hasData: function( elem ) { - return data_user.hasData( elem ) || data_priv.hasData( elem ); - }, - - data: function( elem, name, data ) { - return data_user.access( elem, name, data ); - }, - - removeData: function( elem, name ) { - data_user.remove( elem, name ); - }, - - // TODO: Now that all calls to _data and _removeData have been replaced - // with direct calls to data_priv methods, these can be deprecated. - _data: function( elem, name, data ) { - return data_priv.access( elem, name, data ); - }, - - _removeData: function( elem, name ) { - data_priv.remove( elem, name ); - } -}); - -jQuery.fn.extend({ - data: function( key, value ) { - var i, name, data, - elem = this[ 0 ], - attrs = elem && elem.attributes; - - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = data_user.get( elem ); - - if ( elem.nodeType === 1 && !data_priv.get( elem, "hasDataAttrs" ) ) { - i = attrs.length; - while ( i-- ) { - - // Support: IE11+ - // The attrs elements can be null (#14894) - if ( attrs[ i ] ) { - name = attrs[ i ].name; - if ( name.indexOf( "data-" ) === 0 ) { - name = jQuery.camelCase( name.slice(5) ); - dataAttr( elem, name, data[ name ] ); - } - } - } - data_priv.set( elem, "hasDataAttrs", true ); - } - } - - return data; - } - - // Sets multiple values - if ( typeof key === "object" ) { - return this.each(function() { - data_user.set( this, key ); - }); - } - - return access( this, function( value ) { - var data, - camelKey = jQuery.camelCase( key ); - - // The calling jQuery object (element matches) is not empty - // (and therefore has an element appears at this[ 0 ]) and the - // `value` parameter was not undefined. An empty jQuery object - // will result in `undefined` for elem = this[ 0 ] which will - // throw an exception if an attempt to read a data cache is made. - if ( elem && value === undefined ) { - // Attempt to get data from the cache - // with the key as-is - data = data_user.get( elem, key ); - if ( data !== undefined ) { - return data; - } - - // Attempt to get data from the cache - // with the key camelized - data = data_user.get( elem, camelKey ); - if ( data !== undefined ) { - return data; - } - - // Attempt to "discover" the data in - // HTML5 custom data-* attrs - data = dataAttr( elem, camelKey, undefined ); - if ( data !== undefined ) { - return data; - } - - // We tried really hard, but the data doesn't exist. - return; - } - - // Set the data... - this.each(function() { - // First, attempt to store a copy or reference of any - // data that might've been store with a camelCased key. - var data = data_user.get( this, camelKey ); - - // For HTML5 data-* attribute interop, we have to - // store property names with dashes in a camelCase form. - // This might not apply to all properties...* - data_user.set( this, camelKey, value ); - - // *... In the case of properties that might _actually_ - // have dashes, we need to also store a copy of that - // unchanged property. - if ( key.indexOf("-") !== -1 && data !== undefined ) { - data_user.set( this, key, value ); - } - }); - }, null, value, arguments.length > 1, null, true ); - }, - - removeData: function( key ) { - return this.each(function() { - data_user.remove( this, key ); - }); - } -}); - - -jQuery.extend({ - queue: function( elem, type, data ) { - var queue; - - if ( elem ) { - type = ( type || "fx" ) + "queue"; - queue = data_priv.get( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !queue || jQuery.isArray( data ) ) { - queue = data_priv.access( elem, type, jQuery.makeArray(data) ); - } else { - queue.push( data ); - } - } - return queue || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks( elem, type ), - next = function() { - jQuery.dequeue( elem, type ); - }; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - startLength--; - } - - if ( fn ) { - - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } - - // clear up the last queue stop function - delete hooks.stop; - fn.call( elem, next, hooks ); - } - - if ( !startLength && hooks ) { - hooks.empty.fire(); - } - }, - - // not intended for public consumption - generates a queueHooks object, or returns the current one - _queueHooks: function( elem, type ) { - var key = type + "queueHooks"; - return data_priv.get( elem, key ) || data_priv.access( elem, key, { - empty: jQuery.Callbacks("once memory").add(function() { - data_priv.remove( elem, [ type + "queue", key ] ); - }) - }); - } -}); - -jQuery.fn.extend({ - queue: function( type, data ) { - var setter = 2; - - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; - } - - if ( arguments.length < setter ) { - return jQuery.queue( this[0], type ); - } - - return data === undefined ? - this : - this.each(function() { - var queue = jQuery.queue( this, type, data ); - - // ensure a hooks for this queue - jQuery._queueHooks( this, type ); - - if ( type === "fx" && queue[0] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - }); - }, - dequeue: function( type ) { - return this.each(function() { - jQuery.dequeue( this, type ); - }); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, obj ) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - }; - - if ( typeof type !== "string" ) { - obj = type; - type = undefined; - } - type = type || "fx"; - - while ( i-- ) { - tmp = data_priv.get( elements[ i ], type + "queueHooks" ); - if ( tmp && tmp.empty ) { - count++; - tmp.empty.add( resolve ); - } - } - resolve(); - return defer.promise( obj ); - } -}); -var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source; - -var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; - -var isHidden = function( elem, el ) { - // isHidden might be called from jQuery#filter function; - // in that case, element will be second argument - elem = el || elem; - return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); - }; - -var rcheckableType = (/^(?:checkbox|radio)$/i); - - - -(function() { - var fragment = document.createDocumentFragment(), - div = fragment.appendChild( document.createElement( "div" ) ), - input = document.createElement( "input" ); - - // #11217 - WebKit loses check when the name is after the checked attribute - // Support: Windows Web Apps (WWA) - // `name` and `type` need .setAttribute for WWA - input.setAttribute( "type", "radio" ); - input.setAttribute( "checked", "checked" ); - input.setAttribute( "name", "t" ); - - div.appendChild( input ); - - // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3 - // old WebKit doesn't clone checked state correctly in fragments - support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Make sure textarea (and checkbox) defaultValue is properly cloned - // Support: IE9-IE11+ - div.innerHTML = ""; - support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; -})(); -var strundefined = typeof undefined; - - - -support.focusinBubbles = "onfocusin" in window; - - -var - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|pointer|contextmenu)|click/, - rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; - -function returnTrue() { - return true; -} - -function returnFalse() { - return false; -} - -function safeActiveElement() { - try { - return document.activeElement; - } catch ( err ) { } -} - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - - var handleObjIn, eventHandle, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = data_priv.get( elem ); - - // Don't attach events to noData or text/comment nodes (but allow plain objects) - if ( !elemData ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if ( !(events = elemData.events) ) { - events = elemData.events = {}; - } - if ( !(eventHandle = elemData.handle) ) { - eventHandle = elemData.handle = function( e ) { - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ? - jQuery.event.dispatch.apply( elem, arguments ) : undefined; - }; - } - - // Handle multiple events separated by a space - types = ( types || "" ).match( rnotwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); - - // There *must* be a type, no attaching namespace-only handlers - if ( !type ) { - continue; - } - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend({ - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join(".") - }, handleObjIn ); - - // Init the event handler queue if we're the first - if ( !(handlers = events[ type ]) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener if the special events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - }, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - - var j, origCount, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = data_priv.hasData( elem ) && data_priv.get( elem ); - - if ( !elemData || !(events = elemData.events) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( rnotwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); - - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); - - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - delete elemData.handle; - data_priv.remove( elem, "events" ); - } - }, - - trigger: function( event, data, elem, onlyHandlers ) { - - var i, cur, tmp, bubbleType, ontype, handle, special, - eventPath = [ elem || document ], - type = hasOwn.call( event, "type" ) ? event.type : event, - namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; - - cur = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf(".") >= 0 ) { - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split("."); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf(":") < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join("."); - event.namespace_re = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === (elem.ownerDocument || document) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { - - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( data_priv.get( cur, "events" ) || {} )[ event.type ] && data_priv.get( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && handle.apply && jQuery.acceptData( cur ) ) { - event.result = handle.apply( cur, data ); - if ( event.result === false ) { - event.preventDefault(); - } - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && - jQuery.acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name name as the event. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - elem[ type ](); - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - dispatch: function( event ) { - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( event ); - - var i, j, ret, matched, handleObj, - handlerQueue = [], - args = slice.call( arguments ), - handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[0] = event; - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { - - // Triggered event must either 1) have no namespace, or - // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). - if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) - .apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( (event.result = ret) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - handlers: function( event, handlers ) { - var i, matches, sel, handleObj, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Find delegate handlers - // Black-hole SVG instance trees (#13180) - // Avoid non-left-click bubbling in Firefox (#3861) - if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { - - for ( ; cur !== this; cur = cur.parentNode || this ) { - - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.disabled !== true || event.type !== "click" ) { - matches = []; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; - - if ( matches[ sel ] === undefined ) { - matches[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) >= 0 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matches[ sel ] ) { - matches.push( handleObj ); - } - } - if ( matches.length ) { - handlerQueue.push({ elem: cur, handlers: matches }); - } - } - } - } - - // Add the remaining (directly-bound) handlers - if ( delegateCount < handlers.length ) { - handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); - } - - return handlerQueue; - }, - - // Includes some event props shared by KeyEvent and MouseEvent - props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), - - fixHooks: {}, - - keyHooks: { - props: "char charCode key keyCode".split(" "), - filter: function( event, original ) { - - // Add which for key events - if ( event.which == null ) { - event.which = original.charCode != null ? original.charCode : original.keyCode; - } - - return event; - } - }, - - mouseHooks: { - props: "button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "), - filter: function( event, original ) { - var eventDoc, doc, body, - button = original.button; - - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && original.clientX != null ) { - eventDoc = event.target.ownerDocument || document; - doc = eventDoc.documentElement; - body = eventDoc.body; - - event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); - event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && button !== undefined ) { - event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); - } - - return event; - } - }, - - fix: function( event ) { - if ( event[ jQuery.expando ] ) { - return event; - } - - // Create a writable copy of the event object and normalize some properties - var i, prop, copy, - type = event.type, - originalEvent = event, - fixHook = this.fixHooks[ type ]; - - if ( !fixHook ) { - this.fixHooks[ type ] = fixHook = - rmouseEvent.test( type ) ? this.mouseHooks : - rkeyEvent.test( type ) ? this.keyHooks : - {}; - } - copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; - - event = new jQuery.Event( originalEvent ); - - i = copy.length; - while ( i-- ) { - prop = copy[ i ]; - event[ prop ] = originalEvent[ prop ]; - } - - // Support: Cordova 2.5 (WebKit) (#13255) - // All events should have a target; Cordova deviceready doesn't - if ( !event.target ) { - event.target = document; - } - - // Support: Safari 6.0+, Chrome < 28 - // Target should not be a text node (#504, #13143) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } - - return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; - }, - - special: { - load: { - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - focus: { - // Fire native event if possible so blur/focus sequence is correct - trigger: function() { - if ( this !== safeActiveElement() && this.focus ) { - this.focus(); - return false; - } - }, - delegateType: "focusin" - }, - blur: { - trigger: function() { - if ( this === safeActiveElement() && this.blur ) { - this.blur(); - return false; - } - }, - delegateType: "focusout" - }, - click: { - // For checkbox, fire native event so checked state will be right - trigger: function() { - if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { - this.click(); - return false; - } - }, - - // For cross-browser consistency, don't fire native .click() on links - _default: function( event ) { - return jQuery.nodeName( event.target, "a" ); - } - }, - - beforeunload: { - postDispatch: function( event ) { - - // Support: Firefox 20+ - // Firefox doesn't alert if the returnValue field is not set. - if ( event.result !== undefined && event.originalEvent ) { - event.originalEvent.returnValue = event.result; - } - } - } - }, - - simulate: function( type, elem, event, bubble ) { - // Piggyback on a donor event to simulate a different one. - // Fake originalEvent to avoid donor's stopPropagation, but if the - // simulated event prevents default then we do the same on the donor. - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true, - originalEvent: {} - } - ); - if ( bubble ) { - jQuery.event.trigger( e, null, elem ); - } else { - jQuery.event.dispatch.call( elem, e ); - } - if ( e.isDefaultPrevented() ) { - event.preventDefault(); - } - } -}; - -jQuery.removeEvent = function( elem, type, handle ) { - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle, false ); - } -}; - -jQuery.Event = function( src, props ) { - // Allow instantiation without the 'new' keyword - if ( !(this instanceof jQuery.Event) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = src.defaultPrevented || - src.defaultPrevented === undefined && - // Support: Android < 4.0 - src.returnValue === false ? - returnTrue : - returnFalse; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - - preventDefault: function() { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - - if ( e && e.preventDefault ) { - e.preventDefault(); - } - }, - stopPropagation: function() { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - - if ( e && e.stopPropagation ) { - e.stopPropagation(); - } - }, - stopImmediatePropagation: function() { - var e = this.originalEvent; - - this.isImmediatePropagationStopped = returnTrue; - - if ( e && e.stopImmediatePropagation ) { - e.stopImmediatePropagation(); - } - - this.stopPropagation(); - } -}; - -// Create mouseenter/leave events using mouseover/out and event-time checks -// Support: Chrome 15+ -jQuery.each({ - mouseenter: "mouseover", - mouseleave: "mouseout", - pointerenter: "pointerover", - pointerleave: "pointerout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !jQuery.contains( target, related )) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -}); - -// Create "bubbling" focus and blur events -// Support: Firefox, Chrome, Safari -if ( !support.focusinBubbles ) { - jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler on the document while someone wants focusin/focusout - var handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - var doc = this.ownerDocument || this, - attaches = data_priv.access( doc, fix ); - - if ( !attaches ) { - doc.addEventListener( orig, handler, true ); - } - data_priv.access( doc, fix, ( attaches || 0 ) + 1 ); - }, - teardown: function() { - var doc = this.ownerDocument || this, - attaches = data_priv.access( doc, fix ) - 1; - - if ( !attaches ) { - doc.removeEventListener( orig, handler, true ); - data_priv.remove( doc, fix ); - - } else { - data_priv.access( doc, fix, attaches ); - } - } - }; - }); -} - -jQuery.fn.extend({ - - on: function( types, selector, data, fn, /*INTERNAL*/ one ) { - var origFn, type; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - this.on( type, selector, data, types[ type ], one ); - } - return this; - } - - if ( data == null && fn == null ) { - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return this; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return this.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - }); - }, - one: function( types, selector, data, fn ) { - return this.on( types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each(function() { - jQuery.event.remove( this, types, fn, selector ); - }); - }, - - trigger: function( type, data ) { - return this.each(function() { - jQuery.event.trigger( type, data, this ); - }); - }, - triggerHandler: function( type, data ) { - var elem = this[0]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -}); - - -var - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, - rtagName = /<([\w:]+)/, - rhtml = /<|&#?\w+;/, - rnoInnerhtml = /<(?:script|style|link)/i, - // checked="checked" or checked - rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, - rscriptType = /^$|\/(?:java|ecma)script/i, - rscriptTypeMasked = /^true\/(.*)/, - rcleanScript = /^\s*\s*$/g, - - // We have to close these tags to support XHTML (#13200) - wrapMap = { - - // Support: IE 9 - option: [ 1, "" ], - - thead: [ 1, "
") - .append(I18N.getText("experiments.listTitle")) - .append("
webapp <port>
", "
" ], - col: [ 2, "", "
" ], - tr: [ 2, "", "
" ], - td: [ 3, "", "
" ], - - _default: [ 0, "", "" ] - }; - -// Support: IE 9 -wrapMap.optgroup = wrapMap.option; - -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -// Support: 1.x compatibility -// Manipulating tables requires a tbody -function manipulationTarget( elem, content ) { - return jQuery.nodeName( elem, "table" ) && - jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? - - elem.getElementsByTagName("tbody")[0] || - elem.appendChild( elem.ownerDocument.createElement("tbody") ) : - elem; -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - elem.type = (elem.getAttribute("type") !== null) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - var match = rscriptTypeMasked.exec( elem.type ); - - if ( match ) { - elem.type = match[ 1 ]; - } else { - elem.removeAttribute("type"); - } - - return elem; -} - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - data_priv.set( - elems[ i ], "globalEval", !refElements || data_priv.get( refElements[ i ], "globalEval" ) - ); - } -} - -function cloneCopyEvent( src, dest ) { - var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; - - if ( dest.nodeType !== 1 ) { - return; - } - - // 1. Copy private data: events, handlers, etc. - if ( data_priv.hasData( src ) ) { - pdataOld = data_priv.access( src ); - pdataCur = data_priv.set( dest, pdataOld ); - events = pdataOld.events; - - if ( events ) { - delete pdataCur.handle; - pdataCur.events = {}; - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - } - - // 2. Copy user data - if ( data_user.hasData( src ) ) { - udataOld = data_user.access( src ); - udataCur = jQuery.extend( {}, udataOld ); - - data_user.set( dest, udataCur ); - } -} - -function getAll( context, tag ) { - var ret = context.getElementsByTagName ? context.getElementsByTagName( tag || "*" ) : - context.querySelectorAll ? context.querySelectorAll( tag || "*" ) : - []; - - return tag === undefined || tag && jQuery.nodeName( context, tag ) ? - jQuery.merge( [ context ], ret ) : - ret; -} - -// Support: IE >= 9 -function fixInput( src, dest ) { - var nodeName = dest.nodeName.toLowerCase(); - - // Fails to persist the checked state of a cloned checkbox or radio button. - if ( nodeName === "input" && rcheckableType.test( src.type ) ) { - dest.checked = src.checked; - - // Fails to return the selected option to the default selected state when cloning options - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - -jQuery.extend({ - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var i, l, srcElements, destElements, - clone = elem.cloneNode( true ), - inPage = jQuery.contains( elem.ownerDocument, elem ); - - // Support: IE >= 9 - // Fix Cloning issues - if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && - !jQuery.isXMLDoc( elem ) ) { - - // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - fixInput( srcElements[ i ], destElements[ i ] ); - } - } - - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - cloneCopyEvent( srcElements[ i ], destElements[ i ] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } - - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } - - // Return the cloned set - return clone; - }, - - buildFragment: function( elems, context, scripts, selection ) { - var elem, tmp, tag, wrap, contains, j, - fragment = context.createDocumentFragment(), - nodes = [], - i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - elem = elems[ i ]; - - if ( elem || elem === 0 ) { - - // Add nodes directly - if ( jQuery.type( elem ) === "object" ) { - // Support: QtWebKit - // jQuery.merge because push.apply(_, arraylike) throws - jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - - // Convert non-html into a text node - } else if ( !rhtml.test( elem ) ) { - nodes.push( context.createTextNode( elem ) ); - - // Convert html into DOM nodes - } else { - tmp = tmp || fragment.appendChild( context.createElement("div") ); - - // Deserialize a standard representation - tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); - wrap = wrapMap[ tag ] || wrapMap._default; - tmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, "<$1>" ) + wrap[ 2 ]; - - // Descend through wrappers to the right content - j = wrap[ 0 ]; - while ( j-- ) { - tmp = tmp.lastChild; - } - - // Support: QtWebKit - // jQuery.merge because push.apply(_, arraylike) throws - jQuery.merge( nodes, tmp.childNodes ); - - // Remember the top-level container - tmp = fragment.firstChild; - - // Fixes #12346 - // Support: Webkit, IE - tmp.textContent = ""; - } - } - } - - // Remove wrapper from fragment - fragment.textContent = ""; - - i = 0; - while ( (elem = nodes[ i++ ]) ) { - - // #4087 - If origin and destination elements are the same, and this is - // that element, do not do anything - if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { - continue; - } - - contains = jQuery.contains( elem.ownerDocument, elem ); - - // Append to fragment - tmp = getAll( fragment.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( contains ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( (elem = tmp[ j++ ]) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); - } - } - } - } - - return fragment; - }, - - cleanData: function( elems ) { - var data, elem, type, key, - special = jQuery.event.special, - i = 0; - - for ( ; (elem = elems[ i ]) !== undefined; i++ ) { - if ( jQuery.acceptData( elem ) ) { - key = elem[ data_priv.expando ]; - - if ( key && (data = data_priv.cache[ key ]) ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } - if ( data_priv.cache[ key ] ) { - // Discard any remaining `private` data - delete data_priv.cache[ key ]; - } - } - } - // Discard any remaining `user` data - delete data_user.cache[ elem[ data_user.expando ] ]; - } - } -}); - -jQuery.fn.extend({ - text: function( value ) { - return access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().each(function() { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - this.textContent = value; - } - }); - }, null, value, arguments.length ); - }, - - append: function() { - return this.domManip( arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.appendChild( elem ); - } - }); - }, - - prepend: function() { - return this.domManip( arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.insertBefore( elem, target.firstChild ); - } - }); - }, - - before: function() { - return this.domManip( arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this ); - } - }); - }, - - after: function() { - return this.domManip( arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this.nextSibling ); - } - }); - }, - - remove: function( selector, keepData /* Internal Use Only */ ) { - var elem, - elems = selector ? jQuery.filter( selector, this ) : this, - i = 0; - - for ( ; (elem = elems[i]) != null; i++ ) { - if ( !keepData && elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem ) ); - } - - if ( elem.parentNode ) { - if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { - setGlobalEval( getAll( elem, "script" ) ); - } - elem.parentNode.removeChild( elem ); - } - } - - return this; - }, - - empty: function() { - var elem, - i = 0; - - for ( ; (elem = this[i]) != null; i++ ) { - if ( elem.nodeType === 1 ) { - - // Prevent memory leaks - jQuery.cleanData( getAll( elem, false ) ); - - // Remove any remaining nodes - elem.textContent = ""; - } - } - - return this; - }, - - clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map(function() { - return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); - }); - }, - - html: function( value ) { - return access( this, function( value ) { - var elem = this[ 0 ] || {}, - i = 0, - l = this.length; - - if ( value === undefined && elem.nodeType === 1 ) { - return elem.innerHTML; - } - - // See if we can take a shortcut and just use innerHTML - if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { - - value = value.replace( rxhtmlTag, "<$1>" ); - - try { - for ( ; i < l; i++ ) { - elem = this[ i ] || {}; - - // Remove element nodes and prevent memory leaks - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch( e ) {} - } - - if ( elem ) { - this.empty().append( value ); - } - }, null, value, arguments.length ); - }, - - replaceWith: function() { - var arg = arguments[ 0 ]; - - // Make the changes, replacing each context element with the new content - this.domManip( arguments, function( elem ) { - arg = this.parentNode; - - jQuery.cleanData( getAll( this ) ); - - if ( arg ) { - arg.replaceChild( elem, this ); - } - }); - - // Force removal if there was no new content (e.g., from empty arguments) - return arg && (arg.length || arg.nodeType) ? this : this.remove(); - }, - - detach: function( selector ) { - return this.remove( selector, true ); - }, - - domManip: function( args, callback ) { - - // Flatten any nested arrays - args = concat.apply( [], args ); - - var fragment, first, scripts, hasScripts, node, doc, - i = 0, - l = this.length, - set = this, - iNoClone = l - 1, - value = args[ 0 ], - isFunction = jQuery.isFunction( value ); - - // We can't cloneNode fragments that contain checked, in WebKit - if ( isFunction || - ( l > 1 && typeof value === "string" && - !support.checkClone && rchecked.test( value ) ) ) { - return this.each(function( index ) { - var self = set.eq( index ); - if ( isFunction ) { - args[ 0 ] = value.call( this, index, self.html() ); - } - self.domManip( args, callback ); - }); - } - - if ( l ) { - fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); - first = fragment.firstChild; - - if ( fragment.childNodes.length === 1 ) { - fragment = first; - } - - if ( first ) { - scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); - hasScripts = scripts.length; - - // Use the original fragment for the last item instead of the first because it can end up - // being emptied incorrectly in certain situations (#8070). - for ( ; i < l; i++ ) { - node = fragment; - - if ( i !== iNoClone ) { - node = jQuery.clone( node, true, true ); - - // Keep references to cloned scripts for later restoration - if ( hasScripts ) { - // Support: QtWebKit - // jQuery.merge because push.apply(_, arraylike) throws - jQuery.merge( scripts, getAll( node, "script" ) ); - } - } - - callback.call( this[ i ], node, i ); - } - - if ( hasScripts ) { - doc = scripts[ scripts.length - 1 ].ownerDocument; - - // Reenable scripts - jQuery.map( scripts, restoreScript ); - - // Evaluate executable scripts on first document insertion - for ( i = 0; i < hasScripts; i++ ) { - node = scripts[ i ]; - if ( rscriptType.test( node.type || "" ) && - !data_priv.access( node, "globalEval" ) && jQuery.contains( doc, node ) ) { - - if ( node.src ) { - // Optional AJAX dependency, but won't run scripts if not present - if ( jQuery._evalUrl ) { - jQuery._evalUrl( node.src ); - } - } else { - jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) ); - } - } - } - } - } - } - - return this; - } -}); - -jQuery.each({ - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function( name, original ) { - jQuery.fn[ name ] = function( selector ) { - var elems, - ret = [], - insert = jQuery( selector ), - last = insert.length - 1, - i = 0; - - for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone( true ); - jQuery( insert[ i ] )[ original ]( elems ); - - // Support: QtWebKit - // .get() because push.apply(_, arraylike) throws - push.apply( ret, elems.get() ); - } - - return this.pushStack( ret ); - }; -}); - - -var iframe, - elemdisplay = {}; - -/** - * Retrieve the actual display of a element - * @param {String} name nodeName of the element - * @param {Object} doc Document object - */ -// Called only from within defaultDisplay -function actualDisplay( name, doc ) { - var style, - elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), - - // getDefaultComputedStyle might be reliably used only on attached element - display = window.getDefaultComputedStyle && ( style = window.getDefaultComputedStyle( elem[ 0 ] ) ) ? - - // Use of this method is a temporary fix (more like optmization) until something better comes along, - // since it was removed from specification and supported only in FF - style.display : jQuery.css( elem[ 0 ], "display" ); - - // We don't have any data stored on the element, - // so use "detach" method as fast way to get rid of the element - elem.detach(); - - return display; -} - -/** - * Try to determine the default display value of an element - * @param {String} nodeName - */ -function defaultDisplay( nodeName ) { - var doc = document, - display = elemdisplay[ nodeName ]; - - if ( !display ) { - display = actualDisplay( nodeName, doc ); - - // If the simple way fails, read from inside an iframe - if ( display === "none" || !display ) { - - // Use the already-created iframe if possible - iframe = (iframe || jQuery( "