0977ada9bdd30f84027bc0505140feb15259bda1
[org.ibex.core.git] / src / org / xwt / Surface.java
1 // Copyright 2003 Adam Megacz, see the COPYING file for licensing [GPL]
2 package org.xwt;
3
4 import org.bouncycastle.util.encoders.Base64;
5 import org.xwt.js.*;
6 import org.xwt.util.*;
7 import java.io.*;
8 import java.util.*;
9
10 /** 
11  *  A Surface, as described in the XWT Reference.
12  *
13  *  Platform subclasses should include an inner class subclass of
14  *  Surface to return from the Platform._createSurface() method
15  *
16  *  Note that the members in the section 'state variables' are either
17  *  in real-time (the actual size/position/state), or in
18  *  Scheduler-time (the size/position/state at the time that the
19  *  now-executing message was enqueued). This distinction is important.
20  */
21 public abstract class Surface extends PixelBuffer {
22
23     // Static Data ////////////////////////////////////////////////////////////////////////////////
24
25     /**< the most recently enqueued Move message; used to throttle the message rate */
26     private static Scheduler.Task lastMoveMessage = null;
27
28     /** all instances of Surface which need to be refreshed by the Scheduler */
29     public static Vec allSurfaces = new Vec();
30     
31     /** When set to true, render() should abort as soon as possible and restart the rendering process */
32     static volatile boolean abort = false;
33
34     public static boolean alt = false;          ///< true iff the alt button is pressed down, in real time
35     public static boolean control = false;      ///< true iff the control button is pressed down, in real time
36     public static boolean shift = false;        ///< true iff the shift button is pressed down, in real time
37     public static boolean button1 = false;      ///< true iff button 1 is depressed, in Scheduler-time
38     public static boolean button2 = false;      ///< true iff button 2 is depressed, in Scheduler-time
39     public static boolean button3 = false;      ///< true iff button 3 is depressed, in Scheduler-time
40
41      
42
43     // Instance Data ///////////////////////////////////////////////////////////////////////
44
45     public Box root;      /**< The Box at the root of this surface */
46     public String cursor = "default";
47
48     public int mousex;                    ///< the x position of the mouse, relative to this Surface, in Scheduler-time
49     public int mousey;                    ///< the y position of the mouse, relative to this Surface, in Scheduler-time
50     public boolean minimized = false;     ///< True iff this surface is minimized, in real time
51     public boolean maximized = false;     ///< True iff this surface is maximized, in real time
52
53     /** Dirty regions on the backbuffer which need to be rebuilt using Box.render() */
54     private DirtyList dirtyRegions = new DirtyList();
55
56
57     // Used For Simulating Clicks and DoubleClicks /////////////////////////////////////////////////
58
59     int last_press_x = Integer.MAX_VALUE;      ///< the x-position of the mouse the last time a Press message was enqueued
60     int last_press_y = Integer.MAX_VALUE;      ///< the y-position of the mouse the last time a Press message was enqueued
61     static int lastClickButton = 0;            ///< the last button to recieve a Click message; used for simulating DoubleClick's
62     static long lastClickTime = 0;             ///< the last time a Click message was processed; used for simulating DoubleClick's
63     
64     
65     // Methods to be overridden by subclasses ///////////////////////////////////////////////////////
66
67     public abstract void toBack();      ///< when invoked, the surface should push itself to the back of the stacking order
68     public abstract void toFront();     ///< when invoked, the surface should pull itself to the front of the stacking order
69     public abstract void syncCursor();  ///< the <i>actual</i> cursor for this surface to the cursor referenced by <tt>cursor</tt>
70     public abstract void setInvisible(boolean b);      ///< If <tt>b</tt>, make window invisible; otherwise, make it non-invisible.
71     protected abstract void _setMaximized(boolean b);  ///< If <tt>b</tt>, maximize the surface; otherwise, un-maximize it.
72     protected abstract void _setMinimized(boolean b);  ///< If <tt>b</tt>, minimize the surface; otherwise, un-minimize it.
73     public abstract void setLocation();                      ///< Set the surface's x/y position to that of the root box
74     public abstract void setTitleBarText(String s);      ///< Sets the surface's title bar text, if applicable
75     public abstract void setIcon(Picture i);      ///< Sets the surface's title bar text, if applicable
76     public abstract void _dispose();      ///< Destroy the surface
77
78
79
80     // Sizing /////////////////////////////////////////////////////////////////////////////////
81
82     private int width = 0;
83     private int height = 0;
84     public int getWidth() { return width; }
85     public int getHeight() { return height; }
86
87     public void setLimits(int min_width, int min_height, int max_width, int max_height) { }
88
89     /// should be overriden by subclasses; programmatically sets the height
90     protected abstract void _setSize(int width, int height); 
91
92     public final void setWidth(int width) { setSize(width, this.height); }
93     public final void setHeight(int height) { setSize(this.width, height); }
94     protected final void setSize(int width, int height) {
95         if (this.width == width && this.height == height) return;
96         this.width = Math.max(Main.scarImage.getWidth(), width);
97         this.height = Math.max(Main.scarImage.getHeight(), height);
98         _setSize(width, height);
99     }
100
101
102     // Helper methods for subclasses ////////////////////////////////////////////////////////////
103
104     protected final void Press(final int button) {
105         last_press_x = mousex;
106         last_press_y = mousey;
107
108         if (button == 1) button1 = true;
109         else if (button == 2) button2 = true;
110         else if (button == 3) button3 = true;
111
112         if (button == 1) new SimpleMessage("Press1", Boolean.TRUE, Box.whoIs(root, mousex, mousey));
113         else if (button == 2) new SimpleMessage("Press2", Boolean.TRUE, Box.whoIs(root, mousex, mousey));
114         else if (button == 3) {
115             final Box who = Box.whoIs(root, mousex, mousey);
116             Scheduler.add(new Scheduler.Task() { public void perform() {
117                 Platform.clipboardReadEnabled = true;
118                 root.putAndTriggerTraps("Press3", Boolean.TRUE);
119                 Platform.clipboardReadEnabled = false;
120             }});
121         }
122     }
123
124     protected final void Release(int button) {
125         if (button == 1) button1 = false;
126         else if (button == 2) button2 = false;
127         else if (button == 3) button3 = false;
128
129         if (button == 1) new SimpleMessage("Release1", Boolean.TRUE, Box.whoIs(root, mousex, mousey));
130         else if (button == 2) new SimpleMessage("Release2", Boolean.TRUE, Box.whoIs(root, mousex, mousey));
131         else if (button == 3) new SimpleMessage("Release3", Boolean.TRUE, Box.whoIs(root, mousex, mousey));
132
133         if (Platform.needsAutoClick() && Math.abs(last_press_x - mousex) < 5 && Math.abs(last_press_y - mousey) < 5) Click(button);
134         last_press_x = Integer.MAX_VALUE;
135         last_press_y = Integer.MAX_VALUE;
136     }
137
138     protected final void Click(int button) {
139         if (button == 1) new SimpleMessage("Click1", Boolean.TRUE, Box.whoIs(root, mousex, mousey));
140         else if (button == 2) new SimpleMessage("Click2", Boolean.TRUE, Box.whoIs(root, mousex, mousey));
141         else if (button == 3) new SimpleMessage("Click3", Boolean.TRUE, Box.whoIs(root, mousex, mousey));
142         if (Platform.needsAutoDoubleClick()) {
143             long now = System.currentTimeMillis();
144             if (lastClickButton == button && now - lastClickTime < 350) DoubleClick(button);
145             lastClickButton = button;
146             lastClickTime = now;
147         }
148     }
149
150     protected final void DoubleClick(int button) {
151         if (button == 1) new SimpleMessage("DoubleClick1", Boolean.TRUE, Box.whoIs(root, mousex, mousey));
152         else if (button == 2) new SimpleMessage("DoubleClick2", Boolean.TRUE, Box.whoIs(root, mousex, mousey));
153         else if (button == 3) new SimpleMessage("DoubleClick3", Boolean.TRUE, Box.whoIs(root, mousex, mousey));
154     }
155
156     /** Sends a KeyPressed message; subclasses should not add the C- or A- prefixes,
157      *  nor should they capitalize alphabet characters
158      */
159     protected final void KeyPressed(String key) {
160         if (key == null) return;
161
162         if (key.toLowerCase().endsWith("shift")) shift = true;
163         else if (shift) key = key.toUpperCase();
164
165         if (key.toLowerCase().equals("alt")) alt = true;
166         else if (alt) key = "A-" + key;
167
168         if (key.toLowerCase().endsWith("control")) control = true;
169         else if (control) key = "C-" + key;
170
171         final String fkey = key;
172         Scheduler.add(new KMessage(key));
173     }
174
175     // This is implemented as a private static class instead of an anonymous class to work around a GCJ bug
176     private class KMessage extends Scheduler.Task {
177         String key = null;
178         public KMessage(String k) { key = k; }
179         public void perform() {
180             if (key.equals("C-v") || key.equals("A-v")) Platform.clipboardReadEnabled = true;
181             outer: for(int i=0; i<keywatchers.size(); i++) {
182                 Box b = (Box)keywatchers.elementAt(i);
183                 for(Box cur = b; cur != null; cur = cur.parent)
184                     if (!cur.test(cur.VISIBLE)) continue outer;
185                 b.putAndTriggerTraps("KeyPressed", key);
186             }
187             Platform.clipboardReadEnabled = false;
188         }
189     }
190
191     Vec keywatchers = new Vec();
192
193     /** sends a KeyReleased message; subclasses should not add the C- or A- prefixes,
194      *  nor should they capitalize alphabet characters */
195     protected final void KeyReleased(final String key) {
196         if (key == null) return;
197         if (key.toLowerCase().equals("alt")) alt = false;
198         else if (key.toLowerCase().equals("control")) control = false;
199         else if (key.toLowerCase().equals("shift")) shift = false;
200         Scheduler.add(new Scheduler.Task() { public void perform() {
201             outer: for(int i=0; i<keywatchers.size(); i++) {
202                 Box b = (Box)keywatchers.elementAt(i);
203                 for(Box cur = b; cur != null; cur = cur.parent)
204                     if (!cur.test(cur.VISIBLE)) continue outer;
205                 b.putAndTriggerTraps("KeyReleased", key);
206             }
207         }});
208     }
209
210     /**
211      *  Notify XWT that the mouse has moved. If the mouse leaves the
212      *  surface, but the host windowing system does not provide its new
213      *  position (for example, a Java MouseListener.mouseExited()
214      *  message), the subclass should use (-1,-1).
215      */
216     protected final void Move(final int newmousex, final int newmousey) {
217         Scheduler.add(lastMoveMessage = new Scheduler.Task() { public void perform() {
218             synchronized(Surface.this) {
219
220                 // if move messages are arriving faster than we can process them, we just start ignoring them
221                 if (lastMoveMessage != this) return;
222
223                 int oldmousex = mousex;
224                 int oldmousey = mousey;
225                 mousex = newmousex;
226                 mousey = newmousey;
227
228                 String oldcursor = cursor;
229                 cursor = "default";
230
231                 // Root gets motion events outside itself (if trapped, of course)
232                 if (!root.inside(oldmousex, oldmousey) && !root.inside(mousex, mousey) && (button1 || button2 || button3))
233                     root.putAndTriggerTraps("Move", Boolean.TRUE);
234
235                 root.Move(oldmousex, oldmousey, mousex, mousey);
236                 if (!cursor.equals(oldcursor)) syncCursor();
237             }
238         }});
239     }
240
241     protected final void SizeChange(final int width, final int height) {
242         Scheduler.add(new Scheduler.Task() { public void perform() {
243             root.set(root.REFLOW);
244             Surface.this.width = width;
245             Surface.this.height = height;
246         }});
247         abort = true;
248     }
249
250     protected final void PosChange(final int x, final int y) {
251         Scheduler.add(new Scheduler.Task() { public void perform() {
252             root.x = x;
253             root.y = y;
254             root.putAndTriggerTraps("PosChange", Boolean.TRUE);
255         }});
256     }
257
258     protected final void Close() { new SimpleMessage("Close", Boolean.TRUE, root); }
259     protected final void Minimized(boolean b) { minimized = b; new SimpleMessage("Minimized", b ? Boolean.TRUE : Boolean.FALSE, root); }
260     protected final void Maximized(boolean b) { maximized = b; new SimpleMessage("Maximized", b ? Boolean.TRUE : Boolean.FALSE, root); }
261     protected final void Focused(boolean b) { new SimpleMessage("Focused", b ? Boolean.TRUE : Boolean.FALSE, root); }
262     public static void Refresh() {
263         Scheduler.add(new Scheduler.Task() { public void perform() {
264             renderAll();
265         }}); }
266
267     public static void renderAll() {
268         for(int i=0; i<allSurfaces.size(); i++)
269             ((Surface)allSurfaces.elementAt(i)).render();
270     }
271
272     public final void setMaximized(boolean b) { if (b != maximized) _setMaximized(maximized = b); }
273     public final void setMinimized(boolean b) { if (b != minimized) _setMinimized(minimized = b); }
274
275
276     // Other Methods ///////////////////////////////////////////////////////////////////////////////
277
278     /** Indicates that the Surface is no longer needed */
279     public final void dispose(boolean quitIfAllSurfacesGone) {
280         if (Log.on) Log.log(this, "disposing " + this);
281         allSurfaces.removeElement(this);
282         _dispose();
283         if (allSurfaces.size() == 0) {
284             if (Log.on) Log.log(this, "exiting because last surface was destroyed");
285             System.exit(0);
286         }
287     }
288
289     public void dirty(int x, int y, int w, int h) {
290         dirtyRegions.dirty(x, y, w, h);
291         Refresh();
292     }
293
294     public static Surface fromBox(Box b) {
295         for(int i=0; i<allSurfaces.size(); i++) {
296             Surface s = (Surface)allSurfaces.elementAt(i);
297             if (s.root == b) return s;
298         }
299         return null;
300     }
301
302     public Surface(Box root) {
303         this.root = root;
304         Surface old = fromBox(root);
305         if (old != null) old.dispose(false);
306         else try {
307             root.put("thisbox", null);
308         } catch (JSExn e) {
309             throw new Error("this should never happen");
310         }
311         Refresh();
312     }
313
314     private static VectorGraphics.Affine identity = VectorGraphics.Affine.identity();
315
316     /** runs the prerender() and render() pipelines in the root Box to regenerate the backbuffer, then blits it to the screen */
317     public synchronized void render() {
318
319         // FIXME ugly
320         if (root.width != width || root.height != height)
321             root.dirty(0, root.height - Main.scarImage.getHeight(), Main.scarImage.getWidth(), Main.scarImage.getHeight());
322
323         // make sure the root is properly sized
324         do {
325             abort = false;
326             root.reflow(width, height);
327             setSize(width, height);
328             // update mouseinside and trigger Enter/Leave as a result of box size/position changes
329             String oldcursor = cursor;
330             cursor = "default";
331             root.Move(mousex, mousey, mousex, mousey);
332             if (!cursor.equals(oldcursor)) syncCursor();
333         } while(abort);
334
335         //Box.sizePosChangesSinceLastRender = 0;
336         int[][] dirt = dirtyRegions.flush();
337         for(int i = 0; dirt != null && i < dirt.length; i++) {
338             if (dirt[i] == null) continue;
339             int x = dirt[i][0], y = dirt[i][1], w = dirt[i][2], h = dirt[i][3];
340             if (x < 0) x = 0;
341             if (y < 0) y = 0;
342             if (x+w > root.width) w = root.width - x;
343             if (y+h > root.height) h = root.height - y;
344             if (w <= 0 || h <= 0) continue;
345
346             root.render(0, 0, x, y, x + w, y + h, this, identity);
347             drawPicture(Main.scarImage,
348                         0, root.height - Main.scarImage.getHeight(), 
349                         x, y, w, h);
350             
351             if (abort) {
352
353                 // x,y,w,h is only partially reconstructed, so we must be careful not to re-blit it
354                 dirtyRegions.dirty(x, y, w, h);
355
356                 // put back all the dirty regions we haven't yet processed (including the current one)
357                 for(int j=i; j<dirt.length; j++)
358                     if (dirt[j] != null)
359                         dirtyRegions.dirty(dirt[j][0], dirt[j][1], dirt[j][2], dirt[j][3]);
360
361                 // tail-recurse
362                 render();
363                 return;
364             }
365         }
366     }
367
368     // FEATURE: reinstate recycler
369     public class SimpleMessage extends Scheduler.Task {
370         
371         private Box boxContainingMouse;
372         private Object value;
373         public String name;
374         
375         SimpleMessage(String name, Object value, Box boxContainingMouse) {
376             this.boxContainingMouse = boxContainingMouse;
377             this.name = name;
378             this.value = value;
379             Scheduler.add(this);
380         }
381         
382         public void perform() { boxContainingMouse.putAndTriggerTraps(name, value); }
383         public String toString() { return "SimpleMessage [name=" + name + ", value=" + value + "]"; }
384
385     }
386
387
388     // Default PixelBuffer implementation /////////////////////////////////////////////////////////
389
390     public static abstract class DoubleBufferedSurface extends Surface {
391
392         public DoubleBufferedSurface(Box root) { super(root); }
393         PixelBuffer backbuffer = Platform.createPixelBuffer(Platform.getScreenWidth(), Platform.getScreenHeight(), this);
394         DirtyList screenDirtyRegions = new DirtyList();
395
396         public void drawPicture(Picture source, int dx, int dy, int cx1, int cy1, int cx2, int cy2) {
397             screenDirtyRegions.dirty(cx1, cy1, cx2 - cx1, cy2 - cy1);
398             backbuffer.drawPicture(source, dx, dy, cx1, cy1, cx2, cy2);
399         }
400
401         public void drawPictureAlphaOnly(Picture source, int dx, int dy, int cx1, int cy1, int cx2, int cy2, int argb) {
402             screenDirtyRegions.dirty(cx1, cy1, cx2 - cx1, cy2 - cy1);
403             backbuffer.drawPictureAlphaOnly(source, dx, dy, cx1, cy1, cx2, cy2, argb);
404         }
405
406         public void fillTrapezoid(int x1, int x2, int y1, int x3, int x4, int y2, int color) {
407             screenDirtyRegions.dirty(Math.min(x1, x3), y1, Math.max(x2, x4) - Math.min(x1, x3), y2 - y1);
408             backbuffer.fillTrapezoid(x1, x2, y1, x3, x4, y2, color);
409         }
410
411         public void render() {
412             super.render();
413             render_();
414         }
415
416         public void render_() {
417             int[][] dirt = screenDirtyRegions.flush();
418             for(int i = 0; dirt != null && i < dirt.length; i++) {
419                 if (dirt[i] == null) continue;
420                 int x = dirt[i][0];
421                 int y = dirt[i][1];
422                 int w = dirt[i][2];
423                 int h = dirt[i][3];
424                 if (x < 0) x = 0;
425                 if (y < 0) y = 0;
426                 if (x+w > root.width) w = root.width - x;
427                 if (y+h > root.height) h = root.height - y;
428                 if (w <= 0 || h <= 0) continue;
429                 blit(backbuffer, x, y, x, y, w + x, h + y);
430             }
431         }
432
433         /** This is how subclasses signal a 'shallow dirty', indicating that although the backbuffer is valid, the screen is not */
434         public final void Dirty(int x, int y, int w, int h) {
435             screenDirtyRegions.dirty(x, y, w, h);
436             Refresh();
437         }
438
439         /** copies a region from the doublebuffer to this surface */
440         public abstract void blit(PixelBuffer source, int sx, int sy, int dx, int dy, int dx2, int dy2);
441
442     }
443
444 }