Factored the subtitle and comic chat stuff out of Yohoho. All completely
untested. Whee! I thought I had gotten around to making a standalone avatar chat room based on the Vilya Stage code, and was hoping to add this there, but I guess I never did that. git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@1021 ed5b42cb-e716-0410-a449-f6a68f950b19
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Vilya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/vilya/
|
||||
//
|
||||
// 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,208 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Vilya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/vilya/
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a chat overlay.
|
||||
*/
|
||||
public ChatOverlay (CrowdContext ctx)
|
||||
{
|
||||
_ctx = ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message bundle used to translate default messages.
|
||||
*/
|
||||
protected String getDefaultMessageBundle ()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The light of our life. */
|
||||
protected CrowdContext _ctx;
|
||||
|
||||
/** 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,944 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Vilya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/vilya/
|
||||
//
|
||||
// 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, JScrollBar historyBar, int subtitleHeight)
|
||||
{
|
||||
super(ctx, 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 (placeOf(type)) == 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 (placeOf(type)) {
|
||||
case INFO:
|
||||
case 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: {
|
||||
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 (modeOf(type) == 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, 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, 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;
|
||||
}
|
||||
|
||||
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);
|
||||
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 (modeOf(type)) {
|
||||
case SHOUT:
|
||||
case THINK:
|
||||
case 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 (modeOf(type)) {
|
||||
case SHOUT:
|
||||
case EMOTE:
|
||||
case 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 (placeOf(type)) {
|
||||
case INFO:
|
||||
case ATTENTION:
|
||||
// upper left
|
||||
r.setLocation(vbounds.x + BUBBLE_SPACING,
|
||||
vbounds.y + BUBBLE_SPACING);
|
||||
return;
|
||||
|
||||
case 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 (placeOf(type)) {
|
||||
case INFO:
|
||||
case ATTENTION:
|
||||
// boring rectangle wrapped in an Area for translation
|
||||
return new Area(r);
|
||||
}
|
||||
|
||||
switch (modeOf(type)) {
|
||||
case 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: {
|
||||
// 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 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 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 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 (modeOf(type) == 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 (modeOf(type) == 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 = 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 getDisplayDurationIndex ()
|
||||
{
|
||||
// normalize the duration returned by super. Annoying.
|
||||
return super.getDisplayDurationIndex() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 == INFO) || (_type == 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user