diff --git a/src/java/com/threerings/chat/ChatLogic.java b/src/java/com/threerings/chat/ChatLogic.java new file mode 100644 index 00000000..b4ebef40 --- /dev/null +++ b/src/java/com/threerings/chat/ChatLogic.java @@ -0,0 +1,398 @@ +// +// $Id$ +// +// Nenya library - tools for developing networked games +// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/nenya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.chat; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.geom.Area; +import java.awt.geom.Ellipse2D; + +import com.samskivert.swing.Label; +import com.samskivert.util.Tuple; + +import com.threerings.crowd.chat.data.ChatCodes; + +/** + * Contains all of the routines that might (or must) be customized by a system that makes use of + * the comic chat system. + */ +public abstract class ChatLogic +{ + /** The default chat decay parameter. See {@link #getDisplayDurationIndex}. */ + public static final int DEFAULT_CHAT_DECAY = 1; + + /** The padding in each direction around the text to the edges of a chat 'bubble'. */ + public static final int PAD = 10; + + /** Type mode code for default chat type (speaking). */ + public static final int SPEAK = 0; + + /** Type mode code for shout chat type. */ + public static final int SHOUT = 1; + + /** Type mode code for emote chat type. */ + public static final int EMOTE = 2; + + /** Type mode code for think chat type. */ + public static final int THINK = 3; + + /** Type place code for default place chat (cluster, scene). */ + public static final int PLACE = 1 << 4; + + // 2 and 3 are skipped for legacy migration reasons + + /** Type code for a chat type that was used in some special context, like in a negotiation. */ + public static final int SPECIALIZED = 4 << 4; + + /** Our internal code for tell chat. */ + public static final int TELL = 5 << 4; + + /** Our internal code for tell feedback chat. */ + public static final int TELLFEEDBACK = 6 << 4; + + /** Our internal code for info system messges. */ + public static final int INFO = 7 << 4; + + /** Our internal code for feedback system messages. */ + public static final int FEEDBACK = 8 << 4; + + /** Our internal code for attention system messages. */ + public static final int ATTENTION = 9 << 4; + + /** Our internal code for any type of chat that is continued in a subtitle. */ + public static final int CONTINUATION = 10 << 4; + + /** Type place code for broadcast chat type. */ + public static final int BROADCAST = 11 << 4; + + /** Our internal code for a chat type we will ignore. */ + public static final int IGNORECHAT = -1; + + /** + * Returns the message bundle used to translate default messages. + */ + public abstract String getDefaultMessageBundle (); + + /** + * Determines the format string and whether to use quotes based on the chat type. + */ + public Tuple decodeFormat (int type, String format) + { + boolean quotes = true; + switch (placeOf(type)) { + // derived classes may wish to change the format here based on the place + case PLACE: + switch (modeOf(type)) { + case EMOTE: + quotes = false; + break; + } + break; + } + return Tuple.newTuple(format, quotes); + } + + /** + * Decodes the main chat type given the supplied localtype provided by the chat system. + */ + public int decodeType (String localtype) + { + if (ChatCodes.USER_CHAT_TYPE.equals(localtype)) { + return TELL; + } else if (ChatCodes.PLACE_CHAT_TYPE.equals(localtype)) { + return PLACE; + } else { + return 0; + } + } + + /** + * Adjust the chat type based on the mode of the chat message. + */ + public int adjustTypeByMode (int mode, int type) + { + switch (mode) { + case ChatCodes.DEFAULT_MODE: + return type | SPEAK; + case ChatCodes.EMOTE_MODE: + return type | EMOTE; + case ChatCodes.THINK_MODE: + return type | THINK; + case ChatCodes.SHOUT_MODE: + return type | SHOUT; + case ChatCodes.BROADCAST_MODE: + return BROADCAST; // broadcast always looks like broadcast + default: + return type; + } + } + + /** + * Get the font to use for the given bubble type. + */ + public Font getFont (int type) + { + return DEFAULT_FONT; + } + + /** + * Creates a label for the specified text. Derived classes may wish to use specialized labels. + */ + public Label createLabel (String text) + { + return new Label(text); + } + + /** + * Computes the chat glyph outline color from the chat message type. + */ + public Color getOutlineColor (int type) + { + switch (type) { + case BROADCAST: + return BROADCAST_COLOR; + case TELL: + return TELL_COLOR; + case TELLFEEDBACK: + return TELLFEEDBACK_COLOR; + case INFO: + return INFO_COLOR; + case FEEDBACK: + return FEEDBACK_COLOR; + case ATTENTION: + return ATTENTION_COLOR; + default: + return Color.black; + } + } + + /** + * Get the appropriate shape for the specified type of chat. + * + * @param r the rectangle bounding the chat label. + * @param b the rectangle bounding the chat label and icon. + */ + public Shape getSubtitleShape (int type, Rectangle r, Rectangle b) + { + int placeType = placeOf(type); + switch (placeType) { + case SPECIALIZED: + case PLACE: + return getPlaceSubtitleShape(type, r); + + case TELL: { + // lightning box! + int halfy = r.y + r.height/2; + Polygon p = new Polygon(); + p.addPoint(r.x - PAD - 2, r.y); + p.addPoint(r.x - PAD / 2 - 2, halfy); + p.addPoint(r.x - PAD, halfy); + p.addPoint(r.x - PAD / 2, r.y + r.height); + p.addPoint(r.x + r.width + PAD + 2, r.y + r.height); + p.addPoint(r.x + r.width + PAD / 2 + 2, halfy); + p.addPoint(r.x + r.width + PAD, halfy); + p.addPoint(r.x + r.width + PAD / 2, r.y); + return p; + } + + case TELLFEEDBACK: { + // reverse-lightning box! + int halfy = r.y + r.height/2; + Polygon p = new Polygon(); + p.addPoint(r.x - PAD - 2, r.y + r.height); + p.addPoint(r.x - PAD / 2 - 2, halfy); + p.addPoint(r.x - PAD, halfy); + p.addPoint(r.x - PAD / 2, r.y); + p.addPoint(r.x + r.width + PAD + 2, r.y); + p.addPoint(r.x + r.width + PAD / 2 + 2, halfy); + p.addPoint(r.x + r.width + PAD, halfy); + p.addPoint(r.x + r.width + PAD / 2, r.y + r.height); + return p; + } + + case FEEDBACK: { + // slanted box subtitle + Polygon p = new Polygon(); + p.addPoint(r.x - PAD / 2, r.y); + p.addPoint(r.x + r.width + PAD, r.y); + p.addPoint(r.x + r.width + PAD / 2, r.y + r.height); + p.addPoint(r.x - PAD, r.y + r.height); + return p; + } + + case BROADCAST: + case CONTINUATION: + case INFO: + case ATTENTION: + default: { + Rectangle grown = new Rectangle(r); + grown.grow(PAD, 0); + return new Area(grown); + } + } + } + + /** + * Get the spacing, in pixels, between the latest subtitle of the specified type and the + * previous subtitle. + */ + public int getSubtitleSpacing (int type) + { + switch (placeOf(type)) { + // derived classes may wish to adjust subtitle spacing here based on chat type + default: + return 1; + } + } + + /** + * A helper function for {@link #getSubtitleShape}. + */ + public Shape getPlaceSubtitleShape (int type, Rectangle r) + { + switch (modeOf(type)) { + default: + case SPEAK: { + // rounded rectangle subtitle + Area a = new Area(r); + a.add(new Area(new Ellipse2D.Float(r.x - PAD, r.y, PAD * 2, r.height))); + a.add(new Area(new Ellipse2D.Float(r.x + r.width - PAD, r.y, PAD * 2, r.height))); + return a; + } + + case THINK: { + r.grow(PAD / 2, 0); + Area a = new Area(r); + int dia = 8; + int num = (int) Math.ceil(r.height / ((float) dia)); + int leftside = r.x - dia/2; + int rightside = r.x + r.width - dia/2 - 1; + + int maxh = r.height - dia; + for (int ii=0; ii < num; ii++) { + int ypos = r.y + Math.min((r.height * ii) / num, maxh); + a.add(new Area(new Ellipse2D.Float(leftside, ypos, dia, dia))); + a.add(new Area(new Ellipse2D.Float(rightside, ypos, dia, dia))); + } + return a; + } + + case SHOUT: { + r.grow(PAD / 2, 0); + Area a = new Area(r); + Polygon left = new Polygon(); + Polygon right = new Polygon(); + + int spikehei = 8; + int num = (int) Math.ceil(r.height / ((float) spikehei)); + left.addPoint(r.x, r.y); + left.addPoint(r.x - PAD, r.y + spikehei / 2); + left.addPoint(r.x, r.y + spikehei); + right.addPoint(r.x + r.width , r.y); + right.addPoint(r.x + r.width + PAD, r.y + spikehei / 2); + right.addPoint(r.x + r.width, r.y + spikehei); + + int ypos = 0; + int maxpos = r.y + r.height - spikehei + 1; + for (int ii=0; ii < num; ii++) { + int newpos = Math.min((r.height * ii) / num, maxpos); + left.translate(0, newpos - ypos); + right.translate(0, newpos - ypos); + a.add(new Area(left)); + a.add(new Area(right)); + ypos = newpos; + } + + return a; + } + + case EMOTE: { + // a box that curves inward on the left and right + r.grow(PAD, 0); + Area a = new Area(r); + a.subtract(new Area(new Ellipse2D.Float(r.x - PAD / 2, r.y, PAD, r.height))); + a.subtract(new Area(new Ellipse2D.Float(r.x + r.width - PAD / 2, r.y, PAD, r.height))); + return a; + } + } + } + + /** + * Returns metrics on how long chat messages should be displayed. + */ + public long[] getDisplayDurations (int indexOffset) + { + return DISPLAY_DURATION_PARAMS[getDisplayDurationIndex() + indexOffset]; + } + + /** + * Get the current display duration parameters: 0 = fast, 1 = medium, 2 = long. + * See {@link #DISPLAY_DURATION_PARAMS}. + */ + protected int getDisplayDurationIndex () + { + return DEFAULT_CHAT_DECAY; + } + + /** + * Extracts the mode constant from the type value. + */ + protected static int modeOf (int type) + { + return type & 0xF; + } + + /** + * Extract the place constant from the type value. + */ + protected static int placeOf (int type) + { + return type & ~0xF; + } + + /** + * Times to display chat: { (time per character), (min time), (max time) } + * + * Groups 0/1/2 are short/medium/long for chat bubbles, and groups 1/2/3 are short/medium/long + * for subtitles. + */ + protected static final long[][] DISPLAY_DURATION_PARAMS = { + { 125L, 10000L, 30000L }, // fastest durations + { 200L, 15000L, 40000L }, // medium (default) durations + { 275L, 20000L, 50000L }, // longest regular duration.. + { 350L, 25000L, 60000L } // grampatime! + }; + + // used to color chat bubbles + protected static final Color BROADCAST_COLOR = new Color(0x990000); + protected static final Color FEEDBACK_COLOR = new Color(0x00AA00); + protected static final Color TELL_COLOR = new Color(0x0000AA); + protected static final Color TELLFEEDBACK_COLOR = new Color(0x00AAAA); + protected static final Color INFO_COLOR = new Color(0xAAAA00); + protected static final Color ATTENTION_COLOR = new Color(0xFF5000); + + /** Our default chat font. */ + protected static final Font DEFAULT_FONT = new Font("SansSerif", Font.PLAIN, 12); +} diff --git a/src/java/com/threerings/chat/ChatOverlay.java b/src/java/com/threerings/chat/ChatOverlay.java index 551ad4b1..0993982a 100644 --- a/src/java/com/threerings/chat/ChatOverlay.java +++ b/src/java/com/threerings/chat/ChatOverlay.java @@ -63,14 +63,6 @@ public abstract class ChatOverlay public Point getSpeaker (Name speaker); } - /** - * Construct a chat overlay. - */ - public ChatOverlay (CrowdContext ctx) - { - _ctx = ctx; - } - /** * Causes the chat overlay to make itself visible or invisible. */ @@ -152,6 +144,15 @@ public abstract class ChatOverlay { } + /** + * Construct a chat overlay. + */ + protected ChatOverlay (CrowdContext ctx, ChatLogic logic) + { + _ctx = ctx; + _logic = logic; + } + /** * Returns true if this chat overlay is showing and should therefore update its display * accordingly. @@ -166,7 +167,7 @@ public abstract class ChatOverlay */ protected String xlate (String message) { - return xlate(getDefaultMessageBundle(), message); + return xlate(_logic.getDefaultMessageBundle(), message); } /** @@ -186,17 +187,12 @@ public abstract class ChatOverlay return message; } - /** - * Returns the message bundle used to translate default messages. - */ - protected String getDefaultMessageBundle () - { - return null; - } - /** The light of our life. */ protected CrowdContext _ctx; + /** Contains all of our customizations. */ + protected ChatLogic _logic; + /** The component in which we are being displayed. */ protected VirtualMediaPanel _target; diff --git a/src/java/com/threerings/chat/ComicChatOverlay.java b/src/java/com/threerings/chat/ComicChatOverlay.java index 56c2bb13..f9e93ef6 100644 --- a/src/java/com/threerings/chat/ComicChatOverlay.java +++ b/src/java/com/threerings/chat/ComicChatOverlay.java @@ -65,9 +65,10 @@ public class ComicChatOverlay extends SubtitleChatOverlay * * @param subtitleHeight the amount of vertical space to use for subtitles. */ - public ComicChatOverlay (CrowdContext ctx, JScrollBar historyBar, int subtitleHeight) + public ComicChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar historyBar, + int subtitleHeight) { - super(ctx, historyBar, subtitleHeight); + super(ctx, logic, historyBar, subtitleHeight); } @Override @@ -177,7 +178,7 @@ public class ComicChatOverlay extends SubtitleChatOverlay */ protected boolean isPlaceOrientedType (int type) { - return (placeOf(type)) == PLACE; + return (ChatLogic.placeOf(type)) == ChatLogic.PLACE; } @Override @@ -186,16 +187,16 @@ public class ComicChatOverlay extends SubtitleChatOverlay // we might need to modify the textual part with translations, // but we can't do that to the message object, since other chatdisplays also get it. String text = message.message; - switch (placeOf(type)) { - case INFO: - case ATTENTION: + switch (ChatLogic.placeOf(type)) { + case ChatLogic.INFO: + case ChatLogic.ATTENTION: if (createBubble(layoutGfx, type, message.timestamp, text, null, null)) { return; // EXIT; } // if the bubble didn't fit (unlikely), make it a subtitle break; - case PLACE: { + case ChatLogic.PLACE: { UserMessage msg = (UserMessage) message; Point speakerloc = _provider.getSpeaker(msg.speaker); if (speakerloc == null) { @@ -205,7 +206,7 @@ public class ComicChatOverlay extends SubtitleChatOverlay } // emotes won't actually have tails, but we do want them to appear near the pirate - if (modeOf(type) == EMOTE) { + if (ChatLogic.modeOf(type) == ChatLogic.EMOTE) { text = xlate( MessageBundle.tcompose("m.emote_format", msg.getSpeakerDisplayName())) + " " + text; @@ -227,7 +228,7 @@ public class ComicChatOverlay extends SubtitleChatOverlay if (leftover.length() > 0 && !isHistoryMode()) { String ltext = MessageBundle.tcompose("m.continue_format", msg.speaker); ltext = xlate(ltext) + " \"" + leftover + "\""; - addSubtitle(createSubtitle(layoutGfx, CONTINUATION, + addSubtitle(createSubtitle(layoutGfx, ChatLogic.CONTINUATION, message.timestamp, null, 0, ltext, true)); } return; // EXIT @@ -272,7 +273,7 @@ public class ComicChatOverlay extends SubtitleChatOverlay protected boolean createBubble ( Graphics2D gfx, int type, long timestamp, String text, Name speaker, Point speakerloc) { - Label label = layoutText(gfx, getFont(type), text); + Label label = layoutText(gfx, _logic.getFont(type), text); label.setAlignment(Label.CENTER); gfx.dispose(); @@ -341,24 +342,17 @@ public class ComicChatOverlay extends SubtitleChatOverlay full = area; } - Color color; - switch (type) { - case INFO: color = INFO_COLOR; break; - case ATTENTION: color = ATTENTION_COLOR; break; - default: color = Color.BLACK; break; - } - // finally, add the bubble long lifetime = getChatExpire(timestamp, label.getText())-timestamp; BubbleGlyph newbub = new BubbleGlyph( - this, type, lifetime, full, label, - adjustLabel(type, r.getLocation()), shape, speaker, color); + this, type, lifetime, full, label, adjustLabel(type, r.getLocation()), shape, + speaker, _logic.getOutlineColor(type)); newbub.setDim(_dimmed); _bubbles.add(newbub); _target.addAnimation(newbub); - // and we need to dirty all the bubbles because they'll all - // be painted in slightly different colors + // and we need to dirty all the bubbles because they'll all be painted in slightly + // different colors int numbubs = _bubbles.size(); for (int ii=0; ii < numbubs; ii++) { _bubbles.get(ii).setAgeLevel(numbubs - ii - 1); @@ -375,10 +369,10 @@ public class ComicChatOverlay extends SubtitleChatOverlay */ protected Rectangle getBubbleSize (int type, Dimension d) { - switch (modeOf(type)) { - case SHOUT: - case THINK: - case EMOTE: + switch (ChatLogic.modeOf(type)) { + case ChatLogic.SHOUT: + case ChatLogic.THINK: + case ChatLogic.EMOTE: // extra room for these two monsters return new Rectangle(d.width + (PAD * 4), d.height + (PAD * 4)); @@ -392,10 +386,10 @@ public class ComicChatOverlay extends SubtitleChatOverlay */ protected Point adjustLabel (int type, Point labelpos) { - switch (modeOf(type)) { - case SHOUT: - case EMOTE: - case THINK: + switch (ChatLogic.modeOf(type)) { + case ChatLogic.SHOUT: + case ChatLogic.EMOTE: + case ChatLogic.THINK: labelpos.translate(PAD * 2, PAD * 2); break; @@ -422,15 +416,15 @@ public class ComicChatOverlay extends SubtitleChatOverlay // otherwise we have different areas for different types Rectangle vbounds = _target.getViewBounds(); - switch (placeOf(type)) { - case INFO: - case ATTENTION: + switch (ChatLogic.placeOf(type)) { + case ChatLogic.INFO: + case ChatLogic.ATTENTION: // upper left r.setLocation(vbounds.x + BUBBLE_SPACING, vbounds.y + BUBBLE_SPACING); return; - case PLACE: + case ChatLogic.PLACE: log.warning("Got to a place where I shouldn't get!"); break; // fall through } @@ -475,21 +469,21 @@ public class ComicChatOverlay extends SubtitleChatOverlay */ protected Shape getBubbleShape (int type, Rectangle r) { - switch (placeOf(type)) { - case INFO: - case ATTENTION: + switch (ChatLogic.placeOf(type)) { + case ChatLogic.INFO: + case ChatLogic.ATTENTION: // boring rectangle wrapped in an Area for translation return new Area(r); } - switch (modeOf(type)) { - case SPEAK: + switch (ChatLogic.modeOf(type)) { + case ChatLogic.SPEAK: // a rounded rectangle balloon, put in an Area so that it's // translatable return new Area(new RoundRectangle2D.Float( r.x, r.y, r.width, r.height, PAD * 4, PAD * 4)); - case SHOUT: { + case ChatLogic.SHOUT: { // spikey balloon Polygon left = new Polygon(), right = new Polygon(); Polygon top = new Polygon(), bot = new Polygon(); @@ -577,7 +571,7 @@ public class ComicChatOverlay extends SubtitleChatOverlay return a; } - case EMOTE: { + case ChatLogic.EMOTE: { // a box that curves inward on all sides Area a = new Area(r); a.subtract(new Area(new Ellipse2D.Float(r.x, r.y - PAD, r.width, PAD * 2))); @@ -587,7 +581,7 @@ public class ComicChatOverlay extends SubtitleChatOverlay return a; } - case THINK: { + case ChatLogic.THINK: { // cloudy balloon! int x = r.x + PAD; int y = r.y + PAD; @@ -624,7 +618,7 @@ public class ComicChatOverlay extends SubtitleChatOverlay } // fall back to subtitle shape - return getSubtitleShape(type, r, r); + return _logic.getSubtitleShape(type, r, r); } /** @@ -633,7 +627,7 @@ public class ComicChatOverlay extends SubtitleChatOverlay protected Shape getTail (int type, Rectangle r, Point speaker) { // emotes don't actually have tails - if (modeOf(type) == EMOTE) { + if (ChatLogic.modeOf(type) == ChatLogic.EMOTE) { return new Area(); // empty shape } @@ -647,7 +641,7 @@ public class ComicChatOverlay extends SubtitleChatOverlay float dist = (float) Math.sqrt(xx * xx + yy * yy); float perc = (dist - SPEAKER_DISTANCE) / dist; - if (modeOf(type) == THINK) { + if (ChatLogic.modeOf(type) == ChatLogic.THINK) { int steps = Math.max((int) (dist / SPEAKER_DISTANCE), 2); float step = perc / steps; Area a = new Area(); @@ -737,7 +731,7 @@ public class ComicChatOverlay extends SubtitleChatOverlay */ protected Label layoutText (Graphics2D gfx, Font font, String text) { - Label label = createLabel(text); + Label label = _logic.createLabel(text); label.setFont(font); // layout in one line @@ -807,10 +801,9 @@ public class ComicChatOverlay extends SubtitleChatOverlay } @Override - protected int getDisplayDurationIndex () + protected int getDisplayDurationOffset () { - // normalize the duration returned by super. Annoying. - return super.getDisplayDurationIndex() - 1; + return 0; // we don't do any funny hackery, unlike our super class } /** @@ -841,7 +834,7 @@ public class ComicChatOverlay extends SubtitleChatOverlay @Override public void viewDidScroll (int dx, int dy) { // only system info and attention messages remain fixed, all others scroll - if ((_type == INFO) || (_type == ATTENTION)) { + if ((_type == ChatLogic.INFO) || (_type == ChatLogic.ATTENTION)) { translate(dx, dy); } } diff --git a/src/java/com/threerings/chat/SubtitleChatOverlay.java b/src/java/com/threerings/chat/SubtitleChatOverlay.java index 8eb4a540..74a5bc1d 100644 --- a/src/java/com/threerings/chat/SubtitleChatOverlay.java +++ b/src/java/com/threerings/chat/SubtitleChatOverlay.java @@ -26,7 +26,6 @@ import java.util.List; import java.awt.Color; import java.awt.Dimension; -import java.awt.Font; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Polygon; @@ -67,36 +66,34 @@ import static com.threerings.NenyaLog.log; public class SubtitleChatOverlay extends ChatOverlay implements ChangeListener, HistoryList.Observer { - /** The default chat decay parameter. See {@link #getDisplayDurationIndex}. */ - public static final int DEFAULT_CHAT_DECAY = 1; - /** * Construct a subtitle chat overlay. * * @param subtitleHeight the height of the subtitle area. */ - public SubtitleChatOverlay (CrowdContext ctx, JScrollBar bar, int subtitleHeight) + public SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar, + int subtitleHeight) { - this(ctx, bar, subtitleHeight, false, 8, 8); + this(ctx, logic, bar, subtitleHeight, false, 8, 8); } /** * Construct a comic chat overlay using all the available space for subtitling. */ - public SubtitleChatOverlay (CrowdContext ctx, JScrollBar bar) + public SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar) { - this(ctx, bar, false); + this(ctx, logic, bar, false); } - public SubtitleChatOverlay (CrowdContext ctx, JScrollBar bar, boolean full) + public SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar, boolean full) { - this(ctx, bar, 0, true, full ? 8 : 3, full ? 8 : 4); + this(ctx, logic, bar, 0, true, full ? 8 : 3, full ? 8 : 4); } - public SubtitleChatOverlay (CrowdContext ctx, JScrollBar bar, boolean full, + public SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar, boolean full, boolean overrideHistory) { - this(ctx, bar, full); + this(ctx, logic, bar, full); _overrideHistory = overrideHistory; } @@ -191,8 +188,7 @@ public class SubtitleChatOverlay extends ChatOverlay Rectangle vbounds = _target.getViewBounds(); if ((vbounds.height < 1) || (vbounds.width < 1)) { - // fuck that! - return; + return; // fuck that! } clearGlyphs(_subtitles); // we'll re-populate from the history @@ -278,10 +274,10 @@ public class SubtitleChatOverlay extends ChatOverlay /** * Shared chained constructor. */ - protected SubtitleChatOverlay (CrowdContext ctx, JScrollBar bar, int height, + protected SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar, int height, boolean fill, int xspace, int yspace) { - super(ctx); + super(ctx, logic); _scrollbar = bar; _subtitleHeight = height; @@ -532,7 +528,7 @@ public class SubtitleChatOverlay extends ChatOverlay protected int getHistorySubtitleSpacing (int index) { ChatMessage message = _history.get(index); - return getSubtitleSpacing(getType(message, true)); + return _logic.getSubtitleSpacing(getType(message, true)); } protected boolean isLaidOut () @@ -573,8 +569,7 @@ public class SubtitleChatOverlay extends ChatOverlay { // get the non-history message type... int type = getType(message, false); - - if (type != IGNORECHAT) { + if (type != ChatLogic.IGNORECHAT) { // display it now displayMessage(message, type, gfx); } @@ -583,15 +578,13 @@ public class SubtitleChatOverlay extends ChatOverlay /** * Display the message after we've decided which type it is. */ - protected void displayMessage ( - ChatMessage message, int type, Graphics2D layoutGfx) + protected void displayMessage (ChatMessage message, int type, Graphics2D layoutGfx) { // if we're in history mode, this will show up in the history and we'll rebuild our // subtitle list if and when history goes away if (isHistoryMode()) { return; } - addSubtitle(createSubtitle(message, type, layoutGfx, true)); } @@ -602,7 +595,7 @@ public class SubtitleChatOverlay extends ChatOverlay { // scroll up the old subtitles Rectangle r = rec.getBounds(); - scrollUpSubtitles(-r.height - getSubtitleSpacing(rec.getType())); + scrollUpSubtitles(-r.height - _logic.getSubtitleSpacing(rec.getType())); // put this one in place Rectangle vbounds = _target.getViewBounds(); @@ -624,7 +617,7 @@ public class SubtitleChatOverlay extends ChatOverlay // we might need to modify the textual part with translations, but we can't do that to the // message object, since other chatdisplays also get it. String text = message.message; - Tuple finfo = decodeFormat(type, message.getFormat()); + Tuple finfo = _logic.decodeFormat(type, message.getFormat()); String format = finfo.left; boolean quotes = finfo.right; @@ -641,25 +634,6 @@ public class SubtitleChatOverlay extends ChatOverlay return createSubtitle(layoutGfx, type, message.timestamp, null, 0, text, expires); } - /** - * Determines the format string and whether to use quotes based on the chat type. - */ - protected Tuple decodeFormat (int type, String format) - { - boolean quotes = true; - switch (placeOf(type)) { - // derived classes may wish to change the format here based on the place - case PLACE: - switch (modeOf(type)) { - case EMOTE: - quotes = false; - break; - } - break; - } - return Tuple.newTuple(format, quotes); - } - /** * Create a subtitle- a line of text that goes on the bottom. */ @@ -672,8 +646,8 @@ public class SubtitleChatOverlay extends ChatOverlay } Rectangle vbounds = _target.getViewBounds(); - Label label = createLabel(text); - label.setFont(getFont(type)); + Label label = _logic.createLabel(text); + label.setFont(_logic.getFont(type)); int paddedIconWidth = (icon == null) ? 0 : is.width + ICON_PADDING; label.setTargetWidth( vbounds.width - indent - paddedIconWidth - @@ -697,9 +671,9 @@ public class SubtitleChatOverlay extends ChatOverlay if (expires) { lifetime = getChatExpire(timestamp, label.getText()) - timestamp; } - Shape shape = getSubtitleShape(type, lr, r); + Shape shape = _logic.getSubtitleShape(type, lr, r); return new ChatGlyph(this, type, lifetime, r.union(shape.getBounds()), shape, icon, - iconpos, label, labelpos, getOutlineColor(type)); + iconpos, label, labelpos, _logic.getOutlineColor(type)); } /** @@ -707,7 +681,7 @@ public class SubtitleChatOverlay extends ChatOverlay */ protected long getChatExpire (long timestamp, String text) { - long[] durations = DISPLAY_DURATION_PARAMS[getDisplayDurationIndex()]; + long[] durations = _logic.getDisplayDurations(getDisplayDurationOffset()); // start the computation from the maximum of the timestamp or our last expire time long start = Math.max(timestamp, _lastExpire); // set the next expire to a time proportional to the text length @@ -718,6 +692,17 @@ public class SubtitleChatOverlay extends ChatOverlay return Math.max(timestamp + durations[1], _lastExpire); } + /** + * A hack to allow subtitle chat to display longer and comic chat to display for a normal + * duration. + */ + protected int getDisplayDurationOffset () + { + // the subtitle view adds one to bump up to the next longer display duration because we + // want subtitles to stick around longer + return 1; + } + /** * Called by a chat glyph when it has determined that it is expired. */ @@ -735,74 +720,40 @@ public class SubtitleChatOverlay extends ChatOverlay if (message instanceof TellFeedbackMessage) { if (((TellFeedbackMessage)message).isFailure()) { - return FEEDBACK; + return ChatLogic.FEEDBACK; } - return (history || isApprovedLocalType(localtype)) ? TELLFEEDBACK : IGNORECHAT; + return (history || isApprovedLocalType(localtype)) ? + ChatLogic.TELLFEEDBACK : ChatLogic.IGNORECHAT; } else if (message instanceof UserMessage) { - int type = decodeType(localtype); + int type = _logic.decodeType(localtype); // factor in the mode if (type != 0) { - type = adjustTypeByMode(((UserMessage) message).mode, type); + type = _logic.adjustTypeByMode(((UserMessage) message).mode, type); } // if we're showing from history, include specialized chat messages if (history) { - return SPECIALIZED; + return ChatLogic.SPECIALIZED; } } else if (message instanceof SystemMessage) { if (history || isApprovedLocalType(localtype)) { switch (((SystemMessage) message).attentionLevel) { case SystemMessage.INFO: - return INFO; + return ChatLogic.INFO; case SystemMessage.FEEDBACK: - return FEEDBACK; + return ChatLogic.FEEDBACK; case SystemMessage.ATTENTION: - return ATTENTION; + return ChatLogic.ATTENTION; default: log.warning("Unknown attention level for system message", "msg", message); } } - return IGNORECHAT; + return ChatLogic.IGNORECHAT; } log.warning("Skipping received message of unknown type", "msg", message); - return IGNORECHAT; - } - - /** - * Decoes the main chat type given the supplied localtype provided by the chat system. - */ - protected int decodeType (String localtype) - { - if (ChatCodes.USER_CHAT_TYPE.equals(localtype)) { - return TELL; - } else if (ChatCodes.PLACE_CHAT_TYPE.equals(localtype)) { - return PLACE; - } else { - return 0; - } - } - - /** - * Adjust the chat type based on the mode of the chat message. - */ - protected int adjustTypeByMode (int mode, int type) - { - switch (mode) { - case ChatCodes.DEFAULT_MODE: - return type | SPEAK; - case ChatCodes.EMOTE_MODE: - return type | EMOTE; - case ChatCodes.THINK_MODE: - return type | THINK; - case ChatCodes.SHOUT_MODE: - return type | SHOUT; - case ChatCodes.BROADCAST_MODE: - return BROADCAST; // broadcast always looks like broadcast - default: - return type; - } + return ChatLogic.IGNORECHAT; } /** @@ -813,19 +764,6 @@ public class SubtitleChatOverlay extends ChatOverlay return true; // we show everything, the ComicChat is a little more picky } - /** - * Get the spacing, in pixels, between the latest subtitle of the specified type and the - * previous subtitle. - */ - protected int getSubtitleSpacing (int type) - { - switch (placeOf(type)) { - // derived classes may wish to adjust subtitle spacing here based on chat type - default: - return 1; - } - } - /** * Scroll all the subtitles up by the specified amount. */ @@ -844,209 +782,6 @@ public class SubtitleChatOverlay extends ChatOverlay } } - /** - * Get the font to use for the given bubble type. - */ - protected Font getFont (int type) - { - return DEFAULT_FONT; - } - - /** - * Creates a label for the specified text. Derived classes may wish to use specialized labels. - */ - protected Label createLabel (String text) - { - return new Label(text); - } - - /** - * Get the appropriate shape for the specified type of chat. - * - * @param r the rectangle bounding the chat label. - * @param b the rectangle bounding the chat label and icon. - */ - protected Shape getSubtitleShape (int type, Rectangle r, Rectangle b) - { - int placeType = placeOf(type); - switch (placeType) { - case SPECIALIZED: - case PLACE: - return getPlaceSubtitleShape(type, r); - - case TELL: { - // lightning box! - int halfy = r.y + r.height/2; - Polygon p = new Polygon(); - p.addPoint(r.x - PAD - 2, r.y); - p.addPoint(r.x - PAD / 2 - 2, halfy); - p.addPoint(r.x - PAD, halfy); - p.addPoint(r.x - PAD / 2, r.y + r.height); - p.addPoint(r.x + r.width + PAD + 2, r.y + r.height); - p.addPoint(r.x + r.width + PAD / 2 + 2, halfy); - p.addPoint(r.x + r.width + PAD, halfy); - p.addPoint(r.x + r.width + PAD / 2, r.y); - return p; - } - - case TELLFEEDBACK: { - // reverse-lightning box! - int halfy = r.y + r.height/2; - Polygon p = new Polygon(); - p.addPoint(r.x - PAD - 2, r.y + r.height); - p.addPoint(r.x - PAD / 2 - 2, halfy); - p.addPoint(r.x - PAD, halfy); - p.addPoint(r.x - PAD / 2, r.y); - p.addPoint(r.x + r.width + PAD + 2, r.y); - p.addPoint(r.x + r.width + PAD / 2 + 2, halfy); - p.addPoint(r.x + r.width + PAD, halfy); - p.addPoint(r.x + r.width + PAD / 2, r.y + r.height); - return p; - } - - case FEEDBACK: { - // slanted box subtitle - Polygon p = new Polygon(); - p.addPoint(r.x - PAD / 2, r.y); - p.addPoint(r.x + r.width + PAD, r.y); - p.addPoint(r.x + r.width + PAD / 2, r.y + r.height); - p.addPoint(r.x - PAD, r.y + r.height); - return p; - } - - case BROADCAST: - case CONTINUATION: - case INFO: - case ATTENTION: - default: { - Rectangle grown = new Rectangle(r); - grown.grow(PAD, 0); - return new Area(grown); - } - } - } - - /** - * Computes the chat glyph outline color from the chat message type. - */ - protected Color getOutlineColor (int type) - { - switch (type) { - case BROADCAST: - return BROADCAST_COLOR; - case TELL: - return TELL_COLOR; - case TELLFEEDBACK: - return TELLFEEDBACK_COLOR; - case INFO: - return INFO_COLOR; - case FEEDBACK: - return FEEDBACK_COLOR; - case ATTENTION: - return ATTENTION_COLOR; - default: - return Color.black; - } - } - - /** - * A helper function for {@link #getSubtitleShape}. - */ - protected Shape getPlaceSubtitleShape (int type, Rectangle r) - { - switch (modeOf(type)) { - default: - case SPEAK: { - // rounded rectangle subtitle - Area a = new Area(r); - a.add(new Area(new Ellipse2D.Float(r.x - PAD, r.y, PAD * 2, r.height))); - a.add(new Area(new Ellipse2D.Float(r.x + r.width - PAD, r.y, PAD * 2, r.height))); - return a; - } - - case THINK: { - r.grow(PAD / 2, 0); - Area a = new Area(r); - int dia = 8; - int num = (int) Math.ceil(r.height / ((float) dia)); - int leftside = r.x - dia/2; - int rightside = r.x + r.width - dia/2 - 1; - - int maxh = r.height - dia; - for (int ii=0; ii < num; ii++) { - int ypos = r.y + Math.min((r.height * ii) / num, maxh); - a.add(new Area(new Ellipse2D.Float(leftside, ypos, dia, dia))); - a.add(new Area(new Ellipse2D.Float(rightside, ypos, dia, dia))); - } - return a; - } - - case SHOUT: { - r.grow(PAD / 2, 0); - Area a = new Area(r); - Polygon left = new Polygon(); - Polygon right = new Polygon(); - - int spikehei = 8; - int num = (int) Math.ceil(r.height / ((float) spikehei)); - left.addPoint(r.x, r.y); - left.addPoint(r.x - PAD, r.y + spikehei / 2); - left.addPoint(r.x, r.y + spikehei); - right.addPoint(r.x + r.width , r.y); - right.addPoint(r.x + r.width + PAD, r.y + spikehei / 2); - right.addPoint(r.x + r.width, r.y + spikehei); - - int ypos = 0; - int maxpos = r.y + r.height - spikehei + 1; - for (int ii=0; ii < num; ii++) { - int newpos = Math.min((r.height * ii) / num, maxpos); - left.translate(0, newpos - ypos); - right.translate(0, newpos - ypos); - a.add(new Area(left)); - a.add(new Area(right)); - ypos = newpos; - } - - return a; - } - - case EMOTE: { - // a box that curves inward on the left and right - r.grow(PAD, 0); - Area a = new Area(r); - a.subtract(new Area(new Ellipse2D.Float(r.x - PAD / 2, r.y, PAD, r.height))); - a.subtract(new Area(new Ellipse2D.Float(r.x + r.width - PAD / 2, r.y, PAD, r.height))); - return a; - } - } - } - - /** - * Extract the mode constant from the type value. - */ - protected int modeOf (int type) - { - return type & 0xF; - } - - /** - * Extract the place constant from the type value. - */ - protected int placeOf (int type) - { - return type & ~0xF; - } - - /** - * Get the current display duration parameters: an integer: 0 = fast, 1 = medium, 2 = long. - * See {@link #DISPLAY_DURATION_PARAMS}. - */ - protected int getDisplayDurationIndex () - { - // by default we add one, because it's assumed that we're in subtitle-only view - return DEFAULT_CHAT_DECAY + 1; - } - /** List of existing messages from our chat director. */ protected HistoryList _history; @@ -1096,82 +831,12 @@ public class SubtitleChatOverlay extends ChatOverlay /** A guess of how many history lines fit onscreen at a time. */ protected int _historyExtent; - /** Our default chat font. */ - protected static final Font DEFAULT_FONT = new Font("SansSerif", Font.PLAIN, 12); - /** A guess as to the height of a subtitle (plus spacing). */ protected static final int SUBTITLE_HEIGHT_GUESS = 16; - /** The padding in each direction around the text to the edges of a chat 'bubble'. */ - protected static final int PAD = 10; - /** The amount of space to insert between the icon and the text. */ protected static final int ICON_PADDING = 4; - /** - * Times to display chat. - * { (time per character), (min time), (max time) } - * - * Groups 0/1/2 are short/medium/long for chat bubbles, - * and groups 1/2/3 are short/medium/long for subtitles. - */ - protected static final long[][] DISPLAY_DURATION_PARAMS = { - { 125L, 10000L, 30000L }, // fastest durations - { 200L, 15000L, 40000L }, // medium (default) durations - { 275L, 20000L, 50000L }, // longest regular duration.. - { 350L, 25000L, 60000L } // grampatime! - }; - - /** Type mode code for default chat type (speaking). */ - protected static final int SPEAK = 0; - - /** Type mode code for shout chat type. */ - protected static final int SHOUT = 1; - - /** Type mode code for emote chat type. */ - protected static final int EMOTE = 2; - - /** Type mode code for think chat type. */ - protected static final int THINK = 3; - - /** Type place code for default place chat (cluster, scene). */ - protected static final int PLACE = 1 << 4; - - // 2 and 3 are skipped for legacy migration reasons - - /** Type code for a chat type that was used in some special context, - * like in a negotiation. */ - protected static final int SPECIALIZED = 4 << 4; - - /** Our internal code for tell chat. */ - protected static final int TELL = 5 << 4; - - /** Our internal code for tell feedback chat. */ - protected static final int TELLFEEDBACK = 6 << 4; - - /** Our internal code for info system messges. */ - protected static final int INFO = 7 << 4; - - /** Our internal code for feedback system messages. */ - protected static final int FEEDBACK = 8 << 4; - - /** Our internal code for attention system messages. */ - protected static final int ATTENTION = 9 << 4; - - /** Our internal code for any type of chat that is continued in a subtitle. */ - protected static final int CONTINUATION = 10 << 4; - - /** Type place code for broadcast chat type. */ - protected static final int BROADCAST = 11 << 4; - - /** Our internal code for a chat type we will ignore. */ - protected static final int IGNORECHAT = -1; - - // used to color chat bubbles - protected static final Color BROADCAST_COLOR = new Color(0x990000); - protected static final Color FEEDBACK_COLOR = new Color(0x00AA00); - protected static final Color TELL_COLOR = new Color(0x0000AA); - protected static final Color TELLFEEDBACK_COLOR = new Color(0x00AAAA); - protected static final Color INFO_COLOR = new Color(0xAAAA00); - protected static final Color ATTENTION_COLOR = new Color(0xFF5000); + /** The padding in each direction around the text to the edges of a chat 'bubble'. */ + protected static final int PAD = ChatLogic.PAD; }