root/trunk/luigi/gui.d

Revision 51, 68.3 kB (checked in by baxissimo, 1 year ago)

Various small fixes and improvements.
event.d: added is_left_press et al properties (I found myselft getting confused as to whether 'left_down' mean it was a left down event or just that left was generally down. Maybe that should become left_is_down...

gui.d: made mixins refer to fully qualified names. Fixes problems with mixing-in in a different context.

theme stuff: Fixed dxut to not crash when presented with an unknown subclass.

Line 
1 //---------------------------------------------------------------------
2 /**
3    Luigi is an OpenGL based GUI library.
4   
5    The goal is to provide a simple but flexible way to add a simple
6    GUI to an OpenGL program.
7
8    Luigi supports themes and input adaptors.  Themes give you a way
9    to customize the look of GUI.  Input adaptors do the work of
10    taking events from the OS or from a windowing toolkit like GLD, and
11    turning them into something Luigi can use.
12 */ 
13 //---------------------------------------------------------------------
14 /*
15   luigi/gui.d -- main import file for 'luigi' user interface library.
16   version 0.5, December 3, 2006
17
18   Copyright (C) 2006 William V. Baxter III
19
20   This software is provided 'as-is', without any express or implied
21   warranty.  In no event will the authors be held liable for any
22   damages arising from the use of this software.
23
24   Permission is granted to anyone to use this software for any
25   purpose, including commercial applications, and to alter it and
26   redistribute it freely, subject to the following restrictions:
27
28   1. The origin of this software must not be misrepresented; you must
29      not claim that you wrote the original software. If you use this
30      software in a product, an acknowledgment in the product
31      documentation would be appreciated but is not required.
32
33   2. Altered source versions must be plainly marked as such, and must
34      not be misrepresented as being the original software.
35   3. This notice may not be removed or altered from any source distribution.
36
37   William Baxter wbaxter@gmail.com
38 */
39 //----------------------------------------------------------------------------
40 /*
41   WARNING: Because of how overloading works in D (at least as of DMD 0.176),
42            *******************************************
43            ** ALL SETTERS MUST COME BEFORE GETTERS  **
44            *******************************************
45   I.e.
46
47           int value(int newval)
48
49    should always appear before
50
51           int value()
52
53    Otherwise, signal.connect(value) will silently connect to the
54    getter rather than the setter.
55  */
56 module luigi.gui;
57
58 import luigi.opengl;
59 import luigi.base;
60 import luigi.event;
61 import luigi.font;
62 import luigi.theme;
63 static import luigi.themes.std;
64 import luigi.arranger;
65 import luigi.adapter;
66 //import sslot.signal;
67 import luigi.signalobj;
68
69 //import adapter = luigi.adapter.glfw;
70
71 import drawsys = luigi.gldraw;
72
73 import math = std.math;
74 import string = std.string;
75 import std.ctype : isprint;
76 import std.uni : isUniAlpha;
77 static import std.path;
78 static import std.file;
79
80 // Just for debug?
81 import std.stdio : writefln;
82
83
84 /**
85  * The singleton master object. 
86  * Holds GUI globals including the theme, the input adapter, and keeps track
87  * of all the top-level Frames and Overlays.
88  */
89 class Luigi
90 {
91     /// Return the singleton instance
92     static Luigi opCall()
93     {
94         static Luigi instance = null;
95         if (!instance) instance = new Luigi;
96         return instance;
97     }
98
99     /// Return the singleton instance
100     static Luigi inst() {
101         return Luigi();
102     }
103
104     /// Set the theme
105     Theme theme(Theme th) { return m_theme=th; }
106     /// Get the current theme
107     Theme theme() {
108         if (!m_theme) {
109             m_theme = new luigi.themes.std.StdTheme();
110         }
111         return m_theme;
112     }
113
114     /// Add a location where themes should look for resource files
115     /// (e.g. images, textures, etc.  The built-in themes look for some files
116     /// relative to the luigi base directory, so you may need to add it to
117     /// your path.)  The current directory '.' is the only thing on the resource path
118     /// by default.
119     void add_resource_location(char[] path) {
120         char[] workp = path.dup;
121         if (workp[$-1..$] != std.path.sep) { workp ~= std.path.sep; }
122         m_resource_path ~= workp;
123     }
124     /// Add a location where themes should look for resource files to the beginning of
125     /// the resource path.
126     /// (e.g. images, textures, etc.  The built-in themes look for some files
127     /// relative to the luigi base directory, so you may need to add it to
128     /// your path.)  The current directory '.' is the only thing on the resource path
129     /// by default.
130     void prepend_resource_location(char[] path) {
131         char[] workp = path.dup;
132         if (workp[$-1..$] != std.path.sep) { workp ~= std.path.sep; }
133         m_resource_path = workp ~ m_resource_path;
134     }
135     /// Get the current resource path.  This is a list of paths where Luigi
136     /// should look for resource files like images.
137     char[][] resource_path() {
138         return m_resource_path;
139     }
140     /// Finds the specified resource file on the resource path.  This could be
141     /// an image or anything else.  Used to find theme textures internally.
142     /// A list of names passed in will treated as alternative names for the same
143     /// resource file and searched for in the order provided.
144     /// Returns null if the resource could not be located on the resource path.
145     char[] find_resource_location(char[][] name...) {
146         foreach(char[] aname; name) {
147             foreach(char[] p; m_resource_path) {
148                 char[] fullpath = p ~ aname;
149                 if (std.file.exists(fullpath)) {
150                     return fullpath;
151                 }
152             }
153         }
154         return null;
155     }
156
157     /** Add a top level overlay. 
158         This is called automatically by Overlay, so typically there is no
159         reason for users to call it themselves.
160     */
161     void add_overlay(Overlay ov) {
162         m_guis ~= ov;
163
164         if (!m_inputsys) {
165             throw new GUIException("Must set Luigi().adapter before creating Overlays");
166         }
167
168         // add in top-level input hooks
169         with (m_inputsys) {
170             addSysKeyCallback(&ov.on_sys_key);
171             addSysMouseButtonCallback(&ov.on_sys_mouse_button) ;
172             addMouseMoveCallback(&ov.on_sys_mouse_move);
173             addMouseWheelCallback(&ov.on_sys_mouse_wheel);
174             addWindowSizeCallback(&ov.on_sys_window_size);
175             addWindowCloseCallback(&ov.on_sys_window_close);
176         }
177     }
178
179     /** Returns the size of the specified window */
180     Size get_window_size(WindowHandle win) {
181         return m_inputsys.get_window_size(win);
182     }
183
184     /** Set the current input adapter */
185     void adapter(InputAdapter inputsys) {
186         m_inputsys = inputsys;
187     }
188     /** Get the current input adapter */
189     InputAdapter adapter() {
190         return m_inputsys;
191     }
192
193
194
195 private:
196
197     this() {
198         add_resource_location(".");
199     }
200
201     ~this() {
202     }
203
204     Overlay[] m_guis;
205     InputAdapter m_inputsys;
206     Theme m_theme;
207     char[][] m_resource_path;
208 }
209
210
211 /// Add a user-level input callback functions and delegates
212 void add_key_callback(KeyEventFn cb)                  {Luigi().adapter.addKeyCallback(cb);}
213 void add_key_callback(KeyEventDg cb)                  {Luigi().adapter.addKeyCallback(cb);}
214 void add_mouse_button_callback(MouseButtonEventFn cb) {Luigi().adapter.addMouseButtonCallback(cb); }
215 void add_mouse_button_callback(MouseButtonEventDg cb) {Luigi().adapter.addMouseButtonCallback(cb);}
216 void add_mouse_move_callback(MouseMoveEventFn cb)     {Luigi().adapter.addMouseMoveCallback(cb);}
217 void add_mouse_move_callback(MouseMoveEventDg cb)     {Luigi().adapter.addMouseMoveCallback(cb);}
218 void add_mouse_wheel_callback(MouseWheelEventFn cb)   {Luigi().adapter.addMouseWheelCallback(cb);}
219 void add_mouse_wheel_callback(MouseWheelEventDg cb)   {Luigi().adapter.addMouseWheelCallback(cb);}
220 void add_window_size_callback(WindowSizeEventFn cb)   {Luigi().adapter.addWindowSizeCallback(cb);}
221 void add_window_size_callback(WindowSizeEventDg cb)   {Luigi().adapter.addWindowSizeCallback(cb);}
222 void add_window_close_callback(WindowCloseEventFn cb) {Luigi().adapter.addWindowCloseCallback(cb);}
223 void add_window_close_callback(WindowCloseEventDg cb) {Luigi().adapter.addWindowCloseCallback(cb);}
224 /// Add a system-level input callback function or delegate -- Use with caution!
225 void add_sys_key_callback(KeyEventFn cb)                  {Luigi().adapter.addSysKeyCallback(cb);}
226 void add_sys_key_callback(KeyEventDg cb)                  {Luigi().adapter.addSysKeyCallback(cb);}
227 void add_sys_mouse_button_callback(MouseButtonEventFn cb) {Luigi().adapter.addSysMouseButtonCallback(cb); }
228 void add_sys_mouse_button_callback(MouseButtonEventDg cb) {Luigi().adapter.addSysMouseButtonCallback(cb); }
229
230
231 /// Remove installed callbacks
232 void remove_key_callback(KeyEventFn cb)                  {Luigi().adapter.removeKeyCallback(cb);}
233 void remove_key_callback(KeyEventDg cb)                  {Luigi().adapter.removeKeyCallback(cb);}
234 void remove_mouse_button_callback(MouseButtonEventFn cb) {Luigi().adapter.removeMouseButtonCallback(cb); }
235 void remove_mouse_button_callback(MouseButtonEventDg cb) {Luigi().adapter.removeMouseButtonCallback(cb); }
236 void remove_mouse_move_callback(MouseMoveEventFn cb)     {Luigi().adapter.removeMouseMoveCallback(cb);}
237 void remove_mouse_move_callback(MouseMoveEventDg cb)     {Luigi().adapter.removeMouseMoveCallback(cb);}
238 void remove_mouse_wheel_callback(MouseWheelEventFn cb)   {Luigi().adapter.removeMouseWheelCallback(cb);}
239 void remove_mouse_wheel_callback(MouseWheelEventDg cb)   {Luigi().adapter.removeMouseWheelCallback(cb);}
240 void remove_window_size_callback(WindowSizeEventFn cb)   {Luigi().adapter.removeWindowSizeCallback(cb);}
241 void remove_window_size_callback(WindowSizeEventDg cb)   {Luigi().adapter.removeWindowSizeCallback(cb);}
242 void remove_window_close_callback(WindowCloseEventFn cb) {Luigi().adapter.removeWindowCloseCallback(cb);}
243 void remove_window_close_callback(WindowCloseEventDg cb) {Luigi().adapter.removeWindowCloseCallback(cb);}
244 void remove_sys_key_callback(KeyEventFn cb)                  {Luigi().adapter.removeSysKeyCallback(cb);}
245 void remove_sys_key_callback(KeyEventDg cb)                  {Luigi().adapter.removeSysKeyCallback(cb);}
246 void remove_sys_mouse_button_callback(MouseButtonEventFn cb) {Luigi().adapter.removeSysMouseButtonCallback(cb); }
247 void remove_sys_mouse_button_callback(MouseButtonEventDg cb) {Luigi().adapter.removeSysMouseButtonCallback(cb); }
248
249
250
251
252 /**
253  * Represents a real OS window used exclusively by Luigi.
254  * If the GUI is drawn on top of the app's GL window, use Overlay instead.
255  */
256 class Frame
257 {
258     // maybe later -- GLFW doesn't have a way to make more than one windowframe.
259     // SDL doesn't either.
260     // And maybe this should just be Window, because e.g. GLUT has sub-windows that can be
261     // treated basically as top level windows.
262
263     private:
264     WindowHandle m_winhandle;
265 }
266
267 template WidgetMixin()
268 {
269     alias typeof(this) WidgetType;
270     static assert(is(WidgetType:luigi.gui.Widget),
271                   "WidgetMixin should be derived from Widget");
272        
273     /**
274     This function is a convenient way to add a newly constructed widget
275     to an arranger while passing along extra necessary arguments.
276
277     It is always possible to add the widget directy to the parent's arranger using
278     the arranger.add(), but this way lets you construct the widget, add it to the
279     arranger with arguments, and assign it to a variable of the derived type all
280     in one line.
281
282     For example:
283       Button b = arranger_add(new Button(parent, "Name"), Arranger.Left, Arranger.Top);
284     
285     Returns: A pointer to 'this', with the proper derived type.
286     */
287     WidgetType arranged_(VArg...)(VArg varg)
288     {
289         //static assert(is(T:Widget), "arranger_add requires a Widget as argument 0");
290         if (parent && parent.arranger && !parent.arranger.auto_add) {
291             parent.arranger.add(this, varg);
292         }
293         return this;
294     }
295 }
296
297 template PanelMixin()
298 {
299     alias typeof(this) PanelType;
300     static assert(is(PanelType:luigi.gui.Panel),
301                   "PanelMixin should be derived from Panel");
302        
303     /** Add the widget to this panel.
304      *
305      * This method returns the widget passed in with its full derived type,
306      * so add_widget can be used like:
307      * ---------
308      *   auto b = border_panel.add_widget(new Button("Clicky"));
309      * ---------
310      * and the resulting b will have type Button.
311      *
312      * See_Also: add_arranged, Panel.add, Arranger.add
313      */
314     W add_widget(W)(W widget)
315     {
316         static assert(is(W:luigi.gui.Widget), "add_widget requires a Widget as argument");
317         add(widget);
318         return widget;
319     }
320
321     /** Add the widget to this panel and also to the panel's arranger
322      *  with arguments.
323      *
324      * This method returns the widget passed in with its full derived type,
325      * so add_arranged can be used like:
326      * ---------
327      *   auto b = border_panel.add_arranged(new Button("Clicky"), Region.East);
328      * ---------
329      * and the resulting b will have type Button.
330      *
331      * See_Also: add_widget, Panel.add, Arranger.add
332      */
333     W add_arranged(W, Varg...)(W widget, Varg args)
334     {
335         static assert(is(W:luigi.gui.Widget),
336                       "add_arranged requires a Widget as argument");
337         add(widget);
338         assert(arranger, "add_arranged called with no arranger set");
339         if (arranger && !arranger.auto_add) arranger.add(widget, args);
340         return widget;
341     }
342 }
343
344 /**
345  * The base class for any GUI entity that occupies space on the screen.
346  */
347 class Widget : Arrangeable
348 {
349     mixin WidgetMixin;
350
351     // Some handy aliases everyone should have
352     // This makes it so you can refer to these inside a Widget subclass
353     // without worry
354     alias luigi.base.Size Size;
355     alias luigi.base.Rect Rect;
356     alias luigi.base.Point Point;
357
358
359     this() {}
360    
361     void add(Widget child) {
362         throw new GUIException("Attempt to add child to a Widget"); 
363     }
364    
365     void draw() {
366         // Default is to let the theme draw it.
367         if (shown)
368             Luigi().theme.draw(this);
369     }
370
371     //------------------------------------------------------------------------
372     // Default implementation of Arrangeable interface
373     override Size minimum_size(Size bounds) {
374         return Luigi().theme.minimum_size(this, bounds);
375     }
376     override Size preferred_size(Size bounds) {
377         return Luigi().theme.preferred_size(this, bounds);
378     }
379     override void set_rect(Rect s) { m_rect = s; }
380     override void set_position(Point p) { m_rect.x = p.x; m_rect.y = p.y; }
381     override void set_size(Size sz) { m_rect.width = sz.width; m_rect.height = sz.height; }
382     override void arrange() { /* nothing to do */ }
383
384
385     /** Return the arranger used to arrange this widget's chilrend */
386     Arranger arranger() { return m_arranger; }
387
388     /** Set the arranger used to arrange this widget's chilren */
389     Arranger arranger(Arranger a)
390     {
391         if (a==m_arranger) return m_arranger;
392         m_arranger = a;
393         m_arranger.set_rect(m_rect);
394         return m_arranger;
395     }
396
397     /** Return the arranger used to arrange this widget */
398     Arranger arranged_by() {
399         if (parent) return parent.arranger;
400         return null;
401     }
402
403     /** Return the items rectangle. 
404         The rectangle coordinates are relative to the upper left corner of this
405         widget's parent.
406     */
407     Rect rect() { return m_rect; }
408     // Rect rect(Rect r) { return m_rect; }  // use set rect!
409
410
411     void enable(bool isEnabled=true) { return m_enabled = isEnabled; }
412     void disable() { m_enabled = false; }
413     bool enabled() { return m_enabled; }
414     bool disabled() { return !m_enabled; }
415    
416     void show() { m_shown = true; }
417     void hide() { m_shown = false; }
418     bool shown(bool show_) { return m_shown = show_; }
419     bool shown() { return m_shown; }
420     void toggle_shown() { m_shown = !m_shown; }
421
422     /** Return the parent of this item or null if it has no parent.
423     */
424     Widget parent() { return m_parent; }
425
426     /** Return the list of items that are parented to this one.
427         Panels are the base type for all items with children.
428     */
429     Widget[] children() { return null; }
430
431     /** Transforms the given point from window coordinates into
432         the widget's coordinates.  In widget coordinates,
433         (rect.x,rect.y) is the upper left corner of the widget.
434     */
435     void transform_window_to_widget(inout Point winp)
436     {
437         Widget w = this.parent;
438         while( w )
439         {
440             winp.x -= w.m_rect.x;
441             winp.y -= w.m_rect.y;
442             w = w.parent;
443         }
444     }
445
446     /** Return the item after this one in the tab traversal order */
447     Widget next_item() {
448         Panel p = cast(Panel)parent;
449         if (p) { return p.sibling_after(this); }
450         return null;
451     }
452     /** Return the item before this one in the tab traversal order */
453     Widget prev_item() {
454         Panel p = cast(Panel)parent;
455         if (p) { return p.sibling_before(this); }
456         return null;
457     }
458
459     /** Find the widget at the root of this item's hierarchy */
460     Widget get_root() {
461         Widget r = this;
462         while ( r.parent ) r = r.parent;
463         return r;
464     }
465
466     bool is_grabbing_mouse() {
467         Widget R = get_root();
468         return R._get_mouse_grabber() is this;
469     }
470     bool grab_mouse() {
471         Widget R = get_root();
472         return R._set_mouse_grabber(this);
473     }
474     bool release_mouse() {
475         Widget R = get_root();
476         return R._release_mouse_grabber(this);
477     }
478        
479
480     protected bool _set_mouse_grabber(Widget w)
481     {
482         assert(0, "Widgets don't grab the mouse");
483         return false;
484     }
485     protected bool _release_mouse_grabber(Widget w)
486     {
487         assert(0, "Widgets don't grab the mouse");
488         return false;
489     }
490     protected Widget _get_mouse_grabber()
491     {
492         assert(0, "Widgets don't grab the mouse");
493         return null;
494     }
495
496     //----------------- FOCUS METHODS --------------------------
497
498     /** Returns whether this item has the keyboard focus.
499     */
500     bool focused()
501     in {  get_root()._focus_sanity_check(); } body
502     {
503         return m_focused;
504     }
505
506     bool focusable(bool onOff)
507     in {  get_root()._focus_sanity_check(); } body
508     {
509         if (m_focusable == onOff)
510             // no change
511             return m_focusable;
512
513         if (!onOff && m_focused) {
514             // we were focused but we're being made non-focusable
515             // Try to focus the next guy before turning our focus off.
516             focus_next();
517             if (m_focused) // it didn't work! maybe we're the only item!
518                 _unfocus_rup();
519         }
520         m_focusable = onOff;
521         return m_focusable;
522     }
523
524     bool focusable()
525     in {  get_root()._focus_sanity_check(); } body
526     {
527         return m_focusable && m_enabled;
528     }
529
530     /** Finds the keyboard focus of this widget's hierarchy. */
531     Widget get_focus()
532     in {  get_root()._focus_sanity_check(); } body
533     {
534         Widget R = get_root();
535         Widget w = R._find_focus_rdown();
536         return w;
537     }
538
539     protected Widget _find_focus_rdown()
540     {
541         writefln("Focus rdown widget");
542         // Shouldn't usually get here, unless we have no parent
543         assert(parent==null, "I shouldn't even *be* here today.");
544         return this;
545     }
546
547     /** Sets the keyboard focus of this widget's hierarchy to newfocus.
548      *  If newfocus is null any current focus item in this widget tree
549      *  will be unfocused.
550      */
551     bool set_focus(Widget newfocus)
552     in {  get_root()._focus_sanity_check(); } body
553     {
554         if (newfocus) {
555             if (!newfocus.focusable) return false;
556             if (newfocus.m_focused) return true;
557         }
558
559         // unfocus the current guy first
560         Widget oldFocus = get_focus();
561         if (oldFocus && !oldFocus._can_lose_focus()) {
562             return false;
563         }
564         else if (oldFocus) {
565             oldFocus._unfocus_rup();
566         }
567        
568         // Stop here if there's no new focus item
569         if (!newfocus) return true;
570
571         // now give focus to the new guy.
572         // Set his m_focused flag and percolate m_childFocused flags up the chain.
573         _FocusEvent ev;
574         ev.previous = oldFocus;
575         newfocus.on_focus(&ev);
576         if (!ev.alive) {
577             newfocus.m_focused = true;
578             if (newfocus.parent) newfocus.parent._set_child_focused_rup();
579         }
580         return newfocus.m_focused;
581     }
582
583
584     /** Set the keyboard focus to this item.
585         Returns true if successful, false otherwise.
586     */
587     bool focus()
588     in {  get_root()._focus_sanity_check(); } body
589     {
590         return set_focus(this);
591     }
592
593     protected void _set_child_focused_rup() {
594         // recursively set child focus on all parents
595         assert(false, "recursive focus only for Panel-derived classes.");
596     }
597
598     protected void _unfocus_rup() {
599         // recursively unfocus this item and all parents
600         m_focused = false;
601         if (parent) parent._unfocus_rup();
602     }
603
604     protected Widget _find_first_focusable_rdown() {
605         if (focusable) {
606             return this;
607         }
608         return null;
609     }
610     protected Widget _find_last_focusable_rdown() {
611         if (focusable) {
612             return this;
613         }
614         return null;
615     }
616     protected Widget _find_next_focusable_rdown(inout bool found_focus)
617     {
618         if (m_focused) { found_focus = true; }
619         return null;
620     }
621     protected Widget _find_prev_focusable_rdown(inout bool found_focus)
622     {
623         if (m_focused) { found_focus = true; }
624         return null;
625     }
626
627     protected int _focus_sanity_check(int c=0) {
628         return c + (m_focused?1:0);
629     }
630
631
632     /** Moves focus to the next item in this widget's hierarchy, or
633         to the first item if nothing is currently focused.
634         Return the newly focused item.
635     */
636     Widget focus_next()
637     {
638         Widget w = get_focus();
639         Widget R = get_root();
640         if (!w) {
641             w = R._find_first_focusable_rdown();
642         }
643         else {
644             bool code = false;
645             w = R._find_next_focusable_rdown(code);
646             if (!w) {
647                 w = R._find_first_focusable_rdown();
648             }
649         }
650<