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 java.awt.Font("SansSerif", java.awt.Font.PLAIN, 14), new java.awt.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>(); 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); } 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 abstract static class Channel extends Widget { public final List<Message> msgs = new LinkedList<Message>(); private final Scrollbar sb; public IButton cbtn; public abstract static 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) { super(c, sz, parent); sb = new Scrollbar(new Coord(sz.x, 0), ih(), this, 0, -ih()); cbtn = new IButton(Coord.z, this, cbtni[0], cbtni[1], cbtni[2]); cbtn.recthit = true; cbtn.c = new Coord(sz.x - cbtn.sz.x - sb.sz.x - 3, 0); } public Channel(Widget parent) { this(new Coord(selw, 0), parent.sz.sub(selw, 0), parent); } public void append(Message msg) { 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; } } public void append(String line, Color col) { append(new SimpleMessage(line, col, iw())); } 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)) WebBrowser.self.show(url.url); } 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)); } 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 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); this.name = name; } public String name() { return (name); } } public abstract static class EntryChannel extends Channel { private final TextEntry in; public EntryChannel(Widget parent) { super(parent); 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(""); } }; } 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) { wdgmsg("msg", text); } } public static class MultiChat extends EntryChannel { private final String name; private final boolean notify; 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; } public void uimsg(String msg, Object... args) { if (msg == "msg") { Integer from = (Integer) args[0]; String line = (String) args[1]; if (from == null) { append(new MyMessage(line, iw())); } else { Message cmsg = new NamedMessage(from, line, Color.WHITE, iw()); append(cmsg); if (notify) notify(cmsg); } } } 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 = (String) args[2]; 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())); } else { Message cmsg = new NamedMessage(from, line, col, iw()); append(cmsg); notify(cmsg); } } } } 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 = (String) args[1]; if (t.equals("in")) { Message cmsg = new InMessage(line, iw()); append(cmsg); notify(cmsg); } else if (t.equals("out")) { append(new OutMessage(line, iw())); } } else if (msg == "err") { String err = (String) args[0]; Message cmsg = new SimpleMessage(err, Color.RED, iw()); append(cmsg); notify(cmsg); } } public String name() { BuddyWnd.Buddy b = getparent(GameUI.class).buddies.find(other); if (b == null) return ("???"); else return (b.name); } } static { addtype( "mchat", new WidgetFactory() { 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)); } }); addtype( "pchat", new WidgetFactory() { public Widget create(Coord c, Widget parent, Object[] args) { return (new PartyChat(parent)); } }); addtype( "pmchat", new WidgetFactory() { 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", 10); private final List<DarkChannel> chls = new ArrayList<DarkChannel>(); private int s = 0; private class DarkChannel { public final Channel chan; public Text rname; private DarkChannel(Channel chan) { this.chan = chan; } } 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.rname = nf.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); } private void expand() { resize(new Coord(sz.x, 100)); setcanfocus(true); if (sel != null) sel.show(); chansel.show(); expanded = true; } private void contract() { 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)); } }