/* * 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 java.util.*; import java.lang.annotation.*; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; public class Widget { public UI ui; public Coord c, sz; public Widget next, prev, child, lchild, parent; public boolean focustab = false, focusctl = false, hasfocus = false, visible = true; private boolean canfocus = false, autofocus = false; public boolean canactivate = false, cancancel = false; public Widget focused; public Resource cursor = null; public Object tooltip = null; private Widget prevtt; public final Collection<Anim> anims = new LinkedList<Anim>(); static Map<String, Factory> types = new TreeMap<String, Factory>(); @dolda.jglob.Discoverable @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface RName { public String value(); } @RName("cnt") public static class $Cont implements Factory { public Widget create(Coord c, Widget parent, Object[] args) { return(new Widget(c, (Coord)args[0], parent)); } } @RName("ccnt") public static class $CCont implements Factory { public Widget create(Coord c, Widget parent, Object[] args) { Widget ret = new Widget(c, (Coord)args[0], parent) { public void presize() { c = parent.sz.div(2).sub(sz.div(2)); } }; ret.presize(); return(ret); } } @Resource.PublishedCode(name = "wdg") public interface Factory { public Widget create(Coord c, Widget parent, Object[] par); } private static boolean inited = false; public static void initnames() { if(!inited) { for(Factory f : dolda.jglob.Loader.get(RName.class).instances(Factory.class)) { synchronized(types) { types.put(f.getClass().getAnnotation(RName.class).value(), f); } } inited = true; } } public static Factory gettype2(String name) throws InterruptedException { if(name.indexOf('/') < 0) { synchronized(types) { return(types.get(name)); } } else { int ver = -1, p; if((p = name.indexOf(':')) > 0) { ver = Integer.parseInt(name.substring(p + 1)); name = name.substring(0, p); } Resource res = Resource.load(name, ver); while(true) { try { return(res.getcode(Factory.class, true)); } catch(Resource.Loading l) { l.res.loadwaitint(); } } } } public static Factory gettype(String name) { long start = System.currentTimeMillis(); Factory f; try { f = gettype2(name); } catch(InterruptedException e) { /* XXX: This is not proper behavior. On the other hand, * InterruptedException should not be checked. :-/ */ throw(new RuntimeException("Interrupted while loading resource widget (took " + (System.currentTimeMillis() - start) + " ms)", e)); } if(f == null) throw(new RuntimeException("No such widget type: " + name)); return(f); } public Widget(UI ui, Coord c, Coord sz) { this.ui = ui; this.c = c; this.sz = sz; } public Widget(Coord c, Coord sz, Widget parent) { synchronized(parent.ui) { this.ui = parent.ui; this.c = c; this.sz = sz; this.parent = parent; link(); parent.newchild(this); } } private Coord relpos(String spec, Object[] args, int off) { int i = 0; Stack<Object> st = new Stack<Object>(); while(i < spec.length()) { char op = spec.charAt(i++); if(Character.isDigit(op)) { int e; for(e = i; (e < spec.length()) && Character.isDigit(spec.charAt(e)); e++); st.push(Integer.parseInt(spec.substring(i - 1, e))); i = e; } else if(op == '!') { st.push(args[off++]); } else if(op == '_') { st.push(st.peek()); } else if(op == '.') { st.pop(); } else if(op == '^') { Object a = st.pop(); Object b = st.pop(); st.push(a); st.push(b); } else if(op == 'c') { int y = (Integer)st.pop(); int x = (Integer)st.pop(); st.push(new Coord(x, y)); } else if(op == 'o') { Widget w = (Widget)st.pop(); st.push(w.c.add(w.sz)); } else if(op == 'p') { st.push(((Widget)st.pop()).c); } else if(op == 's') { st.push(((Widget)st.pop()).sz); } else if(op == 'w') { synchronized(ui) { st.push(ui.widgets.get((Integer)st.pop())); } } else if(op == 'x') { st.push(((Coord)st.pop()).x); } else if(op == 'y') { st.push(((Coord)st.pop()).y); } else if(op == '+') { Object b = st.pop(); Object a = st.pop(); if((a instanceof Integer) && (b instanceof Integer)) { st.push((Integer)a + (Integer)b); } else if((a instanceof Coord) && (b instanceof Coord)) { st.push(((Coord)a).add((Coord)b)); } else { throw(new RuntimeException("Invalid addition operands: " + a + " + " + b)); } } else if(op == '-') { Object b = st.pop(); Object a = st.pop(); if((a instanceof Integer) && (b instanceof Integer)) { st.push((Integer)a - (Integer)b); } else if((a instanceof Coord) && (b instanceof Coord)) { st.push(((Coord)a).sub((Coord)b)); } else { throw(new RuntimeException("Invalid subtraction operands: " + a + " - " + b)); } } else if(op == '*') { Object b = st.pop(); Object a = st.pop(); if((a instanceof Integer) && (b instanceof Integer)) { st.push((Integer)a * (Integer)b); } else if((a instanceof Coord) && (b instanceof Integer)) { st.push(((Coord)a).mul((Integer)b)); } else if((a instanceof Coord) && (b instanceof Coord)) { st.push(((Coord)a).mul((Coord)b)); } else { throw(new RuntimeException("Invalid multiplication operands: " + a + " - " + b)); } } else if(op == '/') { Object b = st.pop(); Object a = st.pop(); if((a instanceof Integer) && (b instanceof Integer)) { st.push((Integer)a / (Integer)b); } else if((a instanceof Coord) && (b instanceof Integer)) { st.push(((Coord)a).div((Integer)b)); } else if((a instanceof Coord) && (b instanceof Coord)) { st.push(((Coord)a).div((Coord)b)); } else { throw(new RuntimeException("Invalid division operands: " + a + " - " + b)); } } else if(Character.isWhitespace(op)) { } else { throw(new RuntimeException("Unknown position operation: " + op)); } } return((Coord)st.pop()); } public Widget makechild(String type, Object[] pargs, Object[] cargs) { Coord c; if(pargs[0] instanceof Coord) { c = (Coord)pargs[0]; } else if(pargs[0] instanceof String) { c = relpos((String)pargs[0], pargs, 1); } else { throw(new RuntimeException("Unknown child widget creation specification.")); } return(gettype(type).create(c, this, cargs)); } public void newchild(Widget w) { } public void link() { synchronized(ui) { if(parent.lchild != null) parent.lchild.next = this; if(parent.child == null) parent.child = this; this.prev = parent.lchild; parent.lchild = this; } } public void linkfirst() { synchronized(ui) { if(parent.child != null) parent.child.prev = this; if(parent.lchild == null) parent.lchild = this; this.next = parent.child; parent.child = this; } } public void unlink() { synchronized(ui) { if(next != null) next.prev = prev; if(prev != null) prev.next = next; if(parent.child == this) parent.child = next; if(parent.lchild == this) parent.lchild = prev; next = null; prev = null; } } public Coord xlate(Coord c, boolean in) { return(c); } public Coord parentpos(Widget in) { if(in == this) return(new Coord(0, 0)); return(xlate(parent.parentpos(in).add(c), true)); } public Coord rootpos() { return(parentpos(ui.root)); } public Coord rootxlate(Coord c) { return(c.sub(rootpos())); } public boolean hasparent(Widget w2) { for(Widget w = this; w != null; w = w.parent) { if(w == w2) return(true); } return(false); } public void gotfocus() { if(focusctl && (focused != null)) { focused.hasfocus = true; focused.gotfocus(); } } public void reqdestroy() { destroy(); } public void destroy() { if(canfocus) setcanfocus(false); unlink(); parent.cdestroy(this); } public void cdestroy(Widget w) { } public int wdgid() { Integer id = ui.rwidgets.get(this); if(id == null) return(-1); return(id); } public void lostfocus() { if(focusctl && (focused != null)) { focused.hasfocus = false; focused.lostfocus(); } } public void setfocus(Widget w) { if(focusctl) { if(w != focused) { Widget last = focused; focused = w; if(last != null) last.hasfocus = false; w.hasfocus = true; if(last != null) last.lostfocus(); w.gotfocus(); if((ui != null) && ui.rwidgets.containsKey(w) && ui.rwidgets.containsKey(this)) wdgmsg("focus", ui.rwidgets.get(w)); } if((parent != null) && canfocus) parent.setfocus(this); } else { parent.setfocus(w); } } public void setcanfocus(boolean canfocus) { this.autofocus = this.canfocus = canfocus; if(parent != null) { if(canfocus) { parent.newfocusable(this); } else { parent.delfocusable(this); } } } public void newfocusable(Widget w) { if(focusctl) { if(focused == null) setfocus(w); } else { parent.newfocusable(w); } } public void delfocusable(Widget w) { if(focusctl) { if(focused == w) findfocus(); } else { parent.delfocusable(w); } } private void findfocus() { /* XXX: Might need to check subwidgets recursively */ focused = null; for(Widget w = lchild; w != null; w = w.prev) { if(w.visible && w.autofocus) { focused = w; focused.hasfocus = true; w.gotfocus(); break; } } } public void setfocusctl(boolean focusctl) { if(this.focusctl = focusctl) { findfocus(); setcanfocus(true); } } public void setfocustab(boolean focustab) { if(focustab && !focusctl) setfocusctl(true); this.focustab = focustab; } public void uimsg(String msg, Object... args) { if(msg == "tabfocus") { setfocustab(((Integer)args[0] != 0)); } else if(msg == "act") { canactivate = (Integer)args[0] != 0; } else if(msg == "cancel") { cancancel = (Integer)args[0] != 0; } else if(msg == "autofocus") { autofocus = (Integer)args[0] != 0; } else if(msg == "focus") { Widget w = ui.widgets.get((Integer)args[0]); if(w != null) { if(w.canfocus) setfocus(w); } } else if(msg == "curs") { if(args.length == 0) cursor = null; else cursor = Resource.load((String)args[0], (Integer)args[1]); } else if(msg == "tip") { int a = 0; Object tt = args[a++]; if(tt instanceof String) { tooltip = Text.render((String)tt); } else if(tt instanceof Integer) { final Indir<Resource> tres = ui.sess.getres((Integer)tt); tooltip = new Indir<Tex>() { Text t = null; public Tex get() { if(t == null) { Resource.Pagina pag; try { pag = tres.get().layer(Resource.pagina); } catch(Loading e) { return(null); } t = RichText.render(pag.text, 300); } return(t.tex()); } }; } } else { System.err.println("Unhandled widget message: " + msg); } } public void wdgmsg(String msg, Object... args) { wdgmsg(this, msg, args); } public void wdgmsg(Widget sender, String msg, Object... args) { if(parent == null) ui.wdgmsg(sender, msg, args); else parent.wdgmsg(sender, msg, args); } public void tick(double dt) { Widget next; for(Widget wdg = child; wdg != null; wdg = next) { next = wdg.next; wdg.tick(dt); } /* It would be very nice to do these things in harmless mix-in * classes, but alas, this is Java. */ for(Iterator<Anim> i = anims.iterator(); i.hasNext();) { Anim anim = i.next(); if(anim.tick(dt)) i.remove(); } } public void draw(GOut g, boolean strict) { Widget next; for(Widget wdg = child; wdg != null; wdg = next) { next = wdg.next; if(!wdg.visible) continue; Coord cc = xlate(wdg.c, true); GOut g2; if(strict) g2 = g.reclip(cc, wdg.sz); else g2 = g.reclipl(cc, wdg.sz); wdg.draw(g2); } } public void draw(GOut g) { draw(g, true); } public boolean mousedown(Coord c, int button) { for(Widget wdg = lchild; wdg != null; wdg = wdg.prev) { if(!wdg.visible) continue; Coord cc = xlate(wdg.c, true); if(c.isect(cc, wdg.sz)) { if(wdg.mousedown(c.add(cc.inv()), button)) { return(true); } } } return(false); } public boolean mouseup(Coord c, int button) { for(Widget wdg = lchild; wdg != null; wdg = wdg.prev) { if(!wdg.visible) continue; Coord cc = xlate(wdg.c, true); if(c.isect(cc, wdg.sz)) { if(wdg.mouseup(c.add(cc.inv()), button)) { return(true); } } } return(false); } public boolean mousewheel(Coord c, int amount) { for(Widget wdg = lchild; wdg != null; wdg = wdg.prev) { if(!wdg.visible) continue; Coord cc = xlate(wdg.c, true); if(c.isect(cc, wdg.sz)) { if(wdg.mousewheel(c.add(cc.inv()), amount)) { return(true); } } } return(false); } public void mousemove(Coord c) { for(Widget wdg = lchild; wdg != null; wdg = wdg.prev) { if(!wdg.visible) continue; Coord cc = xlate(wdg.c, true); wdg.mousemove(c.add(cc.inv())); } } public boolean globtype(char key, KeyEvent ev) { for(Widget wdg = child; wdg != null; wdg = wdg.next) { if(wdg.globtype(key, ev)) return(true); } return(false); } public boolean type(char key, KeyEvent ev) { if(canactivate) { if(key == 10) { wdgmsg("activate"); return(true); } } if(cancancel) { if(key == 27) { wdgmsg("cancel"); return(true); } } if(focusctl) { if(focused != null) { if(focused.type(key, ev)) return(true); if(focustab) { if(key == '\t') { Widget f = focused; while(true) { if((ev.getModifiers() & InputEvent.SHIFT_MASK) == 0) { Widget n = f.rnext(); f = ((n == null) || !n.hasparent(this))?child:n; } else { Widget p = f.rprev(); f = ((p == null) || !p.hasparent(this))?lchild:p; } if(f.canfocus) break; } setfocus(f); return(true); } else { return(false); } } else { return(false); } } else { return(false); } } else { for(Widget wdg = child; wdg != null; wdg = wdg.next) { if(wdg.visible) { if(wdg.type(key, ev)) return(true); } } return(false); } } public boolean keydown(KeyEvent ev) { if(focusctl) { if(focused != null) { if(focused.keydown(ev)) return(true); return(false); } else { return(false); } } else { for(Widget wdg = child; wdg != null; wdg = wdg.next) { if(wdg.visible) { if(wdg.keydown(ev)) return(true); } } } return(false); } public boolean keyup(KeyEvent ev) { if(focusctl) { if(focused != null) { if(focused.keyup(ev)) return(true); return(false); } else { return(false); } } else { for(Widget wdg = child; wdg != null; wdg = wdg.next) { if(wdg.visible) { if(wdg.keyup(ev)) return(true); } } } return(false); } public Coord contentsz() { Coord max = new Coord(0, 0); for(Widget wdg = child; wdg != null; wdg = wdg.next) { if(!wdg.visible) continue; Coord br = wdg.c.add(wdg.sz); if(br.x > max.x) max.x = br.x; if(br.y > max.y) max.y = br.y; } return(max); } public void pack() { resize(contentsz()); } public void resize(Coord sz) { this.sz = sz; for(Widget ch = child; ch != null; ch = ch.next) ch.presize(); if(parent != null) parent.cresize(this); } public void cresize(Widget ch) { } public void presize() { } public void raise() { synchronized(ui) { unlink(); link(); } } public void lower() { synchronized(ui) { unlink(); linkfirst(); } } @Deprecated public <T extends Widget> T findchild(Class<T> cl) { for(Widget wdg = child; wdg != null; wdg = wdg.next) { if(cl.isInstance(wdg)) return(cl.cast(wdg)); T ret = wdg.findchild(cl); if(ret != null) return(ret); } return(null); } public Widget rprev() { if(lchild != null) return(lchild); if(prev != null) return(prev); return(parent); } public Widget rnext() { if(child != null) return(child); if(next != null) return(next); for(Widget p = parent; p != null; p = p.parent) { if(p.next != null) return(p.next); } return(null); } public <T extends Widget> Set<T> children(final Class<T> cl) { return(new AbstractSet<T>() { public int size() { int i = 0; for(T w : this) i++; return(i); } public Iterator<T> iterator() { return(new Iterator<T>() { T cur = n(Widget.this.child); private T n(Widget w) { Widget n; if(w == null) { return(null); } else if(w.child != null) { n = w.child; } else if(w.next != null) { n = w.next; } else if(w.parent == Widget.this) { return(null); } else { n = w.parent; } if((n == null) || cl.isInstance(n)) return(cl.cast(n)); else return(n(n)); } public T next() { if(cur == null) throw(new NoSuchElementException()); T ret = cur; cur = n(ret); return(ret); } public boolean hasNext() { return(cur != null); } public void remove() { throw(new UnsupportedOperationException()); } }); } }); } public Resource getcurs(Coord c) { Resource ret; for(Widget wdg = lchild; wdg != null; wdg = wdg.prev) { if(!wdg.visible) continue; Coord cc = xlate(wdg.c, true); if(c.isect(cc, wdg.sz)) { if((ret = wdg.getcurs(c.add(cc.inv()))) != null) return(ret); } } return(cursor); } @Deprecated public Object tooltip(Coord c, boolean again) { return(null); } public Object tooltip(Coord c, Widget prev) { if(prev != this) prevtt = null; if(tooltip != null) { prevtt = null; return(tooltip); } for(Widget wdg = lchild; wdg != null; wdg = wdg.prev) { if(!wdg.visible) continue; Coord cc = xlate(wdg.c, true); if(c.isect(cc, wdg.sz)) { Object ret = wdg.tooltip(c.add(cc.inv()), prevtt); if(ret != null) { prevtt = wdg; return(ret); } } } prevtt = null; return(tooltip(c, prev == this)); } public <T extends Widget> T getparent(Class<T> cl) { for(Widget w = this; w != null; w = w.parent) { if(cl.isInstance(w)) return(cl.cast(w)); } return(null); } public void hide() { visible = false; if(canfocus) parent.delfocusable(this); } public void show() { visible = true; if(canfocus) parent.newfocusable(this); } public boolean show(boolean show) { if(show) show(); else hide(); return(show); } public boolean tvisible() { for(Widget w = this; w != null; w = w.parent) { if(!w.visible) return(false); } return(true); } public abstract class Anim { public Anim() { synchronized(ui) { anims.add(this); } } public void clear() { synchronized(ui) { anims.remove(this); } } public abstract boolean tick(double dt); } public abstract class NormAnim extends Anim { private double a = 0.0; private final double s; public NormAnim(double s) { this.s = 1.0 / s; } public boolean tick(double dt) { a += dt; double na = a * s; if(na >= 1.0) { ntick(1.0); return(true); } else { ntick(na); return(false); } } public abstract void ntick(double a); } }