Get our tools from narya.jar and its dependencies. Moved src/java into
src/main/java. Prepared to move tests out into main project. git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@1040 ed5b42cb-e716-0410-a449-f6a68f950b19
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Nenya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://code.google.com/p/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.AlphaComposite;
|
||||
import java.awt.Color;
|
||||
import java.awt.Composite;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Point;
|
||||
import java.awt.Polygon;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Area;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
import com.samskivert.swing.Label;
|
||||
import com.samskivert.swing.util.SwingUtil;
|
||||
|
||||
import com.threerings.media.MediaPanel;
|
||||
import com.threerings.media.animation.Animation;
|
||||
import com.threerings.media.image.ColorUtil;
|
||||
|
||||
import static com.threerings.NenyaLog.log;
|
||||
|
||||
/**
|
||||
* Responsible for rendering a "unit" of chat on a {@link MediaPanel}.
|
||||
*/
|
||||
public class ChatGlyph extends Animation
|
||||
{
|
||||
/** Can be used by the Overlay to mark our position in the history. */
|
||||
public int histIndex;
|
||||
|
||||
/**
|
||||
* Construct a chat glyph.
|
||||
*
|
||||
* @param owner the subtitle overlay that ownz us.
|
||||
* @param bounds the bounds of the glyph
|
||||
* @param shape the shape to draw/outline.
|
||||
* @param icon the Icon to draw inside the bubble, or null for none
|
||||
* @param iconpos the virtual coordinate at which to draw the icon (can be null if no icon)
|
||||
* @param label the Label to draw inside the bubble.
|
||||
* @param labelpos the virtual coordinate at which to draw the label
|
||||
*/
|
||||
public ChatGlyph (SubtitleChatOverlay owner, int type, long lifetime, Rectangle bounds,
|
||||
Shape shape, Icon icon, Point iconpos, Label label, Point labelpos, Color outline)
|
||||
{
|
||||
super(bounds);
|
||||
jiggleBounds();
|
||||
|
||||
_owner = owner;
|
||||
_shape = shape;
|
||||
_type = type;
|
||||
_icon = icon;
|
||||
_ipos = iconpos;
|
||||
_label = label;
|
||||
_lpos = labelpos;
|
||||
_lifetime = lifetime;
|
||||
_outline = outline;
|
||||
|
||||
if (Color.BLACK.equals(_outline)) {
|
||||
_background = Color.WHITE;
|
||||
} else {
|
||||
_background = ColorUtil.blend(Color.WHITE, _outline, .8f);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not this glyph is in dimmed mode.
|
||||
*/
|
||||
public void setDim (boolean dim)
|
||||
{
|
||||
if (_dim != dim) {
|
||||
_dim = dim;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the chat glyph with no thought to dirty rectangles or
|
||||
* restoring composites.
|
||||
*/
|
||||
public void render (Graphics2D gfx)
|
||||
{
|
||||
Object oalias = SwingUtil.activateAntiAliasing(gfx);
|
||||
gfx.setColor(getBackground());
|
||||
gfx.fill(_shape);
|
||||
|
||||
gfx.setColor(_outline);
|
||||
gfx.draw(_shape);
|
||||
SwingUtil.restoreAntiAliasing(gfx, oalias);
|
||||
|
||||
if (_icon != null) {
|
||||
_icon.paintIcon(_owner.getTarget(), gfx, _ipos.x, _ipos.y);
|
||||
}
|
||||
|
||||
gfx.setColor(Color.BLACK);
|
||||
_label.render(gfx, _lpos.x, _lpos.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the internal chat type of this bubble.
|
||||
*/
|
||||
public int getType ()
|
||||
{
|
||||
return _type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shape of this chat bubble.
|
||||
*/
|
||||
public Shape getShape ()
|
||||
{
|
||||
return _shape;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocation (int x, int y)
|
||||
{
|
||||
// we'll need these so that we can translate our complex shapes
|
||||
int dx = x - _bounds.x, dy = y - _bounds.y;
|
||||
super.setLocation(x, y);
|
||||
|
||||
if (dx != 0 || dy != 0) {
|
||||
// update our icon position, if any
|
||||
if (_ipos != null) {
|
||||
_ipos.translate(dx, dy);
|
||||
}
|
||||
|
||||
// update our label position
|
||||
_lpos.translate(dx, dy);
|
||||
|
||||
if (_shape instanceof Area) {
|
||||
((Area) _shape).transform(
|
||||
AffineTransform.getTranslateInstance(dx, dy));
|
||||
|
||||
} else if (_shape instanceof Polygon) {
|
||||
((Polygon) _shape).translate(dx, dy);
|
||||
|
||||
} else {
|
||||
log.warning("Unable to translate shape", "glyph", this, "shape", _shape + "]!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view has scrolled. The default implementation adjusts the glyph to remain
|
||||
* in the same position relative to the view rather than allowing it to scroll with the view.
|
||||
*/
|
||||
public void viewDidScroll (int dx, int dy)
|
||||
{
|
||||
translate(dx, dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to translate this glyph.
|
||||
*/
|
||||
public void translate (int dx, int dy)
|
||||
{
|
||||
setLocation(_bounds.x + dx, _bounds.y + dy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick (long tickStamp)
|
||||
{
|
||||
// set up our born stamp if we've got one
|
||||
if (_bornStamp == 0L) {
|
||||
_bornStamp = tickStamp;
|
||||
invalidate(); // make sure we're painted the first time
|
||||
}
|
||||
|
||||
// if we're not yet ready to die, do nothing
|
||||
long deathTime = _bornStamp + _lifetime;
|
||||
if (tickStamp < deathTime) {
|
||||
/* TEMPORARILY disabled blinking until we sort it out
|
||||
if (_type == SubtitleChatOverlay.ATTENTION) {
|
||||
float alphaWas = _alpha;
|
||||
long val = tickStamp - _bornStamp;
|
||||
if ((val < 3000) && (val % 1000 < 500)) {
|
||||
_alpha = 0f;
|
||||
} else {
|
||||
_alpha = ALPHA;
|
||||
}
|
||||
if (_alpha != alphaWas) {
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
long msecs = tickStamp - deathTime;
|
||||
if (msecs >= ANIM_TIME) {
|
||||
_alpha = 0f;
|
||||
// stick a fork in ourselves
|
||||
_finished = true;
|
||||
_owner.glyphExpired(this);
|
||||
|
||||
} else {
|
||||
_alpha = ALPHA * (ANIM_TIME - msecs) / ANIM_TIME;
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fastForward (long timeDelta)
|
||||
{
|
||||
if (_bornStamp > 0L) {
|
||||
_bornStamp += timeDelta;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint (Graphics2D gfx)
|
||||
{
|
||||
float alpha = _dim ? _alpha / 3 : _alpha;
|
||||
if (alpha == 0f) {
|
||||
return;
|
||||
}
|
||||
if (alpha != 1f) {
|
||||
Composite ocomp = gfx.getComposite();
|
||||
gfx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
|
||||
render(gfx);
|
||||
gfx.setComposite(ocomp);
|
||||
} else {
|
||||
render(gfx);
|
||||
}
|
||||
}
|
||||
|
||||
protected Color getBackground ()
|
||||
{
|
||||
return _background;
|
||||
}
|
||||
|
||||
/**
|
||||
* The damn repaint manager expects 1 more pixel than the shape gives, so we manually resize.
|
||||
*/
|
||||
protected void jiggleBounds ()
|
||||
{
|
||||
_bounds.setSize(_bounds.width + 1, _bounds.height + 1);
|
||||
}
|
||||
|
||||
/** The subtitle chat overlay that ownz us. */
|
||||
protected SubtitleChatOverlay _owner;
|
||||
|
||||
/** The type of chat represented by this glyph. */
|
||||
protected int _type;
|
||||
|
||||
/** The shape of this glyph. */
|
||||
protected Shape _shape;
|
||||
|
||||
/** The visual icon, or null for none. */
|
||||
protected Icon _icon;
|
||||
|
||||
/** The position at which we render our icon. */
|
||||
protected Point _ipos;
|
||||
|
||||
/** The textual label. */
|
||||
protected Label _label;
|
||||
|
||||
/** The position at which we render our text. */
|
||||
protected Point _lpos;
|
||||
|
||||
/** The alpha level that we'll render at when fading out. */
|
||||
protected float _alpha = ALPHA;
|
||||
|
||||
/** Whether we're in dimmed mode. */
|
||||
protected boolean _dim = false;
|
||||
|
||||
/** The length of the fade animation. */
|
||||
protected static final long ANIM_TIME = 800L;
|
||||
|
||||
/** The number of milliseconds to wait before this bubble will expire
|
||||
* and should begin to fade out. */
|
||||
protected long _lifetime;
|
||||
|
||||
/** The time at which we came into being on the screen. */
|
||||
protected long _bornStamp;
|
||||
|
||||
/** Our outline color. */
|
||||
protected Color _outline;
|
||||
|
||||
/** Our background color. */
|
||||
protected Color _background;
|
||||
|
||||
/** The initial alpha of all chat glyphs. */
|
||||
protected static final float ALPHA = .9f;
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Nenya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://code.google.com/p/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<String, Boolean> 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);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Nenya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://code.google.com/p/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.Point;
|
||||
import java.awt.Shape;
|
||||
import java.util.List;
|
||||
|
||||
import com.threerings.util.MessageBundle;
|
||||
import com.threerings.util.Name;
|
||||
|
||||
import com.threerings.crowd.chat.client.ChatDisplay;
|
||||
import com.threerings.crowd.util.CrowdContext;
|
||||
import com.threerings.media.VirtualMediaPanel;
|
||||
|
||||
import static com.threerings.NenyaLog.log;
|
||||
|
||||
/**
|
||||
* An abstract class that represents a chat display that can be overlayed upon another component.
|
||||
*/
|
||||
public abstract class ChatOverlay
|
||||
implements ChatDisplay
|
||||
{
|
||||
/**
|
||||
* An interface for providing information about what is under the overlay.
|
||||
*/
|
||||
public interface InfoProvider
|
||||
{
|
||||
/**
|
||||
* Get a list of Shape objects that we should attempt to avoid when laying out the chat.
|
||||
* The ChatOverlay will not modify these shape objects. The easy thing to do would be to
|
||||
* just return java.awt.Rectangle objects.
|
||||
*
|
||||
* @param speaker The username of the speaking player, or null.
|
||||
* @param high Add to this list shapes that should never be drawn on.
|
||||
*
|
||||
* @param low If non-null, add to this list shapes that can be drawn on if needed.
|
||||
*/
|
||||
public void getAvoidables (Name speaker, List<Shape> high, List<Shape> low);
|
||||
|
||||
/**
|
||||
* Get a point which is approximately the origin of the speaker, or null if unknown.
|
||||
*/
|
||||
public Point getSpeaker (Name speaker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes the chat overlay to make itself visible or invisible.
|
||||
*/
|
||||
public void setVisible (boolean visible)
|
||||
{
|
||||
// derived classes will want to do the right thing here
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the dimmed mode of the currently displaying glyphs.
|
||||
*/
|
||||
public void setDimmed (boolean dimmed)
|
||||
{
|
||||
_dimmed = dimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the target component was added to the widget hier. Should be called when we
|
||||
* wish to start displaying chat.
|
||||
*/
|
||||
public void added (VirtualMediaPanel target)
|
||||
{
|
||||
_target = target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout the chat overlay inside the previously configured target component. Should be called
|
||||
* if our component changes size.
|
||||
*/
|
||||
public abstract void layout ();
|
||||
|
||||
/**
|
||||
* Indicates that the target component was removed from the widget hier. Should be called when
|
||||
* we no longer wish to paint chat.
|
||||
*/
|
||||
public void removed ()
|
||||
{
|
||||
_target = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback from the target that the place has changed and we are to now talk to the new info
|
||||
* provider.
|
||||
*/
|
||||
public void newPlaceEntered (InfoProvider provider)
|
||||
{
|
||||
_provider = provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback indicating that we've left the place and should stop talking to a particular
|
||||
* infoprovider.
|
||||
*/
|
||||
public void placeExited ()
|
||||
{
|
||||
_provider = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the media panel on which this chat overlay is operating.
|
||||
*/
|
||||
public VirtualMediaPanel getTarget ()
|
||||
{
|
||||
return _target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when a speaker departs the chat area to allow the overlay to clean up.
|
||||
*/
|
||||
public void speakerDeparted (Name speaker)
|
||||
{
|
||||
// The regular overlay doesn't care about speakers leaving.
|
||||
}
|
||||
|
||||
/**
|
||||
* Called if our containing media panel scrolled its view.
|
||||
*/
|
||||
public void viewDidScroll (int dx, int dy)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
protected boolean isShowing ()
|
||||
{
|
||||
return (_target != null) && _target.isShowing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a string using the general client message bundle.
|
||||
*/
|
||||
protected String xlate (String message)
|
||||
{
|
||||
return xlate(_logic.getDefaultMessageBundle(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a string using the specified bundle.
|
||||
*/
|
||||
protected String xlate (String bundle, String message)
|
||||
{
|
||||
if (bundle != null) {
|
||||
MessageBundle msgb = _ctx.getMessageManager().getBundle(bundle);
|
||||
if (msgb == null) {
|
||||
log.warning("No message bundle available to translate message",
|
||||
"bundle", bundle, "message", message);
|
||||
} else {
|
||||
message = msgb.xlate(message);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
|
||||
/** The source of hints to how we layout the overlay. */
|
||||
protected InfoProvider _provider;
|
||||
|
||||
/** Whether the chat glyphs are dimmed or not. */
|
||||
protected boolean _dimmed;
|
||||
}
|
||||
@@ -0,0 +1,937 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Nenya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://code.google.com/p/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.util.List;
|
||||
import java.util.Iterator;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Point;
|
||||
import java.awt.Polygon;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.Area;
|
||||
import java.awt.geom.Ellipse2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
|
||||
import javax.swing.JScrollBar;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import com.samskivert.swing.Label;
|
||||
import com.samskivert.swing.util.SwingUtil;
|
||||
|
||||
import com.threerings.util.MessageBundle;
|
||||
import com.threerings.util.Name;
|
||||
|
||||
import com.threerings.media.image.ColorUtil;
|
||||
|
||||
import com.threerings.crowd.chat.data.ChatCodes;
|
||||
import com.threerings.crowd.chat.data.ChatMessage;
|
||||
import com.threerings.crowd.chat.data.UserMessage;
|
||||
import com.threerings.crowd.util.CrowdContext;
|
||||
|
||||
import static com.threerings.NenyaLog.log;
|
||||
|
||||
/**
|
||||
* Implements comic chat in the yohoho client.
|
||||
*/
|
||||
public class ComicChatOverlay extends SubtitleChatOverlay
|
||||
{
|
||||
/**
|
||||
* Construct a comic chat overlay.
|
||||
*
|
||||
* @param subtitleHeight the amount of vertical space to use for subtitles.
|
||||
*/
|
||||
public ComicChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar historyBar,
|
||||
int subtitleHeight)
|
||||
{
|
||||
super(ctx, logic, historyBar, subtitleHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void newPlaceEntered (InfoProvider provider)
|
||||
{
|
||||
_newPlacePoint = _ctx.getChatDirector().getHistory().size();
|
||||
super.newPlaceEntered(provider);
|
||||
|
||||
// and clear place-oriented bubbles
|
||||
clearBubbles(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void layout ()
|
||||
{
|
||||
clearBubbles(true); // these will get repopulated from the history
|
||||
super.layout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed ()
|
||||
{
|
||||
// we do this before calling super because we want our target to
|
||||
// be around for the bubble clearing
|
||||
clearBubbles(true);
|
||||
|
||||
super.removed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear ()
|
||||
{
|
||||
super.clear();
|
||||
|
||||
clearBubbles(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void viewDidScroll (int dx, int dy)
|
||||
{
|
||||
super.viewDidScroll(dx, dy);
|
||||
viewDidScroll(_bubbles, dx, dy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDimmed (boolean dimmed)
|
||||
{
|
||||
super.setDimmed(dimmed);
|
||||
updateDimmed(_bubbles);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void speakerDeparted (Name speaker)
|
||||
{
|
||||
for (Iterator<BubbleGlyph> iter = _bubbles.iterator(); iter.hasNext();) {
|
||||
BubbleGlyph rec = iter.next();
|
||||
if (rec.isSpeaker(speaker)) {
|
||||
_target.abortAnimation(rec);
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void historyUpdated (int adjustment)
|
||||
{
|
||||
_newPlacePoint -= adjustment;
|
||||
super.historyUpdated(adjustment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear chat bubbles, either all of them or just the place-oriented ones.
|
||||
*/
|
||||
protected void clearBubbles (boolean all)
|
||||
{
|
||||
for (Iterator<BubbleGlyph> iter = _bubbles.iterator(); iter.hasNext();) {
|
||||
ChatGlyph rec = iter.next();
|
||||
if (all || isPlaceOrientedType(rec.getType())) {
|
||||
_target.abortAnimation(rec);
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldShowFromHistory (ChatMessage msg, int index)
|
||||
{
|
||||
// only show if the message was received since we last entered
|
||||
// a new place, or if it's place-less chat.
|
||||
return ((index >= _newPlacePoint) ||
|
||||
(! isPlaceOrientedType(getType(msg, false))));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isApprovedLocalType (String localtype)
|
||||
{
|
||||
if (ChatCodes.PLACE_CHAT_TYPE.equals(localtype) ||
|
||||
ChatCodes.USER_CHAT_TYPE.equals(localtype)) {
|
||||
return true;
|
||||
}
|
||||
log.debug("Ignoring non-standard system/feedback chat", "localtype", localtype);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the type of chat place-oriented.
|
||||
*/
|
||||
protected boolean isPlaceOrientedType (int type)
|
||||
{
|
||||
return (ChatLogic.placeOf(type)) == ChatLogic.PLACE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void displayMessage (ChatMessage message, int type, Graphics2D layoutGfx)
|
||||
{
|
||||
// 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 (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 ChatLogic.PLACE: {
|
||||
UserMessage msg = (UserMessage) message;
|
||||
Point speakerloc = _provider.getSpeaker(msg.speaker);
|
||||
if (speakerloc == null) {
|
||||
log.warning("ChatOverlay.InfoProvider doesn't know the speaker!",
|
||||
"speaker", msg.speaker, "type", type);
|
||||
return;
|
||||
}
|
||||
|
||||
// emotes won't actually have tails, but we do want them to appear near the pirate
|
||||
if (ChatLogic.modeOf(type) == ChatLogic.EMOTE) {
|
||||
text = xlate(
|
||||
MessageBundle.tcompose("m.emote_format", msg.getSpeakerDisplayName())) +
|
||||
" " + text;
|
||||
}
|
||||
|
||||
// try to add all the text as a bubble, but if it doesn't
|
||||
// fit, add some of it and 'continue' the rest in a subtitle.
|
||||
String leftover = text;
|
||||
for (int ii = 1; ii < 7; ii++) {
|
||||
String bubtext = splitNear(text, text.length() / ii);
|
||||
if (createBubble(layoutGfx, type, msg.timestamp,
|
||||
bubtext + ((ii > 1) ? "..." : ""), msg.speaker, speakerloc)) {
|
||||
|
||||
leftover = text.substring(bubtext.length());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (leftover.length() > 0 && !isHistoryMode()) {
|
||||
String ltext = MessageBundle.tcompose("m.continue_format", msg.speaker);
|
||||
ltext = xlate(ltext) + " \"" + leftover + "\"";
|
||||
addSubtitle(createSubtitle(layoutGfx, ChatLogic.CONTINUATION,
|
||||
message.timestamp, null, 0, ltext, true));
|
||||
}
|
||||
return; // EXIT
|
||||
}
|
||||
}
|
||||
|
||||
super.displayMessage(message, type, layoutGfx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the text at the space nearest the specified location.
|
||||
*/
|
||||
protected String splitNear (String text, int pos)
|
||||
{
|
||||
if (pos >= text.length()) {
|
||||
return text;
|
||||
}
|
||||
|
||||
int forward = text.indexOf(' ', pos);
|
||||
int backward = text.lastIndexOf(' ', pos);
|
||||
|
||||
int newpos = (Math.abs(pos - forward) < Math.abs(pos - backward)) ? forward : backward;
|
||||
|
||||
// if we couldn't find a decent place to split, just do it wherever
|
||||
if (newpos == -1) {
|
||||
newpos = pos;
|
||||
|
||||
} else {
|
||||
// actually split the space onto the first part
|
||||
newpos++;
|
||||
}
|
||||
return text.substring(0, newpos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a chat bubble with the specified type and text.
|
||||
*
|
||||
* @param speakerloc if non-null, specifies that a tail should be added which points to that
|
||||
* location.
|
||||
* @return true if we successfully laid out the bubble
|
||||
*/
|
||||
protected boolean createBubble (
|
||||
Graphics2D gfx, int type, long timestamp, String text, Name speaker, Point speakerloc)
|
||||
{
|
||||
Label label = layoutText(gfx, _logic.getFont(type), text);
|
||||
label.setAlignment(Label.CENTER);
|
||||
gfx.dispose();
|
||||
|
||||
// get the size of the new bubble
|
||||
Rectangle r = getBubbleSize(type, label.getSize());
|
||||
|
||||
// get the user's old bubbles.
|
||||
List<BubbleGlyph> oldbubs = getAndExpireBubbles(speaker);
|
||||
int numold = oldbubs.size();
|
||||
|
||||
Rectangle placer, bigR = null;
|
||||
if (numold == 0) {
|
||||
placer = new Rectangle(r);
|
||||
positionRectIdeally(placer, type, speakerloc);
|
||||
|
||||
} else {
|
||||
// get a big rectangle encompassing the old and new
|
||||
bigR = getRectWithOlds(r, oldbubs);
|
||||
placer = new Rectangle(bigR);
|
||||
|
||||
positionRectIdeally(placer, type, speakerloc);
|
||||
// we actually try to place midway between ideal and old
|
||||
// and adjust up half the height of the new boy
|
||||
placer.setLocation((placer.x + bigR.x) / 2,
|
||||
(placer.y + (bigR.y - (r.height / 2))) / 2);
|
||||
}
|
||||
|
||||
// then look for a place nearby where it will fit
|
||||
// (making sure we only put it in the area above the subtitles)
|
||||
Rectangle vbounds = new Rectangle(_target.getViewBounds());
|
||||
vbounds.height -= _subtitleHeight;
|
||||
if (!SwingUtil.positionRect(placer, vbounds, getAvoidList(speaker))) {
|
||||
// we couldn't fit the bubble!
|
||||
return false;
|
||||
}
|
||||
|
||||
// now 'placer' is positioned reasonably.
|
||||
if (0 == numold) {
|
||||
r.setLocation(placer.x, placer.y);
|
||||
|
||||
} else {
|
||||
int dx = placer.x - bigR.x;
|
||||
int dy = placer.y - bigR.y;
|
||||
for (int ii=0; ii < numold; ii++) {
|
||||
BubbleGlyph bub = oldbubs.get(ii);
|
||||
bub.removeTail();
|
||||
Rectangle ob = bub.getBubbleBounds();
|
||||
// recenter the translated bub within placer's width..
|
||||
int xadjust = dx - (ob.x - bigR.x) +
|
||||
(placer.width - ob.width) / 2;
|
||||
bub.translate(xadjust, dy);
|
||||
}
|
||||
|
||||
// and position 'r' in the right place relative to 'placer'
|
||||
r.setLocation(placer.x + (placer.width - r.width) / 2,
|
||||
placer.y + placer.height - r.height);
|
||||
}
|
||||
|
||||
Shape shape = getBubbleShape(type, r);
|
||||
Shape full = shape;
|
||||
|
||||
// if we have a tail, the full area should include that.
|
||||
if (speakerloc != null) {
|
||||
Area area = new Area(getTail(type, r, speakerloc));
|
||||
area.add(new Area(shape));
|
||||
full = area;
|
||||
}
|
||||
|
||||
// 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, _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
|
||||
int numbubs = _bubbles.size();
|
||||
for (int ii=0; ii < numbubs; ii++) {
|
||||
_bubbles.get(ii).setAgeLevel(numbubs - ii - 1);
|
||||
}
|
||||
|
||||
return true; // success!
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the size of the chat bubble based on the dimensions of the label and the type of
|
||||
* chat. It will be turned into a shape later, but we manipulate it for a while as just a
|
||||
* rectangle (which are easier to move about and do intersection tests with, and besides the
|
||||
* Shape interface has no way to translate).
|
||||
*/
|
||||
protected Rectangle getBubbleSize (int type, Dimension d)
|
||||
{
|
||||
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));
|
||||
|
||||
default:
|
||||
return new Rectangle(d.width + (PAD * 2), d.height + (PAD * 2));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Position the label based on the type.
|
||||
*/
|
||||
protected Point adjustLabel (int type, Point labelpos)
|
||||
{
|
||||
switch (ChatLogic.modeOf(type)) {
|
||||
case ChatLogic.SHOUT:
|
||||
case ChatLogic.EMOTE:
|
||||
case ChatLogic.THINK:
|
||||
labelpos.translate(PAD * 2, PAD * 2);
|
||||
break;
|
||||
|
||||
default:
|
||||
labelpos.translate(PAD, PAD);
|
||||
break;
|
||||
}
|
||||
|
||||
return labelpos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position the rectangle in its ideal location given the type and speaker positon (which may
|
||||
* be null).
|
||||
*/
|
||||
protected void positionRectIdeally (Rectangle r, int type, Point speaker)
|
||||
{
|
||||
if (speaker != null) {
|
||||
// center it on top of the speaker (it'll be moved..)
|
||||
r.setLocation(speaker.x - (r.width / 2),
|
||||
speaker.y - (r.height / 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise we have different areas for different types
|
||||
Rectangle vbounds = _target.getViewBounds();
|
||||
switch (ChatLogic.placeOf(type)) {
|
||||
case ChatLogic.INFO:
|
||||
case ChatLogic.ATTENTION:
|
||||
// upper left
|
||||
r.setLocation(vbounds.x + BUBBLE_SPACING,
|
||||
vbounds.y + BUBBLE_SPACING);
|
||||
return;
|
||||
|
||||
case ChatLogic.PLACE:
|
||||
log.warning("Got to a place where I shouldn't get!");
|
||||
break; // fall through
|
||||
}
|
||||
|
||||
// put it in the center..
|
||||
log.debug("Unhandled chat type in getLocation()", "type", type);
|
||||
r.setLocation((vbounds.width - r.width) / 2,
|
||||
(vbounds.height - r.height) / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a rectangle based on the old bubbles, but with room for the new one.
|
||||
*/
|
||||
protected Rectangle getRectWithOlds (Rectangle r, List<BubbleGlyph> oldbubs)
|
||||
{
|
||||
int n = oldbubs.size();
|
||||
// if no old bubs, just return the new one.
|
||||
if (n == 0) {
|
||||
return r;
|
||||
}
|
||||
|
||||
// otherwise, encompass all the oldies
|
||||
Rectangle bigR = null;
|
||||
for (int ii=0; ii < n; ii++) {
|
||||
BubbleGlyph bub = oldbubs.get(ii);
|
||||
if (ii == 0) {
|
||||
bigR = bub.getBubbleBounds();
|
||||
} else {
|
||||
bigR = bigR.union(bub.getBubbleBounds());
|
||||
}
|
||||
}
|
||||
|
||||
// and add space for the new boy
|
||||
bigR.width = Math.max(bigR.width, r.width);
|
||||
bigR.height += r.height;
|
||||
|
||||
return bigR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate shape for the specified type of chat.
|
||||
*/
|
||||
protected Shape getBubbleShape (int type, Rectangle r)
|
||||
{
|
||||
switch (ChatLogic.placeOf(type)) {
|
||||
case ChatLogic.INFO:
|
||||
case ChatLogic.ATTENTION:
|
||||
// boring rectangle wrapped in an Area for translation
|
||||
return new Area(r);
|
||||
}
|
||||
|
||||
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 ChatLogic.SHOUT: {
|
||||
// spikey balloon
|
||||
Polygon left = new Polygon(), right = new Polygon();
|
||||
Polygon top = new Polygon(), bot = new Polygon();
|
||||
|
||||
int x = r.x + PAD;
|
||||
int y = r.y + PAD;
|
||||
int wid = r.width - PAD * 2;
|
||||
int hei = r.height - PAD * 2;
|
||||
Area a = new Area(new Rectangle(x, y, wid, hei));
|
||||
int spikebase = 10;
|
||||
int cornbase = spikebase*3/4;
|
||||
|
||||
// configure spikes to the left and right sides
|
||||
left.addPoint(x, y);
|
||||
left.addPoint(x - PAD, y + spikebase/2);
|
||||
left.addPoint(x, y + spikebase);
|
||||
right.addPoint(x + wid, y);
|
||||
right.addPoint(x + wid + PAD, y + spikebase/2);
|
||||
right.addPoint(x + wid, y + spikebase);
|
||||
|
||||
// add the left and right side spikes
|
||||
int ypos = 0;
|
||||
int ahei = hei - cornbase;
|
||||
int maxpos = ahei - spikebase + 1;
|
||||
int numvert = (int) Math.ceil(ahei / ((float) spikebase));
|
||||
for (int ii=0; ii < numvert; ii++) {
|
||||
int newpos = cornbase/2 +
|
||||
Math.min((ahei * ii) / numvert, maxpos);
|
||||
left.translate(0, newpos - ypos);
|
||||
right.translate(0, newpos - ypos);
|
||||
a.add(new Area(left));
|
||||
a.add(new Area(right));
|
||||
ypos = newpos;
|
||||
}
|
||||
|
||||
// configure spikes for the top and bottom
|
||||
top.addPoint(x, y);
|
||||
top.addPoint(x + spikebase/2, y - PAD);
|
||||
top.addPoint(x + spikebase, y);
|
||||
bot.addPoint(x, y + hei);
|
||||
bot.addPoint(x + spikebase/2, y + hei + PAD);
|
||||
bot.addPoint(x + spikebase, y + hei);
|
||||
|
||||
// add top and bottom spikes
|
||||
int xpos = 0;
|
||||
int awid = wid - cornbase;
|
||||
maxpos = awid - spikebase + 1;
|
||||
int numhorz = (int) Math.ceil(awid / ((float) spikebase));
|
||||
for (int ii=0; ii < numhorz; ii++) {
|
||||
int newpos = cornbase/2 +
|
||||
Math.min((awid * ii) / numhorz, maxpos);
|
||||
top.translate(newpos - xpos, 0);
|
||||
bot.translate(newpos - xpos, 0);
|
||||
a.add(new Area(top));
|
||||
a.add(new Area(bot));
|
||||
xpos = newpos;
|
||||
}
|
||||
|
||||
// and lets also add corner spikes
|
||||
Polygon corner = new Polygon();
|
||||
corner.addPoint(x, y + cornbase);
|
||||
corner.addPoint(x - PAD + 2, y - PAD + 2);
|
||||
corner.addPoint(x + cornbase, y);
|
||||
a.add(new Area(corner));
|
||||
|
||||
corner.reset();
|
||||
corner.addPoint(x + wid - cornbase, y);
|
||||
corner.addPoint(x + wid + PAD - 2, y - PAD + 2);
|
||||
corner.addPoint(x + wid, y + cornbase);
|
||||
a.add(new Area(corner));
|
||||
|
||||
corner.reset();
|
||||
corner.addPoint(x + wid, y + hei - cornbase);
|
||||
corner.addPoint(x + wid + PAD - 2, y + hei + PAD - 2);
|
||||
corner.addPoint(x + wid - cornbase, y + hei);
|
||||
a.add(new Area(corner));
|
||||
|
||||
corner.reset();
|
||||
corner.addPoint(x + cornbase, y + hei);
|
||||
corner.addPoint(x - PAD + 2, y + hei + PAD - 2);
|
||||
corner.addPoint(x, y + hei - cornbase);
|
||||
a.add(new Area(corner));
|
||||
// grunt work!
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
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)));
|
||||
a.subtract(new Area(new Ellipse2D.Float(r.x, r.y + r.height - PAD, r.width, PAD * 2)));
|
||||
a.subtract(new Area(new Ellipse2D.Float(r.x - PAD, r.y, PAD * 2, r.height)));
|
||||
a.subtract(new Area(new Ellipse2D.Float(r.x + r.width - PAD, r.y, PAD * 2, r.height)));
|
||||
return a;
|
||||
}
|
||||
|
||||
case ChatLogic.THINK: {
|
||||
// cloudy balloon!
|
||||
int x = r.x + PAD;
|
||||
int y = r.y + PAD;
|
||||
int wid = r.width - PAD * 2;
|
||||
int hei = r.height - PAD * 2;
|
||||
Area a = new Area(new Rectangle(x, y, wid, hei));
|
||||
|
||||
// small circles on the left and right
|
||||
int dia = 12;
|
||||
int numvert = (int) Math.ceil(hei / ((float) dia));
|
||||
int leftside = x - dia/2;
|
||||
int rightside = x + wid - (dia/2) - 1;
|
||||
int maxh = hei - dia;
|
||||
for (int ii=0; ii < numvert; ii++) {
|
||||
int ypos = y + Math.min((hei * ii) / numvert, maxh);
|
||||
a.add(new Area(new Ellipse2D.Float(leftside, ypos, dia, dia)));
|
||||
a.add(new Area(new Ellipse2D.Float(rightside, ypos, dia, dia)));
|
||||
}
|
||||
|
||||
// larger ovals on the top and bottom
|
||||
dia = 16;
|
||||
int numhorz = (int) Math.ceil(wid / ((float) dia));
|
||||
int topside = y - dia/3;
|
||||
int botside = y + hei - (dia/3) - 1;
|
||||
int maxw = wid - dia;
|
||||
for (int ii=0; ii < numhorz; ii++) {
|
||||
int xpos = x + Math.min((wid * ii) / numhorz, maxw);
|
||||
a.add(new Area(new Ellipse2D.Float(xpos, topside, dia, dia*2/3)));
|
||||
a.add(new Area(new Ellipse2D.Float(xpos, botside, dia, dia*2/3)));
|
||||
}
|
||||
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
// fall back to subtitle shape
|
||||
return _logic.getSubtitleShape(type, r, r);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tail to the specified rectangular area from the speaker point.
|
||||
*/
|
||||
protected Shape getTail (int type, Rectangle r, Point speaker)
|
||||
{
|
||||
// emotes don't actually have tails
|
||||
if (ChatLogic.modeOf(type) == ChatLogic.EMOTE) {
|
||||
return new Area(); // empty shape
|
||||
}
|
||||
|
||||
int midx = r.x + (r.width / 2);
|
||||
int midy = r.y + (r.height / 2);
|
||||
|
||||
// we actually want to start about SPEAKER_DISTANCE away from the
|
||||
// speaker
|
||||
int xx = speaker.x - midx;
|
||||
int yy = speaker.y - midy;
|
||||
float dist = (float) Math.sqrt(xx * xx + yy * yy);
|
||||
float perc = (dist - SPEAKER_DISTANCE) / dist;
|
||||
|
||||
if (ChatLogic.modeOf(type) == ChatLogic.THINK) {
|
||||
int steps = Math.max((int) (dist / SPEAKER_DISTANCE), 2);
|
||||
float step = perc / steps;
|
||||
Area a = new Area();
|
||||
for (int ii=0; ii < steps; ii++, perc -= step) {
|
||||
int radius = Math.min(SPEAKER_DISTANCE / 2 - 1, ii + 2);
|
||||
a.add(new Area(new Ellipse2D.Float(
|
||||
(int) ((1 - perc) * midx + perc * speaker.x) + perc * radius,
|
||||
(int) ((1 - perc) * midy + perc * speaker.y) + perc * radius,
|
||||
radius * 2, radius * 2)));
|
||||
}
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
// ELSE draw a triangular tail shape
|
||||
Polygon p = new Polygon();
|
||||
p.addPoint((int) ((1 - perc) * midx + perc * speaker.x),
|
||||
(int) ((1 - perc) * midy + perc * speaker.y));
|
||||
|
||||
if (Math.abs(speaker.x - midx) > Math.abs(speaker.y - midy)) {
|
||||
int x;
|
||||
if (midx > speaker.x) {
|
||||
x = r.x + PAD;
|
||||
} else {
|
||||
x = r.x + r.width - PAD;
|
||||
}
|
||||
p.addPoint(x, midy - (TAIL_WIDTH / 2));
|
||||
p.addPoint(x, midy + (TAIL_WIDTH / 2));
|
||||
|
||||
} else {
|
||||
int y;
|
||||
if (midy > speaker.y) {
|
||||
y = r.y + PAD;
|
||||
} else {
|
||||
y = r.y + r.height - PAD;
|
||||
}
|
||||
p.addPoint(midx - (TAIL_WIDTH / 2), y);
|
||||
p.addPoint(midx + (TAIL_WIDTH / 2), y);
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expire a bubble, if necessary, and return the old bubbles for the specified speaker.
|
||||
*/
|
||||
protected List<BubbleGlyph> getAndExpireBubbles (Name speaker)
|
||||
{
|
||||
int num = _bubbles.size();
|
||||
|
||||
// first, get all the old bubbles belonging to the user
|
||||
List<BubbleGlyph> oldbubs = Lists.newArrayList();
|
||||
if (speaker != null) {
|
||||
for (int ii=0; ii < num; ii++) {
|
||||
BubbleGlyph bub = _bubbles.get(ii);
|
||||
if (bub.isSpeaker(speaker)) {
|
||||
oldbubs.add(bub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// see if we need to expire this user's oldest bubble
|
||||
if (oldbubs.size() >= MAX_BUBBLES_PER_USER) {
|
||||
BubbleGlyph bub = oldbubs.remove(0);
|
||||
_bubbles.remove(bub);
|
||||
_target.abortAnimation(bub);
|
||||
|
||||
// or some other old bubble
|
||||
} else if (num >= MAX_BUBBLES) {
|
||||
_target.abortAnimation(_bubbles.remove(0));
|
||||
}
|
||||
|
||||
// return the speaker's old bubbles
|
||||
return oldbubs;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void glyphExpired (ChatGlyph glyph)
|
||||
{
|
||||
super.glyphExpired(glyph);
|
||||
_bubbles.remove(glyph);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a label formatted as close to the golden ratio as possible for the specified text and
|
||||
* given the standard padding we use on all bubbles.
|
||||
*/
|
||||
protected Label layoutText (Graphics2D gfx, Font font, String text)
|
||||
{
|
||||
Label label = _logic.createLabel(text);
|
||||
label.setFont(font);
|
||||
|
||||
// layout in one line
|
||||
Rectangle vbounds = _target.getViewBounds();
|
||||
label.setTargetWidth(vbounds.width - PAD * 2);
|
||||
label.layout(gfx);
|
||||
Dimension d = label.getSize();
|
||||
|
||||
// if the label is wide enough, try to split the text into multiple
|
||||
// lines
|
||||
if (d.width > MINIMUM_SPLIT_WIDTH) {
|
||||
int targetheight = getGoldenLabelHeight(d);
|
||||
if (targetheight > 1) {
|
||||
label.setTargetHeight(targetheight * d.height);
|
||||
label.layout(gfx);
|
||||
}
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the specified label dimensions, attempt to find the height that will give us the
|
||||
* width/height ratio that is closest to the golden ratio.
|
||||
*/
|
||||
protected int getGoldenLabelHeight (Dimension d)
|
||||
{
|
||||
// compute the ratio of the one line (addin' the paddin')
|
||||
double lastratio = ((double) d.width + (PAD * 2)) /
|
||||
((double) d.height + (PAD * 2));
|
||||
|
||||
// now try increasing the # of lines and seeing if we get closer to the golden ratio
|
||||
for (int lines=2; true; lines++) {
|
||||
double ratio = ((double) (d.width / lines) + (PAD * 2)) /
|
||||
((double) (d.height * lines) + (PAD * 2));
|
||||
if (Math.abs(ratio - GOLDEN) < Math.abs(lastratio - GOLDEN)) {
|
||||
// we're getting closer
|
||||
lastratio = ratio;
|
||||
} else {
|
||||
// we're getting further away, the last one was the one we want
|
||||
return lines - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of rectangular areas that we should avoid while laying out a bubble for the
|
||||
* specified speaker.
|
||||
*/
|
||||
protected List<Shape> getAvoidList (Name speaker)
|
||||
{
|
||||
List<Shape> avoid = Lists.newArrayList();
|
||||
if (_provider == null) {
|
||||
return avoid;
|
||||
}
|
||||
|
||||
// for now we don't accept low-priority avoids
|
||||
_provider.getAvoidables(speaker, avoid, null);
|
||||
|
||||
// add the existing chatbub non-tail areas from other speakers
|
||||
for (BubbleGlyph bub : _bubbles) {
|
||||
if (!bub.isSpeaker(speaker)) {
|
||||
avoid.add(bub.getBubbleTerritory());
|
||||
}
|
||||
}
|
||||
|
||||
return avoid;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDisplayDurationOffset ()
|
||||
{
|
||||
return 0; // we don't do any funny hackery, unlike our super class
|
||||
}
|
||||
|
||||
/**
|
||||
* A glyph of a particlar chat bubble
|
||||
*/
|
||||
protected static class BubbleGlyph extends ChatGlyph
|
||||
{
|
||||
/**
|
||||
* Construct a chat bubble glyph.
|
||||
*
|
||||
* @param sansTail the chat bubble shape without the tail.
|
||||
*/
|
||||
public BubbleGlyph (
|
||||
SubtitleChatOverlay owner, int type, long lifetime, Shape shape, Label label,
|
||||
Point labelpos, Shape sansTail, Name speaker, Color outline) {
|
||||
super(owner, type, lifetime, shape.getBounds(), shape, null, null,
|
||||
label, labelpos, outline);
|
||||
|
||||
_sansTail = sansTail;
|
||||
_speaker = speaker;
|
||||
}
|
||||
|
||||
public void setAgeLevel (int agelevel) {
|
||||
_agelevel = agelevel;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void viewDidScroll (int dx, int dy) {
|
||||
// only system info and attention messages remain fixed, all others scroll
|
||||
if ((_type == ChatLogic.INFO) || (_type == ChatLogic.ATTENTION)) {
|
||||
translate(dx, dy);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Color getBackground () {
|
||||
if (_background == Color.WHITE) {
|
||||
return BACKGROUNDS[_agelevel];
|
||||
} else {
|
||||
return _background;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the screen real estate that this bubble has reserved and doesn't want to let any
|
||||
* other bubbles take.
|
||||
*/
|
||||
public Shape getBubbleTerritory () {
|
||||
Rectangle bounds = getBubbleBounds();
|
||||
bounds.grow(BUBBLE_SPACING, BUBBLE_SPACING);
|
||||
return bounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bounds of this bubble, sans tail space.
|
||||
*/
|
||||
public Rectangle getBubbleBounds () {
|
||||
return _sansTail.getBounds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the specified player the speaker of this bubble?
|
||||
*/
|
||||
public boolean isSpeaker (Name player) {
|
||||
return (_speaker != null) && _speaker.equals(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the tail on this bubble, if any.
|
||||
*/
|
||||
public void removeTail () {
|
||||
invalidate();
|
||||
_shape = _sansTail;
|
||||
_bounds = _shape.getBounds();
|
||||
jiggleBounds();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/** The shape of this chat bubble, without the tail. */
|
||||
protected Shape _sansTail;
|
||||
|
||||
/** The name of the speaker. */
|
||||
protected Name _speaker;
|
||||
|
||||
/** The age level of the bubble, used to pick the background color. */
|
||||
protected int _agelevel = 0;
|
||||
}
|
||||
|
||||
/** The place in our history at which we last entered a new place. */
|
||||
protected int _newPlacePoint = 0;
|
||||
|
||||
/** The currently displayed bubble areas. */
|
||||
protected List<BubbleGlyph> _bubbles = Lists.newArrayList();
|
||||
|
||||
/** The minimum width of a bubble's label before we consider splitting lines. */
|
||||
protected static final int MINIMUM_SPLIT_WIDTH = 90;
|
||||
|
||||
/** The golden ratio. */
|
||||
protected static final double GOLDEN = (1.0d + Math.sqrt(5.0d)) / 2.0d;
|
||||
|
||||
/** The space we force between adjacent bubbles. */
|
||||
protected static final int BUBBLE_SPACING = 15;
|
||||
|
||||
/** The distance to stay from the speaker. */
|
||||
protected static final int SPEAKER_DISTANCE = 20;
|
||||
|
||||
/** The width of the end of the tail. */
|
||||
protected static final int TAIL_WIDTH = 12;
|
||||
|
||||
/** The maximum number of bubbles to show. */
|
||||
protected static final int MAX_BUBBLES = 8;
|
||||
|
||||
/** The maximum number of bubbles to show per user. */
|
||||
protected static final int MAX_BUBBLES_PER_USER = 3;
|
||||
|
||||
/** The background colors to use when drawing bubbles. */
|
||||
protected static final Color[] BACKGROUNDS = new Color[MAX_BUBBLES];
|
||||
static {
|
||||
Color yellowy = new Color(0xdd, 0xdd, 0x6a);
|
||||
Color blackish = new Color(0xcccccc);
|
||||
|
||||
float steps = (MAX_BUBBLES - 1) / 2;
|
||||
for (int ii=0; ii < MAX_BUBBLES / 2; ii++) {
|
||||
BACKGROUNDS[ii] = ColorUtil.blend(Color.white, yellowy, (steps - ii) / steps);
|
||||
}
|
||||
for (int ii= MAX_BUBBLES / 2; ii < MAX_BUBBLES; ii++) {
|
||||
BACKGROUNDS[ii] = ColorUtil.blend(blackish, yellowy, (ii - steps) / steps);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,836 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Nenya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://code.google.com/p/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.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.Shape;
|
||||
import javax.swing.BoundedRangeModel;
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.JScrollBar;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import com.samskivert.swing.Label;
|
||||
import com.samskivert.util.Tuple;
|
||||
|
||||
import com.threerings.media.VirtualMediaPanel;
|
||||
import com.threerings.util.MessageBundle;
|
||||
|
||||
import com.threerings.crowd.chat.client.HistoryList;
|
||||
import com.threerings.crowd.chat.data.ChatMessage;
|
||||
import com.threerings.crowd.chat.data.SystemMessage;
|
||||
import com.threerings.crowd.chat.data.TellFeedbackMessage;
|
||||
import com.threerings.crowd.chat.data.UserMessage;
|
||||
import com.threerings.crowd.util.CrowdContext;
|
||||
|
||||
import static com.threerings.NenyaLog.log;
|
||||
|
||||
/**
|
||||
* Implements subtitle chat.
|
||||
*/
|
||||
public class SubtitleChatOverlay extends ChatOverlay
|
||||
implements ChangeListener, HistoryList.Observer
|
||||
{
|
||||
/**
|
||||
* Construct a subtitle chat overlay.
|
||||
*
|
||||
* @param subtitleHeight the height of the subtitle area.
|
||||
*/
|
||||
public SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar,
|
||||
int subtitleHeight)
|
||||
{
|
||||
this(ctx, logic, bar, subtitleHeight, false, 8, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a comic chat overlay using all the available space for subtitling.
|
||||
*/
|
||||
public SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar)
|
||||
{
|
||||
this(ctx, logic, bar, false);
|
||||
}
|
||||
|
||||
public SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar, boolean full)
|
||||
{
|
||||
this(ctx, logic, bar, 0, true, full ? 8 : 3, full ? 8 : 4);
|
||||
}
|
||||
|
||||
public SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar, boolean full,
|
||||
boolean overrideHistory)
|
||||
{
|
||||
this(ctx, logic, bar, full);
|
||||
_overrideHistory = overrideHistory;
|
||||
}
|
||||
|
||||
// from interface HistoryList.Observer
|
||||
public void historyUpdated (int adjustment)
|
||||
{
|
||||
if (adjustment != 0) {
|
||||
for (int ii = 0, nn = _showingHistory.size(); ii < nn; ii++) {
|
||||
ChatGlyph cg = _showingHistory.get(ii);
|
||||
cg.histIndex -= adjustment;
|
||||
}
|
||||
// some history entries were deleted, we need to re-figure the history scrollbar action
|
||||
resetHistoryOffset();
|
||||
}
|
||||
|
||||
if (isLaidOut() && isHistoryMode()) {
|
||||
int val = _historyModel.getValue();
|
||||
updateHistBar(val - adjustment);
|
||||
|
||||
// only repaint if we need to
|
||||
if ((val != _historyModel.getValue()) || (adjustment != 0) || !_histOffsetFinal) {
|
||||
figureCurrentHistory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// documentation inherited from interface ChangeListener
|
||||
public void stateChanged (ChangeEvent e)
|
||||
{
|
||||
// the scrollbar has changed.
|
||||
if (!_settingBar) {
|
||||
figureCurrentHistory();
|
||||
}
|
||||
}
|
||||
|
||||
// documentation inherited from superinterface ChatDisplay
|
||||
public void clear ()
|
||||
{
|
||||
clearGlyphs(_subtitles);
|
||||
}
|
||||
|
||||
// documentation inherited from superinterface ChatDisplay
|
||||
public boolean displayMessage (ChatMessage message, boolean alreadyDisplayed)
|
||||
{
|
||||
// nothing doing if we've not been laid out
|
||||
if (!isLaidOut()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// possibly display it now
|
||||
Graphics2D gfx = getTargetGraphics();
|
||||
if (gfx != null) {
|
||||
displayMessage(message, gfx); // display it
|
||||
gfx.dispose(); // clean up
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void viewDidScroll (int dx, int dy)
|
||||
{
|
||||
super.viewDidScroll(dx, dy);
|
||||
viewDidScroll(_subtitles, dx, dy);
|
||||
viewDidScroll(_showingHistory, dx, dy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added (VirtualMediaPanel target)
|
||||
{
|
||||
super.added(target);
|
||||
|
||||
_history.addObserver(this);
|
||||
|
||||
if (_overrideHistory) {
|
||||
setHistoryEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// derived classes may want to override this method and set up chat history mode based on
|
||||
// whatever preference storage mechanism they use
|
||||
}
|
||||
|
||||
@Override
|
||||
public void layout ()
|
||||
{
|
||||
// sanity check
|
||||
if (_target == null) {
|
||||
log.warning(this + " laid out without target?", new Exception());
|
||||
return;
|
||||
}
|
||||
|
||||
Rectangle vbounds = _target.getViewBounds();
|
||||
if ((vbounds.height < 1) || (vbounds.width < 1)) {
|
||||
return; // fuck that!
|
||||
}
|
||||
|
||||
clearGlyphs(_subtitles); // we'll re-populate from the history
|
||||
|
||||
if (_subtitlesFill) {
|
||||
_subtitleHeight = vbounds.height;
|
||||
}
|
||||
|
||||
// make a guess as to the extent of the history (how many avg sized subtitles will fit in
|
||||
// the subtitle area)
|
||||
_historyExtent = ((_subtitleHeight - _subtitleYSpacing) / SUBTITLE_HEIGHT_GUESS);
|
||||
_scrollbar.setBlockIncrement(_historyExtent);
|
||||
|
||||
// show messages that were born recently enough to be shown.
|
||||
long now = System.currentTimeMillis();
|
||||
// find the first message to display
|
||||
int histSize = _history.size();
|
||||
int index = histSize - 1;
|
||||
for ( ; index >= 0; index--) {
|
||||
ChatMessage msg = _history.get(index);
|
||||
_lastExpire = 0L;
|
||||
if (now > getChatExpire(msg.timestamp, msg.message)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// now that we've found the message that's one too old, increment the index so that it
|
||||
// points to the first message we should display
|
||||
index++;
|
||||
_lastExpire = 0L;
|
||||
|
||||
// now dispatch from that point
|
||||
Graphics2D gfx = getTargetGraphics();
|
||||
for ( ; index < histSize; index++) {
|
||||
ChatMessage msg = _history.get(index);
|
||||
if (shouldShowFromHistory(msg, index)) {
|
||||
displayMessage(msg, gfx);
|
||||
}
|
||||
}
|
||||
|
||||
// and clean up
|
||||
gfx.dispose();
|
||||
|
||||
// make a note that we're laid out
|
||||
_laidout = true;
|
||||
|
||||
// reset the history offset..
|
||||
resetHistoryOffset();
|
||||
|
||||
// finally, if we're in history mode, we should figure that out too
|
||||
if (isHistoryMode()) {
|
||||
updateHistBar(histSize - 1);
|
||||
figureCurrentHistory();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDimmed (boolean dimmed)
|
||||
{
|
||||
super.setDimmed(dimmed);
|
||||
updateDimmed(_subtitles);
|
||||
updateDimmed(_showingHistory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed ()
|
||||
{
|
||||
// we need to do this before super so that our target is still around
|
||||
clearGlyphs(_subtitles);
|
||||
clearGlyphs(_showingHistory);
|
||||
|
||||
super.removed();
|
||||
|
||||
_history.removeObserver(this);
|
||||
|
||||
// clear out our history so that when we are once again added, we activate it and go
|
||||
// through the motions of refiguring everything
|
||||
setHistoryEnabled(false);
|
||||
|
||||
// make a note that we'll need to lay ourselves out before we do anything fun next time
|
||||
_laidout = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared chained constructor.
|
||||
*/
|
||||
protected SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar, int height,
|
||||
boolean fill, int xspace, int yspace)
|
||||
{
|
||||
super(ctx, logic);
|
||||
|
||||
_scrollbar = bar;
|
||||
_subtitleHeight = height;
|
||||
_subtitlesFill = fill;
|
||||
_subtitleXSpacing = xspace;
|
||||
_subtitleYSpacing = yspace;
|
||||
_history = ctx.getChatDirector().getHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the chat glyphs in the specified list to be set to the current dimmed setting.
|
||||
*/
|
||||
protected void updateDimmed (List<? extends ChatGlyph> glyphs)
|
||||
{
|
||||
for (ChatGlyph glyph : glyphs) {
|
||||
glyph.setDim(_dimmed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we currently in history mode?
|
||||
*/
|
||||
protected boolean isHistoryMode ()
|
||||
{
|
||||
return (_historyModel != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current Graphics context of our target, or null if not applicable.
|
||||
*/
|
||||
protected Graphics2D getTargetGraphics ()
|
||||
{
|
||||
// this may return null even if target is not null.
|
||||
return (_target == null) ? null : (Graphics2D)_target.getGraphics();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures us for display of chat history or not.
|
||||
*/
|
||||
protected void setHistoryEnabled (boolean historyEnabled)
|
||||
{
|
||||
if (historyEnabled && _historyModel == null) {
|
||||
_historyModel = _scrollbar.getModel();
|
||||
_historyModel.addChangeListener(this);
|
||||
resetHistoryOffset();
|
||||
|
||||
// out with the subtitles, we'll be displaying history
|
||||
clearGlyphs(_subtitles);
|
||||
|
||||
// "scroll" down to the latest history entry
|
||||
updateHistBar(_history.size() - 1);
|
||||
|
||||
// refigure our history
|
||||
figureCurrentHistory();
|
||||
|
||||
} else if (!historyEnabled && _historyModel != null) {
|
||||
_historyModel.removeChangeListener(this);
|
||||
_historyModel = null;
|
||||
|
||||
// out with the history, we'll be displaying subtitles
|
||||
clearGlyphs(_showingHistory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for informing glyphs of the scrolled view.
|
||||
*/
|
||||
protected void viewDidScroll (List<? extends ChatGlyph> glyphs, int dx, int dy)
|
||||
{
|
||||
for (ChatGlyph glyph : glyphs) {
|
||||
glyph.viewDidScroll(dx, dy);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the history scrollbar with the specified value.
|
||||
*/
|
||||
protected void updateHistBar (int val)
|
||||
{
|
||||
// we may need to figure out the new history offset amount..
|
||||
if (!_histOffsetFinal && _history.size() > _histOffset) {
|
||||
Graphics2D gfx = getTargetGraphics();
|
||||
if (gfx != null) {
|
||||
figureHistoryOffset(gfx);
|
||||
gfx.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// then figure out the new value and range
|
||||
int oldval = Math.max(_histOffset, val);
|
||||
int newmaxval = Math.max(0, _history.size() - 1);
|
||||
int newval = (oldval >= newmaxval - 1) ? newmaxval : oldval;
|
||||
|
||||
// and set it, which MAY generate a change event, but we want to ignore it so we use the
|
||||
// _settingBar flag
|
||||
_settingBar = true;
|
||||
_historyModel.setRangeProperties(newval, _historyExtent, _histOffset,
|
||||
newmaxval + _historyExtent,
|
||||
_historyModel.getValueIsAdjusting());
|
||||
_settingBar = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the history offset so that it will be recalculated next time it is needed.
|
||||
*/
|
||||
protected void resetHistoryOffset ()
|
||||
{
|
||||
_histOffsetFinal = false;
|
||||
_histOffset = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out how many of the first history elements fit in our bounds such that we can set the
|
||||
* bounds on the scrollbar correctly such that the scrolling to the smallest value just barely
|
||||
* puts the first element onscreen.
|
||||
*/
|
||||
protected void figureHistoryOffset (Graphics2D gfx)
|
||||
{
|
||||
if (!isLaidOut()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int hei = _subtitleYSpacing;
|
||||
int hsize = _history.size();
|
||||
for (int ii = 0; ii < hsize; ii++) {
|
||||
ChatGlyph rec = getHistorySubtitle(ii, gfx);
|
||||
Rectangle r = rec.getBounds();
|
||||
hei += r.height;
|
||||
|
||||
// oop, we passed it, it was the last one
|
||||
if (hei >= _subtitleHeight) {
|
||||
_histOffset = Math.max(0, ii - 1);
|
||||
_histOffsetFinal = true;
|
||||
return;
|
||||
}
|
||||
|
||||
hei += getHistorySubtitleSpacing(ii);
|
||||
}
|
||||
|
||||
// basically, this means there isn't yet enough history to fill the first 'page' of the
|
||||
// history scrollback, so we set the offset to the max value, but we do not set
|
||||
// _histOffsetFinal to be true so that this will be recalculated
|
||||
_histOffset = hsize - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out which ChatMessages in the history should currently appear in the showing history.
|
||||
*/
|
||||
protected void figureCurrentHistory ()
|
||||
{
|
||||
int first = _historyModel.getValue();
|
||||
int count = 0;
|
||||
Graphics2D gfx = null;
|
||||
|
||||
if (isLaidOut() && !_history.isEmpty()) {
|
||||
gfx = getTargetGraphics();
|
||||
if (gfx == null) {
|
||||
log.warning("Can't figure current history, no graphics.");
|
||||
return;
|
||||
}
|
||||
|
||||
// start from the bottom..
|
||||
Rectangle vbounds = _target.getViewBounds();
|
||||
int ypos = vbounds.height - _subtitleYSpacing;
|
||||
|
||||
for (int ii = first; ii >= 0; ii--, count++) {
|
||||
ChatGlyph rec = getHistorySubtitle(ii, gfx);
|
||||
|
||||
// see if it will fit
|
||||
Rectangle r = rec.getBounds();
|
||||
ypos -= r.height;
|
||||
if ((count != 0) && ypos <= (vbounds.height - _subtitleHeight)) {
|
||||
break; // don't add that one..
|
||||
}
|
||||
|
||||
// position it
|
||||
rec.setLocation(vbounds.x + _subtitleXSpacing, vbounds.y + ypos);
|
||||
// add space for the next
|
||||
ypos -= getHistorySubtitleSpacing(ii);
|
||||
}
|
||||
}
|
||||
|
||||
// finally, because we've been adding to the _showingHistory here (via getHistorySubtitle)
|
||||
// and in figureHistoryOffset (possibly called prior to this method) we now need to prune
|
||||
// out the ChatGlyphs that aren't actually needed and make sure the ones that are are
|
||||
// positioned on the screen correctly
|
||||
for (Iterator<ChatGlyph> itr = _showingHistory.iterator(); itr.hasNext(); ) {
|
||||
ChatGlyph cg = itr.next();
|
||||
boolean managed = (_target != null) && _target.isManaged(cg);
|
||||
if (cg.histIndex <= first && cg.histIndex > (first - count)) {
|
||||
// it should be showing
|
||||
if (!managed) {
|
||||
_target.addAnimation(cg);
|
||||
}
|
||||
|
||||
} else {
|
||||
// it shouldn't be showing
|
||||
if (managed) {
|
||||
_target.abortAnimation(cg);
|
||||
}
|
||||
itr.remove();
|
||||
}
|
||||
}
|
||||
|
||||
if (gfx != null) {
|
||||
gfx.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the glyph for the specified history index, creating if necessary.
|
||||
*/
|
||||
protected ChatGlyph getHistorySubtitle (int index, Graphics2D layoutGfx)
|
||||
{
|
||||
// do a brute search (over a small set) for an already-created subtitle
|
||||
for (int ii = 0, nn = _showingHistory.size(); ii < nn; ii++) {
|
||||
ChatGlyph cg = _showingHistory.get(ii);
|
||||
if (cg.histIndex == index) {
|
||||
return cg;
|
||||
}
|
||||
}
|
||||
|
||||
// it looks like we have to create a new one: expensive!
|
||||
ChatGlyph cg = createHistorySubtitle(index, layoutGfx);
|
||||
cg.histIndex = index;
|
||||
cg.setDim(_dimmed);
|
||||
_showingHistory.add(cg);
|
||||
return cg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a subtitle for display in the history panel.
|
||||
*
|
||||
* @param index the index of the message in the history list
|
||||
*/
|
||||
protected ChatGlyph createHistorySubtitle (int index, Graphics2D layoutGfx)
|
||||
{
|
||||
ChatMessage message = _history.get(index);
|
||||
int type = getType(message, true);
|
||||
return createSubtitle(message, type, layoutGfx, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the amount of spacing to put after a history subtitle.
|
||||
*
|
||||
* @param index the index of the message in the history list
|
||||
*/
|
||||
protected int getHistorySubtitleSpacing (int index)
|
||||
{
|
||||
ChatMessage message = _history.get(index);
|
||||
return _logic.getSubtitleSpacing(getType(message, true));
|
||||
}
|
||||
|
||||
protected boolean isLaidOut ()
|
||||
{
|
||||
return isShowing() && _laidout;
|
||||
}
|
||||
|
||||
/**
|
||||
* We're looking through history to figure out what messages we should be showing, should we
|
||||
* show the following?
|
||||
*/
|
||||
protected boolean shouldShowFromHistory (ChatMessage msg, int index)
|
||||
{
|
||||
return true; // yes by default.
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears out the supplied list of chat glyphs.
|
||||
*/
|
||||
protected void clearGlyphs (List<ChatGlyph> glyphs)
|
||||
{
|
||||
if (_target != null) {
|
||||
for (int ii = 0, nn = glyphs.size(); ii < nn; ii++) {
|
||||
ChatGlyph rec = glyphs.get(ii);
|
||||
_target.abortAnimation(rec);
|
||||
}
|
||||
|
||||
} else if (!glyphs.isEmpty()) {
|
||||
log.warning("No target to abort chat animations");
|
||||
}
|
||||
glyphs.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified message now, unless we are to ignore it.
|
||||
*/
|
||||
protected void displayMessage (ChatMessage message, Graphics2D gfx)
|
||||
{
|
||||
// get the non-history message type...
|
||||
int type = getType(message, false);
|
||||
if (type != ChatLogic.IGNORECHAT) {
|
||||
// display it now
|
||||
displayMessage(message, type, gfx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the message after we've decided which type it is.
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a subtitle for display now.
|
||||
*/
|
||||
protected void addSubtitle (ChatGlyph rec)
|
||||
{
|
||||
// scroll up the old subtitles
|
||||
Rectangle r = rec.getBounds();
|
||||
scrollUpSubtitles(-r.height - _logic.getSubtitleSpacing(rec.getType()));
|
||||
|
||||
// put this one in place
|
||||
Rectangle vbounds = _target.getViewBounds();
|
||||
rec.setLocation(vbounds.x + _subtitleXSpacing,
|
||||
vbounds.y + vbounds.height - _subtitleYSpacing - r.height);
|
||||
|
||||
// add it to our list and to our media panel
|
||||
rec.setDim(_dimmed);
|
||||
_subtitles.add(rec);
|
||||
_target.addAnimation(rec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a subtitle, but don't do anything funny with it.
|
||||
*/
|
||||
protected ChatGlyph createSubtitle (ChatMessage message, int type, Graphics2D layoutGfx,
|
||||
boolean expires)
|
||||
{
|
||||
// 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<String, Boolean> finfo = _logic.decodeFormat(type, message.getFormat());
|
||||
String format = finfo.left;
|
||||
boolean quotes = finfo.right;
|
||||
|
||||
// now format the text
|
||||
if (format != null) {
|
||||
if (quotes) {
|
||||
text = "\"" + text + "\"";
|
||||
}
|
||||
text = " " + text;
|
||||
text = xlate(MessageBundle.tcompose(
|
||||
format, ((UserMessage) message).getSpeakerDisplayName())) + text;
|
||||
}
|
||||
|
||||
return createSubtitle(layoutGfx, type, message.timestamp, null, 0, text, expires);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a subtitle- a line of text that goes on the bottom.
|
||||
*/
|
||||
protected ChatGlyph createSubtitle (Graphics2D gfx, int type, long timestamp, Icon icon,
|
||||
int indent, String text, boolean expires)
|
||||
{
|
||||
Dimension is = new Dimension();
|
||||
if (icon != null) {
|
||||
is.setSize(icon.getIconWidth(), icon.getIconHeight());
|
||||
}
|
||||
|
||||
Rectangle vbounds = _target.getViewBounds();
|
||||
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 -
|
||||
2 * (_subtitleXSpacing + Math.max(UIManager.getInt("ScrollBar.width"), PAD)));
|
||||
label.layout(gfx);
|
||||
gfx.dispose();
|
||||
|
||||
Dimension ls = label.getSize();
|
||||
Rectangle r = new Rectangle(0, 0, ls.width + indent + paddedIconWidth,
|
||||
Math.max(is.height, ls.height));
|
||||
r.grow(0, 1);
|
||||
Point iconpos = r.getLocation();
|
||||
iconpos.translate(indent, r.height - is.height - 1);
|
||||
Point labelpos = r.getLocation();
|
||||
labelpos.translate(indent + paddedIconWidth, 1);
|
||||
Rectangle lr = new Rectangle(r.x + indent + paddedIconWidth, r.y,
|
||||
r.width - indent - paddedIconWidth, ls.height + 2);
|
||||
|
||||
// last a really long time if we're not supposed to expire
|
||||
long lifetime = Integer.MAX_VALUE;
|
||||
if (expires) {
|
||||
lifetime = getChatExpire(timestamp, label.getText()) - timestamp;
|
||||
}
|
||||
Shape shape = _logic.getSubtitleShape(type, lr, r);
|
||||
return new ChatGlyph(this, type, lifetime, r.union(shape.getBounds()), shape, icon,
|
||||
iconpos, label, labelpos, _logic.getOutlineColor(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expire time for the specified chat.
|
||||
*/
|
||||
protected long getChatExpire (long timestamp, String text)
|
||||
{
|
||||
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
|
||||
_lastExpire = start + Math.min(text.length() * durations[0], durations[2]);
|
||||
// but don't let it be longer than the maximum display time
|
||||
_lastExpire = Math.min(timestamp + durations[2], _lastExpire);
|
||||
// and be sure to pop up the returned time so that it is above the min
|
||||
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.
|
||||
*/
|
||||
protected void glyphExpired (ChatGlyph glyph)
|
||||
{
|
||||
_subtitles.remove(glyph);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the Message class/localtype/mode into our internal type code.
|
||||
*/
|
||||
protected int getType (ChatMessage message, boolean history)
|
||||
{
|
||||
String localtype = message.localtype;
|
||||
|
||||
if (message instanceof TellFeedbackMessage) {
|
||||
if (((TellFeedbackMessage)message).isFailure()) {
|
||||
return ChatLogic.FEEDBACK;
|
||||
}
|
||||
return (history || isApprovedLocalType(localtype)) ?
|
||||
ChatLogic.TELLFEEDBACK : ChatLogic.IGNORECHAT;
|
||||
|
||||
} else if (message instanceof UserMessage) {
|
||||
int type = _logic.decodeType(localtype);
|
||||
if (type != 0) {
|
||||
// factor in the mode
|
||||
return _logic.adjustTypeByMode(((UserMessage) message).mode, type);
|
||||
}
|
||||
// if we're showing from history, include specialized chat messages
|
||||
if (history) {
|
||||
return ChatLogic.SPECIALIZED;
|
||||
}
|
||||
// otherwise fall through and IGNORECHAT
|
||||
|
||||
} else if (message instanceof SystemMessage) {
|
||||
if (history || isApprovedLocalType(localtype)) {
|
||||
switch (((SystemMessage) message).attentionLevel) {
|
||||
case SystemMessage.INFO:
|
||||
return ChatLogic.INFO;
|
||||
case SystemMessage.FEEDBACK:
|
||||
return ChatLogic.FEEDBACK;
|
||||
case SystemMessage.ATTENTION:
|
||||
return ChatLogic.ATTENTION;
|
||||
default:
|
||||
log.warning("Unknown attention level for system message", "msg", message);
|
||||
}
|
||||
}
|
||||
return ChatLogic.IGNORECHAT;
|
||||
}
|
||||
|
||||
log.warning("Skipping received message of unknown type", "msg", message);
|
||||
return ChatLogic.IGNORECHAT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if we want to display the specified localtype.
|
||||
*/
|
||||
protected boolean isApprovedLocalType (String localtype)
|
||||
{
|
||||
return true; // we show everything, the ComicChat is a little more picky
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll all the subtitles up by the specified amount.
|
||||
*/
|
||||
protected void scrollUpSubtitles (int dy)
|
||||
{
|
||||
// dirty and move all the old glyphs
|
||||
Rectangle vbounds = _target.getViewBounds();
|
||||
int miny = vbounds.y + vbounds.height - _subtitleHeight;
|
||||
for (Iterator<ChatGlyph> iter = _subtitles.iterator(); iter.hasNext();) {
|
||||
ChatGlyph sub = iter.next();
|
||||
sub.translate(0, dy);
|
||||
if (sub.getBounds().y <= miny) {
|
||||
iter.remove();
|
||||
_target.abortAnimation(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** List of existing messages from our chat director. */
|
||||
protected HistoryList _history;
|
||||
|
||||
/** If set, show history no matter what the client prefs say. */
|
||||
protected boolean _overrideHistory;
|
||||
|
||||
/** The currently displayed subtitles O' history. */
|
||||
protected List<ChatGlyph> _showingHistory = Lists.newArrayList();
|
||||
|
||||
/** Our history scrollbar. */
|
||||
protected JScrollBar _scrollbar;
|
||||
|
||||
/** Tracks whether or not we've been laid out. */
|
||||
protected boolean _laidout;
|
||||
|
||||
/** If we're in history mode, this will be non-null and will notify
|
||||
* us of our historical positioning. */
|
||||
protected BoundedRangeModel _historyModel = null;
|
||||
|
||||
/** The currently displayed subtitle areas. */
|
||||
protected List<ChatGlyph> _subtitles = Lists.newArrayList();
|
||||
|
||||
/** The amount of vertical space to use for subtitles. */
|
||||
protected int _subtitleHeight;
|
||||
|
||||
/** If true, subtitles should fill all available height. */
|
||||
protected boolean _subtitlesFill;
|
||||
|
||||
/** The amount of space we want around the subtitles. */
|
||||
protected int _subtitleXSpacing;
|
||||
protected int _subtitleYSpacing;
|
||||
|
||||
/** The unbounded expire time of the last chat glyph displayed. */
|
||||
protected long _lastExpire;
|
||||
|
||||
/** If the history offset we've figured is all figured out or needs to be refigured. */
|
||||
protected boolean _histOffsetFinal = false;
|
||||
|
||||
/** If true, we're the ones updating the history scrollbar and change
|
||||
* events should be ignored. */
|
||||
boolean _settingBar = false;
|
||||
|
||||
/** The history offset (from 0) such that the history lines (0, _histOffset - 1) will all fit
|
||||
* onscreen if the lowest scrollbar position is _histOffset. */
|
||||
protected int _histOffset = 0;
|
||||
|
||||
/** A guess of how many history lines fit onscreen at a time. */
|
||||
protected int _historyExtent;
|
||||
|
||||
/** A guess as to the height of a subtitle (plus spacing). */
|
||||
protected static final int SUBTITLE_HEIGHT_GUESS = 16;
|
||||
|
||||
/** The amount of space to insert between the icon and the text. */
|
||||
protected static final int ICON_PADDING = 4;
|
||||
|
||||
/** The padding in each direction around the text to the edges of a chat 'bubble'. */
|
||||
protected static final int PAD = ChatLogic.PAD;
|
||||
}
|
||||
Reference in New Issue
Block a user