/* * This file is part of the Haven & Hearth game client. * Copyright (C) 2009 Fredrik Tolf <fredrik@dolda2000.com>, and * Björn Johannessen <johannessen.bjorn@gmail.com> * * Redistribution and/or modification of this file is subject to the * terms of the GNU Lesser General Public License, version 3, as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Other parts of this source tree adhere to other copying * rights. Please see the file `COPYING' in the root directory of the * source tree for details. * * A copy the GNU Lesser General Public License is distributed along * with the source tree of which this file is a part in the file * `doc/LPGL-3'. If it is missing for any reason, please see the Free * Software Foundation's website at <http://www.fsf.org/>, or write * to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, * Boston, MA 02111-1307 USA */ package haven; import haven.minimap.Radar; import static haven.Window.cbtni; import java.util.*; import java.awt.Color; import java.awt.Font; import java.awt.event.KeyEvent; import java.awt.font.TextAttribute; import java.awt.font.TextHitInfo; import java.text.*; import java.text.AttributedCharacterIterator.Attribute; import java.net.URL; import java.util.regex.*; import java.io.IOException; import java.awt.datatransfer.*; public class ChatUI extends Widget { public static final RichText.Foundry fnd = new RichText.Foundry(new ChatParser(TextAttribute.FAMILY, "SansSerif", TextAttribute.SIZE, 12, TextAttribute.FOREGROUND, Color.BLACK)); public static final Text.Foundry qfnd = new Text.Foundry(new Font("SansSerif", Font.PLAIN, 14), new Color(192, 255, 192)); public static final int selw = 100; public Channel sel = null; private final Selector chansel; public boolean expanded = false; private Coord base; private QuickLine qline = null; private final LinkedList<Notification> notifs = new LinkedList<Notification>(); private static final Pattern tags_patt = Pattern.compile("\\$(hl)\\[([^\\[\\]]*)\\]"); public ChatUI(Coord c, int w, Widget parent) { super(c.add(0, -50), new Coord(w, 50), parent); chansel = new Selector(Coord.z, new Coord(selw, sz.y)); chansel.hide(); base = c; setfocusctl(true); setcanfocus(false); } public static boolean hasTags(String text){ return tags_patt.matcher(text).find(); } private static Color lighter(Color col){ int hsl[] = new int[3]; Utils.rgb2hsl(col.getRed(), col.getGreen(), col.getBlue(), hsl); hsl[1] = Math.round(0.7f*hsl[1]); hsl[2] = 100; int rgb[] = Utils.hsl2rgb(hsl); return new Color(rgb[0], rgb[1], rgb[2]); } public static class ChatAttribute extends Attribute { private ChatAttribute(String name) { super(name); } public static final Attribute HYPERLINK = new ChatAttribute("hyperlink"); } public static class FuckMeGentlyWithAChainsaw { /* This wrapper class exists to work around the possibly most * stupid Java bug ever (and that's saying a lot): That * URL.equals and URL.hashCode do DNS lookups and * block. Which, of course, not only sucks performance-wise * but also breaks actual correct URL equality. */ public final URL url; public FuckMeGentlyWithAChainsaw(URL url) { this.url = url; } } public static class ChatParser extends RichText.Parser { public static final Pattern urlpat = Pattern.compile("\\b((https?://)|(www\\.[a-z0-9_.-]+\\.[a-z0-9_.-]+))[a-z0-9/_.~#%+?&:*=-]*", Pattern.CASE_INSENSITIVE); public static final Map<? extends Attribute, ?> urlstyle = RichText.fillattrs(TextAttribute.FOREGROUND, new Color(64,175,255), TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD); public ChatParser(Object... args) { super(args); } protected RichText.Part text(PState s, String text, Map<? extends Attribute, ?> attrs) throws IOException { RichText.Part ret = null; int p = 0; while(true) { Matcher m = urlpat.matcher(text); if(!m.find(p)) break; URL url; try { String su = text.substring(m.start(), m.end()); if(su.indexOf(':') < 0) su = "http://" + su; url = new URL(su); } catch(java.net.MalformedURLException e) { p = m.end(); continue; } RichText.Part lead = new RichText.TextPart(text.substring(0, m.start()), attrs); if(ret == null) ret = lead; else ret.append(lead); Map<Attribute, Object> na = new HashMap<Attribute, Object>(attrs); na.putAll(urlstyle); na.put(ChatAttribute.HYPERLINK, new FuckMeGentlyWithAChainsaw(url)); ret.append(new RichText.TextPart(text.substring(m.start(), m.end()), na)); p = m.end(); } if(ret == null) ret = new RichText.TextPart(text, attrs); else ret.append(new RichText.TextPart(text.substring(p), attrs)); return(ret); } } public static abstract class Channel extends Widget { public final List<Message> msgs = new LinkedList<Message>(){ @Override public boolean add(Message message) { if(size() >= 200){ removeFirst().tex().dispose(); } return super.add(message); } }; private final Scrollbar sb; public IButton cbtn; protected boolean read = true; @Override public void show(){ super.show(); read = true; } public static abstract class Message { public final long time = System.currentTimeMillis(); public abstract Text text(); public abstract Tex tex(); public abstract Coord sz(); } public static class SimpleMessage extends Message { private final Text t; public SimpleMessage(String text, Color col, int w) { if(Config.timestamp) text = Utils.timestamp(text); if(col == null) this.t = fnd.render(RichText.Parser.quote(text), w); else this.t = fnd.render(RichText.Parser.quote(text), w, TextAttribute.FOREGROUND, col); } public Text text() { return(t); } public Tex tex() { return(t.tex()); } public Coord sz() { return(t.sz()); } } public Channel(Coord c, Coord sz, Widget parent, boolean closeable) { super(c, sz, parent); sb = new Scrollbar(new Coord(sz.x, 0), ih(), this, 0, -ih()); if(closeable){ cbtn = new IButton(Coord.z, this, cbtni[0], cbtni[1], cbtni[2]); cbtn.c = new Coord(sz.x - cbtn.sz.x - sb.sz.x - 3, 0); cbtn.recthit = true; } } public Channel(Widget parent, boolean closeable) { this(new Coord(selw, 0), parent.sz.sub(selw, 0), parent, closeable); } public void append(Message msg, boolean attn) { synchronized(msgs) { msgs.add(msg); int y = 0; for(Message m : msgs) y += m.sz().y; boolean b = sb.val >= sb.max; sb.max = y - ih(); if(b) sb.val = sb.max; if(attn){ if(!visible) read = false; } } } public void append(String line, Color col) { append(new SimpleMessage(line, col, iw()), false); } public int iw() { return(sz.x - sb.sz.x); } public int ih() { return(sz.y); } public void draw(GOut g) { g.chcolor(24, 24, 16, 200); g.frect(Coord.z, sz); g.chcolor(); int y = 0; boolean sel = false; synchronized(msgs) { for(Message msg : msgs) { if((selstart != null) && (msg == selstart.msg)) sel = true; int y1 = y - sb.val; int y2 = y1 + msg.sz().y; if((y2 > 0) && (y1 < ih())) { if(sel) drawsel(g, msg, y1); g.image(msg.tex(), new Coord(0, y1)); } if((selend != null) && (msg == selend.msg)) sel = false; y += msg.sz().y; } } sb.max = y - ih(); super.draw(g); } public boolean mousewheel(Coord c, int amount) { sb.ch(amount * 15); return(true); } public void resize(Coord sz) { super.resize(sz); if(sb != null) { sb.resize(ih()); sb.move(new Coord(sz.x, 0)); int y = 0; for(Message m : msgs) y += m.sz().y; boolean b = sb.val >= sb.max; sb.max = y - ih(); if(b) sb.val = sb.max; } if(cbtn != null){ cbtn.c = new Coord(sz.x - cbtn.sz.x - sb.sz.x - 3, 0); } } public void notify(Message msg) { getparent(ChatUI.class).notify(this, msg); } public static class CharPos { public final Message msg; public final RichText.TextPart part; public final TextHitInfo ch; public CharPos(Message msg, RichText.TextPart part, TextHitInfo ch) { this.msg = msg; this.part = part; this.ch = ch; } public boolean equals(Object oo) { if(!(oo instanceof CharPos)) return(false); CharPos o = (CharPos)oo; return((o.msg == this.msg) && (o.part == this.part) && o.ch.equals(this.ch)); } } public final Comparator<CharPos> poscmp = new Comparator<CharPos>() { public int compare(CharPos a, CharPos b) { if(a.msg != b.msg) { synchronized(msgs) { for(Message msg : msgs) { if(msg == a.msg) return(-1); else if(msg == b.msg) return(1); } } throw(new IllegalStateException("CharPos message is no longer contained in the log")); } else if(a.part != b.part) { for(RichText.Part part = ((RichText)a.msg.text()).parts; part != null; part = part.next) { if(part == a.part) return(-1); else return(1); } throw(new IllegalStateException("CharPos is no longer contained in the log")); } else { return(a.ch.getInsertionIndex() - b.ch.getInsertionIndex()); } } }; public Message messageat(Coord c, Coord hc) { int y = -sb.val; synchronized(msgs) { for(Message msg : msgs) { Coord sz = msg.sz(); if((c.y >= y) && (c.y < y + sz.y)) { if(hc != null) { hc.x = c.x; hc.y = c.y - y; } return(msg); } y += sz.y; } } return(null); } public CharPos charat(Coord c) { if(c.y < -sb.val) { if(msgs.size() < 1) return(null); Message msg = msgs.get(0); if(!(msg.text() instanceof RichText)) return(null); RichText.TextPart fp = null; for(RichText.Part part = ((RichText)msg.text()).parts; part != null; part = part.next) { if(part instanceof RichText.TextPart) { fp = (RichText.TextPart)part; break; } } if(fp == null) return(null); return(new CharPos(msg, fp, TextHitInfo.leading(0))); } Coord hc = new Coord(); Message msg = messageat(c, hc); if((msg == null) || !(msg.text() instanceof RichText)) return(null); RichText rt = (RichText)msg.text(); RichText.Part p = rt.partat(hc); if(p == null) { RichText.TextPart lp = null; for(RichText.Part part = ((RichText)msg.text()).parts; part != null; part = part.next) { if(part instanceof RichText.TextPart) lp = (RichText.TextPart)part; } if(lp == null) return(null); return(new CharPos(msg, lp, TextHitInfo.trailing(lp.end - lp.start - 1))); } if(!(p instanceof RichText.TextPart)) return(null); RichText.TextPart tp = (RichText.TextPart)p; return(new CharPos(msg, tp, tp.charat(hc))); } private CharPos selorig, lasthit, selstart, selend; private boolean dragging; public boolean mousedown(Coord c, int btn) { if(super.mousedown(c, btn)) return(true); if(btn == 1) { selstart = selend = null; CharPos ch = charat(c); if(ch != null) { selorig = lasthit = ch; dragging = false; ui.grabmouse(this); } return(true); } return(false); } public void mousemove(Coord c) { if(selorig != null) { CharPos ch = charat(c); if((ch != null) && !ch.equals(lasthit)) { lasthit = ch; if(!dragging && !ch.equals(selorig)) dragging = true; int o = poscmp.compare(selorig, ch); if(o < 0) { selstart = selorig; selend = ch; } else if(o > 0) { selstart = ch; selend = selorig; } else { selstart = selend = null; } } } else { super.mousemove(c); } } protected void selected(CharPos start, CharPos end) { StringBuilder buf = new StringBuilder(); synchronized(msgs) { boolean sel = false; for(Message msg : msgs) { if(!(msg.text() instanceof RichText)) continue; RichText rt = (RichText)msg.text(); RichText.Part part = null; if(sel) { part = rt.parts; } else if(msg == start.msg) { sel = true; for(part = rt.parts; part != null; part = part.next) { if(part == start.part) break; } } if(sel) { for(; part != null; part = part.next) { if(!(part instanceof RichText.TextPart)) continue; RichText.TextPart tp = (RichText.TextPart)part; CharacterIterator iter = tp.ti(); int sch; if(tp == start.part) sch = tp.start + start.ch.getInsertionIndex(); else sch = tp.start; int ech; if(tp == end.part) ech = tp.start + end.ch.getInsertionIndex(); else ech = tp.end; for(int i = sch; i < ech; i++) buf.append(iter.setIndex(i)); if(part == end.part) { sel = false; break; } buf.append(' '); } if(sel) buf.append('\n'); } if(msg == end.msg) break; } } Clipboard cl; if((cl = java.awt.Toolkit.getDefaultToolkit().getSystemSelection()) == null) cl = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard(); try { final CharPos ownsel = selstart; cl.setContents(new StringSelection(buf.toString()), new ClipboardOwner() { public void lostOwnership(Clipboard cl, Transferable tr) { if(selstart == ownsel) selstart = selend = null; } }); } catch(IllegalStateException e) {} } protected void clicked(CharPos pos) { AttributedCharacterIterator inf = pos.part.ti(); inf.setIndex(pos.ch.getCharIndex()); FuckMeGentlyWithAChainsaw url = (FuckMeGentlyWithAChainsaw)inf.getAttribute(ChatAttribute.HYPERLINK); if((url != null) && (WebBrowser.self != null)) { try { WebBrowser.self.show(url.url); } catch(WebBrowser.BrowserException e) { getparent(GameUI.class).error("Could not launch web browser."); } } } public boolean mouseup(Coord c, int btn) { if(btn == 1) { if(selorig != null) { if(selstart != null) selected(selstart, selend); else clicked(selorig); ui.grabmouse(null); selorig = null; dragging = false; } } return(super.mouseup(c, btn)); } public void select() { getparent(ChatUI.class).select(this); } public void display() { select(); ChatUI chat = getparent(ChatUI.class); chat.expand(); chat.parent.setfocus(chat); } private void drawsel(GOut g, Message msg, int y) { RichText rt = (RichText)msg.text(); boolean sel = msg != selstart.msg; for(RichText.Part part = rt.parts; part != null; part = part.next) { if(!(part instanceof RichText.TextPart)) continue; RichText.TextPart tp = (RichText.TextPart)part; if(tp.start == tp.end) continue; TextHitInfo a, b; if(sel) { a = TextHitInfo.leading(0); } else if(tp == selstart.part) { a = selstart.ch; sel = true; } else { continue; } if(tp == selend.part) { sel = false; b = selend.ch; } else { b = TextHitInfo.trailing(tp.end - tp.start - 1); } Coord ul = new Coord(tp.x + (int)tp.advance(0, a.getInsertionIndex()), tp.y + y); Coord sz = new Coord((int)tp.advance(a.getInsertionIndex(), b.getInsertionIndex()), tp.height()); g.chcolor(0, 0, 255, 255); g.frect(ul, sz); g.chcolor(); if(!sel) break; } } public void uimsg(String name, Object... args) { if(name == "sel") { select(); } else if(name == "dsp") { display(); } else { super.uimsg(name, args); } } public void wdgmsg(Widget sender, String msg, Object... args) { if(sender == cbtn) { wdgmsg("close"); } else { super.wdgmsg(sender, msg, args); } } public abstract String name(); } public static class Log extends Channel { private final String name; public Log(Widget parent, String name) { super(parent, false); this.name = name; } public String name() {return(name);} } public static abstract class EntryChannel extends Channel { private final TextEntry in; private List<String> history = new ArrayList<String>(); private int hpos = 0; private String hcurrent; public EntryChannel(Widget parent) { super(parent, true); setfocusctl(true); this.in = new TextEntry(new Coord(0, sz.y - 20), new Coord(sz.x, 20), this, "") { public void activate(String text) { if(text.length() > 0) send(text); settext(""); hpos = history.size(); } public boolean keydown(KeyEvent ev) { if(ev.getKeyCode() == KeyEvent.VK_UP) { if(hpos > 0) { if(hpos == history.size()) hcurrent = text; rsettext(history.get(--hpos)); } return(true); } else if(ev.getKeyCode() == KeyEvent.VK_DOWN) { if(hpos < history.size()) { if(++hpos == history.size()) rsettext(hcurrent); else rsettext(history.get(hpos)); } return(true); } else { return(super.keydown(ev)); } } }; } public int ih() { return(sz.y - 20); } public void resize(Coord sz) { super.resize(sz); if(in != null) { in.c = new Coord(0, this.sz.y - 20); in.resize(new Coord(this.sz.x, 20)); } } public void send(String text) { history.add(text); wdgmsg("msg", text); } } public static class SimpleChat extends EntryChannel { public final String name; public SimpleChat(Widget parent, String name) { super(parent); this.name = name; } public void uimsg(String msg, Object... args) { if((msg == "msg") || (msg == "log")) { String line = parseTags((String)args[0]); if(line != null) { Color col = null; if (args.length > 1) col = (Color) args[1]; if (col == null) col = Color.WHITE; boolean notify = (args.length > 2) ? (((Integer) args[2]) != 0) : false; Message cmsg = new SimpleMessage(line, col, iw()); append(cmsg, false); if (notify) notify(cmsg); } } else { super.uimsg(msg, args); } } public String name() { return(name); } } public static class MultiChat extends EntryChannel { private final String name; private final boolean notify; private final Map<Integer, Color> pc = new HashMap<Integer, Color>(); public class NamedMessage extends Message { public final int from; public final String text; public final int w; public final Color col; private String cn; private Text r = null; public NamedMessage(int from, String text, Color col, int w) { this.from = from; this.text = text; this.w = w; this.col = col; } public Text text() { BuddyWnd.Buddy b = getparent(GameUI.class).buddies.find(from); String nm = (b == null)?"???":(b.name); if((r == null) || !nm.equals(cn)) { String msg = RichText.Parser.quote(String.format("%s: %s", nm, text)); if(Config.timestamp){ msg = Utils.timestamp(msg); } r = fnd.render(msg, w, TextAttribute.FOREGROUND, col); cn = nm; } return(r); } public Tex tex() { return(text().tex()); } public Coord sz() { if(r == null) return(text().sz()); else return(r.sz()); } } public class MyMessage extends SimpleMessage { public MyMessage(String text, int w) { super(text, new Color(192, 192, 255), w); } } public MultiChat(Widget parent, String name, boolean notify) { super(parent); this.name = name; this.notify = notify; } private static final Random cr = new Random(); private static Color randcol() { int[] c = {cr.nextInt(256), cr.nextInt(256), cr.nextInt(256)}; int mc = Math.max(c[0], Math.max(c[1], c[2])); for(int i = 0; i < c.length; i++) c[i] = (c[i] * 255) / mc; return(new Color(c[0], c[1], c[2])); } public Color fromcolor(int from) { synchronized(pc) { Color c = pc.get(from); if(c == null) pc.put(from, c = randcol()); return(c); } } public void uimsg(String msg, Object... args) { if(msg == "msg") { Integer from = (Integer)args[0]; String line = parseTags((String)args[1]); if(line != null) { if (from == null) { append(new MyMessage(line, iw()), false); } else { Message cmsg = new NamedMessage(from, line, fromcolor(from), iw()); append(cmsg, true); if (notify) notify(cmsg); } } } else { super.uimsg(msg, args); } } public String name() { return(name); } } public static class PartyChat extends MultiChat { public PartyChat(Widget parent) { super(parent, "Party", true); } public void uimsg(String msg, Object... args) { if(msg == "msg") { Integer from = (Integer)args[0]; int gobid = (Integer)args[1]; String line = parseTags((String)args[2]); if(line != null) { Color col = Color.WHITE; synchronized (ui.sess.glob.party.memb) { Party.Member pm = ui.sess.glob.party.memb.get((long) gobid); if (pm != null) col = lighter(pm.col); } if (from == null) { append(new MyMessage(line, iw()), false); } else { Message cmsg = new NamedMessage(from, line, col, iw()); append(cmsg, true); notify(cmsg); } } } else { super.uimsg(msg, args); } } } public static class PrivChat extends EntryChannel { private final int other; public static final Color[] gc = new Color[] { new Color(230,48,32), new Color(64,180,200), }; public class InMessage extends SimpleMessage { public InMessage(String text, int w) { super(text, PrivChat.gc[0], w); } } public class OutMessage extends SimpleMessage { public OutMessage(String text, int w) { super(text, PrivChat.gc[1], w); } } public PrivChat(Widget parent, int other) { super(parent); this.other = other; } public void uimsg(String msg, Object... args) { if(msg == "msg") { String t = (String)args[0]; String line = parseTags((String)args[1]); if(line != null) { if (t.equals("in")) { Message cmsg = new InMessage(line, iw()); append(cmsg, true); notify(cmsg); } else if (t.equals("out")) { append(new OutMessage(line, iw()), false); } } } else if(msg == "err") { String err = (String)args[0]; Message cmsg = new SimpleMessage(err, Color.RED, iw()); append(cmsg, false); notify(cmsg); } else { super.uimsg(msg, args); } } public String name() { BuddyWnd.Buddy b = getparent(GameUI.class).buddies.find(other); if(b == null) return("???"); else return(b.name); } } @RName("schan") public static class $SChan implements Factory { public Widget create(Coord c, Widget parent, Object[] args) { String name = (String)args[0]; return(new SimpleChat(parent, name)); } } @RName("mchat") public static class $MChat implements Factory { public Widget create(Coord c, Widget parent, Object[] args) { String name = (String)args[0]; boolean notify = ((Integer)args[1]) != 0; return(new MultiChat(parent, name, notify)); } } @RName("pchat") public static class $PChat implements Factory { public Widget create(Coord c, Widget parent, Object[] args) { return(new PartyChat(parent)); } } @RName("pmchat") public static class $PMChat implements Factory { public Widget create(Coord c, Widget parent, Object[] args) { int other = (Integer)args[0]; return(new PrivChat(parent, other)); } } public Widget makechild(String type, Object[] pargs, Object[] cargs) { return(gettype(type).create(Coord.z, this, cargs)); } private class Selector extends Widget { public final Text.Foundry nf = new Text.Foundry("SansSerif", 12); private final List<DarkChannel> chls = new ArrayList<DarkChannel>(); private int s = 0; public final Text.Foundry nfu = new Text.Foundry("SansSerif", 14, Font.BOLD); private class DarkChannel { public final Channel chan; public Text rname; public boolean rread; private DarkChannel(Channel chan) { this.chan = chan; this.rread = false; } } public Selector(Coord c, Coord sz) { super(c, sz, ChatUI.this); } private void add(Channel chan) { synchronized(chls) { chls.add(new DarkChannel(chan)); } } private void rm(Channel chan) { synchronized(chls) { for(Iterator<DarkChannel> i = chls.iterator(); i.hasNext();) { DarkChannel c = i.next(); if(c.chan == chan) i.remove(); } } } public void draw(GOut g) { g.chcolor(64, 64, 64, 192); g.frect(Coord.z, sz); int i = s; int y = 0; synchronized(chls) { while(i < chls.size()) { DarkChannel ch = chls.get(i); if(ch.chan == sel) { g.chcolor(128, 128, 192, 255); g.frect(new Coord(0, y), new Coord(sz.x, 19)); } g.chcolor(255, 255, 255, 255); if((ch.rname == null) || !ch.rname.text.equals(ch.chan.name()) || ch.rread != ch.chan.read) { ch.rread = ch.chan.read; if(ch.rread){ ch.rname = nf.render(ch.chan.name()); } else { ch.rname = nfu.render(ch.chan.name()); } } g.aimage(ch.rname.tex(), new Coord(sz.x / 2, y + 10), 0.5, 0.5); g.line(new Coord(5, y + 19), new Coord(sz.x - 5, y + 19), 1); y += 20; if(y >= sz.y) break; i++; } } g.chcolor(); } public boolean up() { Channel prev = null; for(DarkChannel ch : chls) { if(ch.chan == sel) { if(prev != null) { select(prev); return(true); } else { return(false); } } prev = ch.chan; } return(false); } public boolean down() { for(Iterator<DarkChannel> i = chls.iterator(); i.hasNext();) { DarkChannel ch = i.next(); if(ch.chan == sel) { if(i.hasNext()) { select(i.next().chan); return(true); } else { return(false); } } } return(false); } private Channel bypos(Coord c) { int i = (c.y / 20) + s; if((i >= 0) && (i < chls.size())) return(chls.get(i).chan); return(null); } public boolean mousedown(Coord c, int button) { if(button == 1) { Channel chan = bypos(c); if(chan != null) select(chan); } return(true); } public boolean mousewheel(Coord c, int amount) { s += amount; if(s >= chls.size() - (sz.y / 20)) s = chls.size() - (sz.y / 20); if(s < 0) s = 0; return(true); } } public void select(Channel chan) { Channel prev = sel; sel = chan; if(expanded) { if(prev != null) prev.hide(); sel.show(); resize(sz); } } private class Notification { public final Channel chan; public final Text chnm; public final Channel.Message msg; public final long time = System.currentTimeMillis(); private Notification(Channel chan, Channel.Message msg) { this.chan = chan; this.msg = msg; this.chnm = chansel.nf.render(chan.name(), Color.WHITE); } } private Text.Line rqline = null; private int rqpre; public void drawsmall(GOut g, Coord br, int h) { Coord c; if(qline != null) { if((rqline == null) || !rqline.text.equals(qline.line)) { String pre = String.format("%s> ", qline.chan.name()); rqline = qfnd.render(pre + qline.line); rqpre = pre.length(); } c = br.sub(0, 20); g.chcolor(24, 24, 16, 200); g.frect(c, rqline.tex().sz()); g.chcolor(); g.image(rqline.tex(), c); int lx = rqline.advance(qline.point + rqpre); g.line(new Coord(br.x + lx + 1, br.y - 18), new Coord(br.x + lx + 1, br.y - 6), 1); } else { c = br.sub(0, 5); } long now = System.currentTimeMillis(); synchronized(notifs) { for(Iterator<Notification> i = notifs.iterator(); i.hasNext();) { Notification n = i.next(); if(now - n.time > 5000) { i.remove(); continue; } if((c.y -= n.msg.sz().y) < br.y - h) break; g.chcolor(24, 24, 16, 200); g.frect(c, n.chnm.tex().sz().add(n.msg.tex().sz().x + selw, 0)); g.chcolor(); g.image(n.chnm.tex(), c, br.sub(0, h), br.add(selw - 10, 0)); g.image(n.msg.tex(), c.add(selw, 0)); } } } public static final Resource notifsfx = Resource.load("sfx/tick"); public void notify(Channel chan, Channel.Message msg) { synchronized(notifs) { notifs.addFirst(new Notification(chan, msg)); } Audio.play(notifsfx); } public void newchild(Widget w) { if(w instanceof Channel) { Channel chan = (Channel)w; select(chan); chansel.add(chan); if(!expanded) chan.hide(); } } public void cdestroy(Widget w) { if(w instanceof Channel) { Channel chan = (Channel)w; if(chan == sel) sel = null; chansel.rm(chan); } } public void resize(Coord sz) { super.resize(sz); this.c = base.add(0, -this.sz.y); chansel.resize(new Coord(selw, this.sz.y)); if(sel != null) sel.resize(new Coord(this.sz.x - selw, this.sz.y)); } public void resize(int w) { resize(new Coord(w, sz.y)); } public void move(Coord base) { this.c = (this.base = base).add(0, -sz.y); } public void expand() { if(expanded) return; resize(new Coord(sz.x, 100)); setcanfocus(true); if(sel != null) sel.show(); chansel.show(); expanded = true; } public void contract() { if(!expanded) return; resize(new Coord(sz.x, 50)); setcanfocus(false); if(sel != null) sel.hide(); chansel.hide(); expanded = false; } private class QuickLine extends LineEdit { public final EntryChannel chan; private QuickLine(EntryChannel chan) { this.chan = chan; } private void cancel() { qline = null; ui.grabkeys(null); } protected void done(String line) { if(line.length() > 0) chan.send(line); cancel(); } public boolean key(char c, int code, int mod) { if(c == 27) { cancel(); } else { return(super.key(c, code, mod)); } return(true); } } public boolean keydown(KeyEvent ev) { boolean M = (ev.getModifiersEx() & (KeyEvent.META_DOWN_MASK | KeyEvent.ALT_DOWN_MASK)) != 0; if(qline != null) { if(M && (ev.getKeyCode() == KeyEvent.VK_UP)) { Channel prev = this.sel; while(chansel.up()) { if(this.sel instanceof EntryChannel) break; } if(!(this.sel instanceof EntryChannel)) { select(prev); return(true); } qline = new QuickLine((EntryChannel)sel); return(true); } else if(M && (ev.getKeyCode() == KeyEvent.VK_DOWN)) { Channel prev = this.sel; while(chansel.down()) { if(this.sel instanceof EntryChannel) break; } if(!(this.sel instanceof EntryChannel)) { select(prev); return(true); } qline = new QuickLine((EntryChannel)sel); return(true); } qline.key(ev); return(true); } else { if(M && (ev.getKeyCode() == KeyEvent.VK_UP)) { chansel.up(); return(true); } else if(M && (ev.getKeyCode() == KeyEvent.VK_DOWN)) { chansel.down(); return(true); } return(super.keydown(ev)); } } public void toggle() { if(!expanded) { expand(); parent.setfocus(this); } else { if(hasfocus) { if(sz.y == 100) resize(new Coord(sz.x, 300)); else contract(); } else { parent.setfocus(this); } } } public boolean type(char key, KeyEvent ev) { if(qline != null) { qline.key(ev); return(true); } else { return(super.type(key, ev)); } } public boolean globtype(char key, KeyEvent ev) { if(key == 10) { if(!expanded && (sel instanceof EntryChannel)) { ui.grabkeys(this); qline = new QuickLine((EntryChannel)sel); return(true); } } return(super.globtype(key, ev)); } private static String parseTags(String text) { try { Matcher m = tags_patt.matcher(text); while (m.find()) { String tag = m.group(1); String val = m.group(2); if (tag.equals("hl")) { try { long id = Long.parseLong(val); Gob gob = UI.instance.sess.glob.oc.getgob(id); if (gob != null) { gob.setattr(new GobHighlight(gob)); } } catch (NumberFormatException ignored) {} return null; } } } catch(Exception ignored) {} return text; } }