licensing cleanup (GPLv2)
[org.ibex.core.git] / src / org / ibex / graphics / Surface.java
1 // Copyright 2000-2005 the Contributors, as shown in the revision logs.
2 // Licensed under the GNU General Public License version 2 ("the License").
3 // You may not use this file except in compliance with the License.
4
5 package org.ibex.graphics;
6
7 import org.ibex.js.*;
8 import org.ibex.util.*;
9 import org.ibex.plat.*;
10
11 import org.ibex.core.*;  // FIXME
12
13 /** 
14  *  A Surface, as described in the Ibex Reference.
15  *
16  *  Platform subclasses should include an inner class subclass of
17  *  Surface to return from the Platform._createSurface() method
18  */
19 public abstract class Surface extends PixelBuffer implements Task {
20
21     // Static Data ////////////////////////////////////////////////////////////////////////////////
22
23     private static final JS T = JS.T;
24     private static final JS F = JS.F;
25
26     /** all instances of Surface which need to be refreshed by the Scheduler */
27     public static Vec allSurfaces = new Vec();
28     
29     /** When set to true, render() should abort as soon as possible and restart the rendering process */
30     public volatile boolean abort = false;
31
32     // these three variables are used to ensure that user resizes trump programmatic resizes
33     public volatile boolean syncRootBoxToSurface = false;
34     public volatile int pendingWidth = 0;
35     public volatile int pendingHeight = 0;
36
37     public static boolean alt = false;          ///< true iff the alt button is pressed down
38     public static boolean control = false;      ///< true iff the control button is pressed down
39     public static boolean shift = false;        ///< true iff the shift button is pressed down
40     public static boolean button1 = false;      ///< true iff button 1 is depressed
41     public static boolean button2 = false;      ///< true iff button 2 is depressed
42     public static boolean button3 = false;      ///< true iff button 3 is depressed
43
44
45     // Instance Data ///////////////////////////////////////////////////////////////////////
46
47     public Box root;                                   ///< The Box at the root of this surface
48     public String cursor = "default";                  ///< The active cursor to switch to when syncCursor() is called
49     public int mousex;                                 ///< x position of the mouse
50     public int mousey;                                 ///< y position of the mouse
51     public int _mousex;                                ///< x position of the mouse FIXME
52     public int _mousey;                                ///< y position of the mouse FIXME
53     public int newmousex = -1;                         ///< x position of the mouse, in real time; this lets us collapse Move's
54     public int newmousey = -1;                         ///< y position of the mouse, in real time; this lets us collapse Move's
55     public boolean minimized = false;                  ///< True iff this surface is minimized, in real time
56     public boolean maximized = false;                  ///< True iff this surface is maximized, in real time
57     public boolean unrendered = true;                  ///< True iff this surface has not yet been rendered
58     DirtyList dirtyRegions = new DirtyList();          ///< Dirty regions on the surface
59
60     // Used For Simulating Clicks and DoubleClicks /////////////////////////////////////////////////
61
62     int last_press_x = Integer.MAX_VALUE;      ///< the x-position of the mouse the last time a Press message was enqueued
63     int last_press_y = Integer.MAX_VALUE;      ///< the y-position of the mouse the last time a Press message was enqueued
64     static int lastClickButton = 0;            ///< the last button to recieve a Click message; used for simulating DoubleClick's
65     static long lastClickTime = 0;             ///< the last time a Click message was processed; used for simulating DoubleClick's
66     
67     
68     // Methods to be overridden by subclasses ///////////////////////////////////////////////////////
69
70     public abstract void toBack();                     ///< should push surface to the back of the stacking order
71     public abstract void toFront();                    ///< should pull surface to the front of the stacking order
72     public abstract void syncCursor();                 ///< set the actual cursor to this.cursor if they do not match
73     public abstract void setInvisible(boolean b);      ///< If <tt>b</tt>, make window invisible; otherwise, make it non-invisible.
74     protected abstract void _setMaximized(boolean b);  ///< If <tt>b</tt>, maximize the surface; otherwise, un-maximize it.
75     protected abstract void _setMinimized(boolean b);  ///< If <tt>b</tt>, minimize the surface; otherwise, un-minimize it.
76     public abstract void setLocation();                ///< Set the surface's x/y position to that of the root box
77     protected abstract void _setSize(int w, int h);    ///< set the actual size of the surface
78     public abstract void setTitleBarText(String s);    ///< Sets the surface's title bar text, if applicable
79     public abstract void setIcon(Picture i);           ///< Sets the surface's title bar text, if applicable
80     public abstract void _dispose();                   ///< Destroy the surface
81     public void setMinimumSize(int minx, int miny, boolean resizable) { }
82     protected void setSize(int w, int h) { _setSize(w, h); }
83
84     public static Picture scarImage = null;
85
86     // Helper methods for subclasses ////////////////////////////////////////////////////////////
87
88     protected final void Press(final int button) {
89         last_press_x = mousex;
90         last_press_y = mousey;
91
92         if (button == 1) button1 = true;
93         else if (button == 2) button2 = true;
94         else if (button == 3) button3 = true;
95
96         if (button == 1) new Message("_Press1", T, root);
97         else if (button == 2) new Message("_Press2", T, root);
98         else if (button == 3) {
99             Scheduler.add(new Task() { public void perform() throws JSExn {
100                 Platform.clipboardReadEnabled = true;
101                 try {
102                     root.putAndTriggerTraps(JS.S("_Press3"), T);
103                 } finally {
104                     Platform.clipboardReadEnabled = false;
105                 }
106             }});
107         }
108     }
109
110     protected final void Release(int button) {
111         if (button == 1) button1 = false;
112         else if (button == 2) button2 = false;
113         else if (button == 3) button3 = false;
114
115         if (button == 1) new Message("_Release1", T, root);
116         else if (button == 2) new Message("_Release2", T, root);
117         else if (button == 3) new Message("_Release3", T, root);
118
119         if (Platform.needsAutoClick() && Math.abs(last_press_x - mousex) < 5 && Math.abs(last_press_y - mousey) < 5) Click(button);
120         last_press_x = Integer.MAX_VALUE;
121         last_press_y = Integer.MAX_VALUE;
122     }
123
124     protected final void Click(int button) {
125         if (button == 1) new Message("_Click1", T, root);
126         else if (button == 2) new Message("_Click2", T, root);
127         else if (button == 3) new Message("_Click3", T, root);
128         if (Platform.needsAutoDoubleClick()) {
129             long now = System.currentTimeMillis();
130             if (lastClickButton == button && now - lastClickTime < 350) DoubleClick(button);
131             lastClickButton = button;
132             lastClickTime = now;
133         }
134     }
135
136     private final static JS MOVE = JS.S("_Move");
137     /** we enqueue ourselves in the Scheduler when we have a Move message to deal with */
138     private Task mover = new Task() {
139             public void perform() {
140                 if (mousex == newmousex && mousey == newmousey) return;
141                 int oldmousex = mousex;     mousex = newmousex;
142                 int oldmousey = mousey;     mousey = newmousey;
143                 String oldcursor = cursor;  cursor = "default";
144                 // FIXME: Root (ONLY) gets motion events outside itself (if trapped)
145                 if (oldmousex != mousex || oldmousey != mousey)
146                     root.putAndTriggerTrapsAndCatchExceptions(MOVE, T);
147                 if (!cursor.equals(oldcursor)) syncCursor();
148             } };
149
150     /**
151      *  Notify Ibex that the mouse has moved. If the mouse leaves the
152      *  surface, but the host windowing system does not provide its new
153      *  position (for example, a Java MouseListener.mouseExited()
154      *  message), the subclass should use (-1,-1).
155      */
156     protected final void Move(final int newmousex, final int newmousey) {
157         this.newmousex = newmousex;
158         this.newmousey = newmousey;
159         Scheduler.add(mover);
160     }
161
162     protected final void HScroll(int pixels) { new Message("_HScroll", JS.N(pixels), root); }
163     protected final void VScroll(int pixels) { new Message("_VScroll", JS.N(pixels), root); }
164     protected final void HScroll(float lines) { new Message("_HScroll", JS.N(lines), root); }
165     protected final void VScroll(float lines) { new Message("_VScroll", JS.N(lines), root); }
166
167     /** subclasses should invoke this method when the user resizes the window */
168     protected final void SizeChange(final int width, final int height) {
169         if (unrendered || (pendingWidth == width && pendingHeight == height)) return;
170         pendingWidth = width;
171         pendingHeight = height;
172         syncRootBoxToSurface = true;
173         abort = true;
174         Scheduler.renderAll();
175     }
176
177     // FEATURE: can we avoid creating objects here?
178     protected final void PosChange(final int x, final int y) {
179         Scheduler.add(new Task() { public void perform() throws JSExn {
180             root.x = x;
181             root.y = y;
182             root.putAndTriggerTrapsAndCatchExceptions(JS.S("PosChange"), T);
183         }});
184     }
185
186     private final String[] doubleClick = new String[] { null, "_DoubleClick1", "_DoubleClick2", "_DoubleClick3" };
187     protected final void DoubleClick(int button) { new Message(doubleClick[button], T, root); }
188     protected final void KeyPressed(String key) { new Message("_KeyPressed", JS.S(key), root); }
189     protected final void KeyReleased(String key) { new Message("_KeyReleased", JS.S(key), root); }
190     protected final void Close() { new Message("Close", T, root); }
191     protected final void Minimized(boolean b) { minimized = b; new Message("Minimized", b ? T : F, root); }
192     protected final void Maximized(boolean b) { maximized = b; new Message("Maximized", b ? T : F, root); }
193     protected final void Focused(boolean b) { new Message("Focused", b ? T : F, root); }
194
195     private boolean scheduled = false;
196     public void Refresh() { if (!scheduled) Scheduler.add(this); scheduled = true; }
197     public void perform() { scheduled = false; Scheduler.renderAll(); }
198
199     public final void setMaximized(boolean b) { if (b != maximized) _setMaximized(maximized = b); }
200     public final void setMinimized(boolean b) { if (b != minimized) _setMinimized(minimized = b); }
201
202
203     // Other Methods ///////////////////////////////////////////////////////////////////////////////
204
205     /** Indicates that the Surface is no longer needed */
206     public final void dispose(boolean quitIfAllSurfacesGone) {
207         if (Log.on) Log.info(this, "disposing " + this);
208         allSurfaces.removeElement(this);
209         _dispose();
210         if (allSurfaces.size() == 0) {
211             if (Log.on) Log.info(this, "exiting because last surface was destroyed");
212             System.exit(0);
213         }
214     }
215
216     public void dirty(int x, int y, int w, int h) {
217         dirtyRegions.dirty(x, y, w, h);
218         Refresh();
219     }
220
221     public static Surface fromBox(Box b) {
222         // FIXME use a hash table here
223         for(int i=0; i<allSurfaces.size(); i++) {
224             Surface s = (Surface)allSurfaces.elementAt(i);
225             if (s.root == b) return s;
226         }
227         return null;
228     }
229
230     public Surface(Box root) {
231         this.root = root;
232         // FIXME: document this in the reference
233         if (!root.test(root.HSHRINK) && root.maxwidth == Integer.MAX_VALUE)
234             root.maxwidth = Platform.getScreenWidth() / 2;
235         if (!root.test(root.VSHRINK) && root.maxheight == Integer.MAX_VALUE)
236             root.maxheight = Platform.getScreenHeight() / 2;
237         root.setWidth(root.minwidth,
238                       root.test(root.HSHRINK)
239                       ? Math.max(root.minwidth, root.contentwidth)
240                       : Math.min(Platform.getScreenWidth(), root.maxwidth));
241         root.setHeight(root.minheight,
242                       root.test(root.VSHRINK)
243                       ? Math.max(root.minheight, root.contentheight)
244                       : Math.min(Platform.getScreenHeight(), root.maxheight));
245         Surface old = fromBox(root);
246         if (old != null) old.dispose(false);
247         else root.removeSelf();
248         Refresh();
249     }
250
251     private static Affine identity = Affine.identity();
252
253     /** runs the prerender() and render() pipelines in the root Box to regenerate the backbuffer, then blits it to the screen */
254     public synchronized void render() {
255         scheduled = false;
256         // make sure the root is properly sized
257         do {
258             abort = false;
259             root.pack();
260             if (syncRootBoxToSurface) {
261                 root.setWidth(root.minwidth, pendingWidth);
262                 root.setHeight(root.minheight, pendingHeight);
263                 syncRootBoxToSurface = false;
264             }
265             int rootwidth = root.test(root.HSHRINK) ? root.contentwidth : root.maxwidth;
266             int rootheight = root.test(root.VSHRINK) ? root.contentheight : root.maxheight;
267             if (rootwidth != root.width || rootheight != root.height) {
268                 // dirty the place where the scar used to be and where it is now
269                 dirty(0, root.height - scarImage.height, scarImage.width, scarImage.height);
270                 dirty(0, rootheight - scarImage.height, scarImage.width, scarImage.height);
271             }
272             root.reflow();
273             setSize(rootwidth, rootheight);
274             /*String oldcursor = cursor;
275             cursor = "default";
276             root.putAndTriggerTrapsAndCatchExceptions("_Move", JS.T);
277             if (!cursor.equals(oldcursor)) syncCursor();*/
278         } while(abort);
279
280         int[][] dirt = dirtyRegions.flush();
281         for(int i = 0; dirt != null && i < dirt.length; i++) {
282             if (dirt[i] == null) continue;
283             int x = dirt[i][0], y = dirt[i][1], w = dirt[i][2], h = dirt[i][3];
284             if (x < 0) x = 0;
285             if (y < 0) y = 0;
286             if (x+w > root.width) w = root.width - x;
287             if (y+h > root.height) h = root.height - y;
288             if (w <= 0 || h <= 0) continue;
289
290             root.render(0, 0, x, y, x + w, y + h, this, identity);
291             drawPicture(scarImage, 0, root.height - scarImage.height, x, y, x+w, y+h);
292             
293             if (abort) {
294                 // x,y,w,h is only partially reconstructed, so we must be careful not to re-blit it
295                 dirtyRegions.dirty(x, y, w, h);
296                 // put back all the dirty regions we haven't yet processed (including the current one)
297                 for(int j=i; j<dirt.length; j++)
298                     if (dirt[j] != null)
299                         dirtyRegions.dirty(dirt[j][0], dirt[j][1], dirt[j][2], dirt[j][3]);
300                 return;
301             }
302         }
303
304         unrendered = false;
305     }
306
307     // FEATURE: reinstate recycler
308     public class Message implements Task {
309         
310         private Box boxContainingMouse;
311         private JS value;
312         public String name;
313         
314         Message(String name, JS value, Box boxContainingMouse) {
315             this.boxContainingMouse = boxContainingMouse;
316             this.name = name;
317             this.value = value;
318             Scheduler.add(this);
319         }
320         
321         public void perform() throws JSExn {
322             if (name.equals("_KeyPressed")) {
323                 String value = JS.toString(this.value);
324                 if (value.toLowerCase().endsWith("shift")) shift = true;     else if (shift) value = value.toUpperCase();
325                 if (value.toLowerCase().equals("alt")) alt = true;           else if (alt) value = "A-" + value;
326                 if (value.toLowerCase().endsWith("control")) control = true; else if (control) value = "C-" + value;
327                 if (value.equals("C-v") || value.equals("A-v")) Platform.clipboardReadEnabled = true;
328                 this.value = JS.S(value);
329             } else if (name.equals("_KeyReleased")) {
330                 String value = JS.toString(this.value);
331                 if (value.toLowerCase().equals("alt")) alt = false;
332                 else if (value.toLowerCase().equals("control")) control = false;
333                 else if (value.toLowerCase().equals("shift")) shift = false;
334                 this.value = JS.S(value);
335             } else if (name.equals("_HScroll") || name.equals("_VScroll")) {
336                 // FIXME: technically points != pixels
337                 if (JS.isInt(value))
338                     value = JS.N(JS.toInt(value) * root.fontSize());
339             }
340             try {
341                 boxContainingMouse.putAndTriggerTrapsAndCatchExceptions(JS.S(name), value);
342             } finally {
343                 Platform.clipboardReadEnabled = false;
344             }
345         }
346         public String toString() { return "Message [name=" + name + ", value=" + JS.debugToString(value) + "]"; }
347     }
348
349
350     // Default PixelBuffer implementation /////////////////////////////////////////////////////////
351
352     public static abstract class DoubleBufferedSurface extends Surface {
353
354         public DoubleBufferedSurface(Box root) { super(root); }
355         PixelBuffer backbuffer = Platform.createPixelBuffer(Platform.getScreenWidth(), Platform.getScreenHeight(), this);
356         DirtyList screenDirtyRegions = new DirtyList();
357
358         public void drawPicture(Picture source, int dx, int dy, int cx1, int cy1, int cx2, int cy2) {
359             screenDirtyRegions.dirty(cx1, cy1, cx2 - cx1, cy2 - cy1);
360             backbuffer.drawPicture(source, dx, dy, cx1, cy1, cx2, cy2);
361         }
362
363         public void drawGlyph(Font.Glyph source, int dx, int dy, int cx1, int cy1, int cx2, int cy2, int argb) {
364             screenDirtyRegions.dirty(cx1, cy1, cx2 - cx1, cy2 - cy1);
365             backbuffer.drawGlyph(source, dx, dy, cx1, cy1, cx2, cy2, argb);
366         }
367
368         public void fillTrapezoid(int x1, int x2, int y1, int x3, int x4, int y2, int color) {
369             screenDirtyRegions.dirty(Math.min(x1, x3), y1, Math.max(x2, x4) - Math.min(x1, x3), y2 - y1);
370             backbuffer.fillTrapezoid(x1, x2, y1, x3, x4, y2, color);
371         }
372
373         public void render() {
374             super.render();
375             if (abort) return;
376             int[][] dirt = screenDirtyRegions.flush();
377             for(int i = 0; dirt != null && i < dirt.length; i++) {
378                 if (dirt[i] == null) continue;
379                 int x = dirt[i][0];
380                 int y = dirt[i][1];
381                 int w = dirt[i][2];
382                 int h = dirt[i][3];
383                 if (x < 0) x = 0;
384                 if (y < 0) y = 0;
385                 if (x+w > root.width) w = root.width - x;
386                 if (y+h > root.height) h = root.height - y;
387                 if (w <= 0 || h <= 0) continue;
388                 if (abort) return;
389                 blit(backbuffer, x, y, x, y, w + x, h + y);
390             }
391         }
392
393         /** This is how subclasses signal a 'shallow dirty', indicating that although the backbuffer is valid, the screen is not */
394         public final void Dirty(int x, int y, int w, int h) {
395             screenDirtyRegions.dirty(x, y, w, h);
396             Scheduler.renderAll();
397         }
398
399         public void dirty(int x, int y, int w, int h) {
400             screenDirtyRegions.dirty(x, y, w, h);
401             super.dirty(x, y, w, h);
402         }
403
404         /** copies a region from the doublebuffer to this surface */
405         public abstract void blit(PixelBuffer source, int sx, int sy, int dx, int dy, int dx2, int dy2);
406
407     }
408
409 }