1 // Written in the D programming language.
2 
3 /**
4 This module contains common Plaform definitions.
5 
6 Platform is abstraction layer for application.
7 
8 
9 Synopsis:
10 
11 ----
12 import dlangui.platforms.common.platform;
13 
14 ----
15 
16 Copyright: Vadim Lopatin, 2014
17 License:   Boost License 1.0
18 Authors:   Vadim Lopatin, coolreader.org@gmail.com
19 */
20 module dlangui.platforms.common.platform;
21 
22 public import dlangui.core.config;
23 public import dlangui.core.events;
24 import dlangui.core.collections;
25 import dlangui.widgets.widget;
26 import dlangui.widgets.popup;
27 import dlangui.graphics.drawbuf;
28 import dlangui.core.stdaction;
29 import dlangui.core.asyncsocket;
30 
31 static if (ENABLE_OPENGL) {
32     private import dlangui.graphics.gldrawbuf;
33 }
34 private import std.algorithm;
35 private import core.sync.mutex;
36 private import std.string;
37 
38 /// entry point - declare such function to use as main for dlangui app
39 extern(C) int UIAppMain(string[] args);
40 
41 
42 // specify debug=DebugMouseEvents for logging mouse handling
43 // specify debug=DebugRedraw for logging drawing and layouts handling
44 // specify debug=DebugKeys for logging of key events
45 
46 /// window creation flags
47 enum WindowFlag : uint {
48     /// window can be resized
49     Resizable = 1,
50     /// window should be shown in fullscreen mode
51     Fullscreen = 2,
52     /// modal window - grabs input focus
53     Modal = 4,
54 }
55 
56 /// Window states
57 enum WindowState : int {
58     /// state is unknown (not supported by platform?), as well for using in setWindowState when only want to activate window or change its size/position
59     unspecified,
60     /// normal state
61     normal,
62     /// window is maximized
63     maximized,
64     /// window is maximized
65     minimized,
66     /// fullscreen mode (supported not on all platforms)
67     fullscreen,
68     /// application is paused (e.g. on Android)
69     paused,
70     /// window is hidden
71     hidden,
72     /// closed
73     closed,
74 }
75 
76 /// Dialog display modes - used to configure dialogs should be showed as a popup or window
77 enum DialogDisplayMode : ulong {
78     /// show all types of dialogs in windows
79     allTypesOfDialogsInWindow = 0,
80     /// show file dialogs in popups
81     fileDialogInPopup = 1,
82     /// show message boxes in popups
83     messageBoxInPopup = 2,
84     /// show input boxes in popups
85     inputBoxInPopup = 4,
86     /// show settings dialogs in popups
87     settingsDialogInPopup = 8,
88     /// show user dialogs in popups - flag for user dialogs
89     userDialogInPopup = 16,
90     /// show all types of dialogs in popups
91     allTypesOfDialogsInPopup = fileDialogInPopup | messageBoxInPopup | inputBoxInPopup | settingsDialogInPopup | userDialogInPopup
92 }
93 
94 /// Window state signal listener
95 interface OnWindowStateHandler {
96     /// signal listener - called when state of window is changed
97     bool onWindowStateChange(Window window, WindowState winState, Rect rect);
98 }
99 
100 /// protected event list
101 /// references to posted messages can be stored here at least to keep live reference and avoid GC
102 /// as well, on some platforms it's easy to send id to message queue, but not pointer
103 class EventList {
104     protected Mutex _mutex;
105     protected Collection!CustomEvent _events;
106     this() {
107         _mutex = new Mutex();
108     }
109     ~this() {
110         destroy(_mutex);
111         _mutex = null;
112     }
113     /// puts event into queue, returns event's unique id
114     long put(CustomEvent event) {
115         _mutex.lock();
116         scope(exit) _mutex.unlock();
117         _events.pushBack(event);
118         return event.uniqueId;
119     }
120     /// return next event
121     CustomEvent get() {
122         _mutex.lock();
123         scope(exit) _mutex.unlock();
124         return _events.popFront();
125     }
126     /// return event by unique id
127     CustomEvent get(uint uniqueId) {
128         _mutex.lock();
129         scope(exit) _mutex.unlock();
130         for (int i = 0; i < _events.length; i++) {
131             if (_events[i].uniqueId == uniqueId) {
132                 return _events.remove(i);
133             }
134         }
135         // not found
136         return null;
137     }
138 }
139 
140 class TimerInfo {
141     static __gshared ulong nextId;
142 
143     this(Widget targetWidget, long intervalMillis) {
144         _id = ++nextId;
145         assert(intervalMillis >= 0 && intervalMillis < 7*24*60*60*1000L);
146         _targetWidget = targetWidget;
147         _interval = intervalMillis;
148         _nextTimestamp = currentTimeMillis + _interval;
149     }
150     /// cancel timer
151     void cancel() {
152         _targetWidget = null;
153     }
154     /// cancel timer
155     void notify() {
156         if (_targetWidget) {
157             _nextTimestamp = currentTimeMillis + _interval;
158             if (!_targetWidget.onTimer(_id)) {
159                 _targetWidget = null;
160             }
161         }
162     }
163     /// unique Id of timer
164     @property ulong id() { return _id; }
165     /// timer interval, milliseconds
166     @property long interval() { return _interval; }
167     /// next timestamp to invoke timer at, as per currentTimeMillis()
168     @property long nextTimestamp() { return _nextTimestamp; }
169     /// widget to route timer event to
170     @property Widget targetWidget() { return _targetWidget; }
171     /// return true if timer is not yet cancelled
172     @property bool valid() { return _targetWidget !is null; }
173 
174     protected ulong _id;
175     protected long _interval;
176     protected long _nextTimestamp;
177     protected Widget _targetWidget;
178 
179     override bool opEquals(Object obj) const {
180         TimerInfo b = cast(TimerInfo)obj;
181         if (!b)
182             return false;
183         return b._nextTimestamp == _nextTimestamp;
184     }
185     override int opCmp(Object obj) {
186         TimerInfo b = cast(TimerInfo)obj;
187         if (!b)
188             return false;
189         if (valid && !b.valid)
190             return -1;
191         if (!valid && b.valid)
192             return 1;
193         if (!valid && !b.valid)
194             return 0;
195         if (_nextTimestamp < b._nextTimestamp)
196             return -1;
197         if (_nextTimestamp > b._nextTimestamp)
198             return 1;
199         return 0;
200     }
201 }
202 
203 
204 /**
205  * Window abstraction layer. Widgets can be shown only inside window.
206  * 
207  */
208 class Window : CustomEventTarget {
209     protected int _dx;
210     protected int _dy;
211     protected uint _keyboardModifiers;
212     protected uint _backgroundColor;
213     protected Widget _mainWidget;
214     protected EventList _eventList;
215     protected uint _flags;
216 
217     @property uint flags() { return _flags; }
218     @property uint backgroundColor() const { return _backgroundColor; }
219     @property void backgroundColor(uint color) { _backgroundColor = color; }
220     @property int width() const { return _dx; }
221     @property int height() const { return _dy; }
222     @property uint keyboardModifiers() const { return _keyboardModifiers; }
223     @property Widget mainWidget() { return _mainWidget; }
224     @property void mainWidget(Widget widget) { 
225         if (_mainWidget !is null) {
226             _mainWidget.window = null;
227             destroy(_mainWidget);
228         }
229         _mainWidget = widget;
230         if (_mainWidget !is null)
231             _mainWidget.window = this;
232     }
233 
234     protected Rect _caretRect;
235     /// blinking caret position (empty rect if no blinking caret)
236     @property void caretRect(Rect rc) { _caretRect = rc; }
237     @property Rect caretRect() { return _caretRect; }
238 
239     protected bool _caretReplace;
240     /// blinking caret is in Replace mode if true, insert mode if false
241     @property void caretReplace(bool flg) { _caretReplace = flg; }
242     @property bool caretReplace() { return _caretReplace; }
243 
244     // Abstract methods : override in platform implementatino
245 
246     /// show window
247     abstract void show();
248     /// returns window caption
249     abstract @property dstring windowCaption();
250     /// sets window caption
251     abstract @property void windowCaption(dstring caption);
252     /// sets window icon
253     abstract @property void windowIcon(DrawBufRef icon);
254     /// request window redraw
255     abstract void invalidate();
256     /// close window
257     abstract void close();
258 
259     protected WindowState _windowState = WindowState.normal;
260     /// returns current window state
261     @property WindowState windowState() {
262         return _windowState;
263     }
264 
265     protected Rect _windowRect = RECT_VALUE_IS_NOT_SET;
266     /// returns window rectangle on screen (includes window frame and title)
267     @property Rect windowRect() {
268         if (_windowRect != RECT_VALUE_IS_NOT_SET)
269             return _windowRect;
270         // fake window rectangle -- at position 0,0 and 
271         return Rect(0, 0, _dx, _dy);
272     }
273     /// window state change signal
274     Signal!OnWindowStateHandler windowStateChanged;
275     /// update and signal window state and/or size/positon changes - for using in platform inplementations
276     protected void handleWindowStateChange(WindowState newState, Rect newWindowRect = RECT_VALUE_IS_NOT_SET) {
277         if (newState != WindowState.unspecified)
278             _windowState = newState;
279         if (newWindowRect != RECT_VALUE_IS_NOT_SET)
280             _windowRect = newWindowRect;
281         if (windowStateChanged.assigned)
282             windowStateChanged(this, newState, newWindowRect);
283     }
284 
285     /// change window state, position, or size; returns true if successful, false if not supported by platform
286     bool setWindowState(WindowState newState, bool activate = false, Rect newWindowRect = RECT_VALUE_IS_NOT_SET) {
287         // override for particular platforms
288         return false;
289     }
290     /// maximize window
291     bool maximizeWindow(bool activate = false) { return setWindowState(WindowState.maximized, activate); }
292     /// minimize window
293     bool minimizeWindow() { return setWindowState(WindowState.minimized); }
294     /// restore window if maximized/minimized/hidden
295     bool restoreWindow(bool activate = false) { return setWindowState(WindowState.normal, activate); }
296     /// restore window if maximized/minimized/hidden
297     bool hideWindow() { return setWindowState(WindowState.hidden); }
298     /// just activate window
299     bool activateWindow() { return setWindowState(WindowState.unspecified, true); }
300     /// change window position only
301     bool moveWindow(Point topLeft, bool activate = false) { return setWindowState(WindowState.unspecified, activate, Rect(topLeft.x, topLeft.y, int.min, int.min)); }
302     /// change window size only
303     bool resizeWindow(Point sz, bool activate = false) { return setWindowState(WindowState.unspecified, activate, Rect(int.min, int.min, sz.x, sz.y)); }
304     /// set window rectangle
305     bool moveAndResizeWindow(Rect rc, bool activate = false) { return setWindowState(WindowState.unspecified, activate, rc); }
306 
307     /// requests layout for main widget and popups
308     void requestLayout() {
309         if (_mainWidget)
310             _mainWidget.requestLayout();
311         foreach(p; _popups)
312             p.requestLayout();
313         if (_tooltip.popup)
314             _tooltip.popup.requestLayout();
315     }
316     void measure() {
317         if (_mainWidget !is null) {
318             _mainWidget.measure(_dx, _dy);
319         }
320         foreach(p; _popups)
321             p.measure(_dx, _dy);
322         if (_tooltip.popup)
323             _tooltip.popup.measure(_dx, _dy);
324     }
325     void layout() {
326         Rect rc = Rect(0, 0, _dx, _dy);
327         if (_mainWidget !is null) {
328             _mainWidget.layout(rc);
329         }
330         foreach(p; _popups)
331             p.layout(rc);
332         if (_tooltip.popup)
333             _tooltip.popup.layout(rc);
334     }
335     void onResize(int width, int height) {
336         if (_dx == width && _dy == height)
337             return;
338         _dx = width;
339         _dy = height;
340         if (_mainWidget !is null) {
341             Log.d("onResize ", _dx, "x", _dy);
342             long measureStart = currentTimeMillis;
343             measure();
344             //Log.d("measured size: ", _mainWidget.measuredWidth, "x", _mainWidget.measuredHeight);
345             long measureEnd = currentTimeMillis;
346             debug Log.d("resize: measure took ", measureEnd - measureStart, " ms");
347             layout();
348             long layoutEnd = currentTimeMillis;
349             debug Log.d("resize: layout took ", layoutEnd - measureEnd, " ms");
350             //Log.d("layout position: ", _mainWidget.pos);
351         }
352         update(true);
353     }
354 
355     protected PopupWidget[] _popups;
356 
357     protected static struct TooltipInfo {
358         PopupWidget popup;
359         ulong timerId;
360         Widget ownerWidget;
361         uint alignment;
362         int x;
363         int y;
364     }
365 
366     protected TooltipInfo _tooltip;
367 
368     /// schedule tooltip for widget be shown with specified delay
369     void scheduleTooltip(Widget ownerWidget, long delay, uint alignment = PopupAlign.Below, int x = 0, int y = 0) {
370         _tooltip.alignment = alignment;
371         _tooltip.x = x;
372         _tooltip.y = y;
373         _tooltip.ownerWidget = ownerWidget;
374         _tooltip.timerId = setTimer(ownerWidget, delay);
375     }
376 
377     /// call when tooltip timer is expired
378     private bool onTooltipTimer() {
379         _tooltip.timerId = 0;
380         if (isChild(_tooltip.ownerWidget)) {
381             Widget w = _tooltip.ownerWidget.createTooltip(_lastMouseX, _lastMouseY, _tooltip.alignment, _tooltip.x, _tooltip.y);
382             if (w)
383                 showTooltip(w, _tooltip.ownerWidget, _tooltip.alignment, _tooltip.x, _tooltip.y);
384         }
385         return false;
386     }
387 
388     /// called when user dragged file(s) to application window
389     void handleDroppedFiles(string[] filenames) {
390         //Log.d("handleDroppedFiles(", filenames, ")");
391         if (_onFilesDropped)
392             _onFilesDropped(filenames);
393     }
394 
395     protected void delegate(string[]) _onFilesDropped;
396     /// get handler for files dropped to app window
397     @property void delegate(string[]) onFilesDropped() { return _onFilesDropped; }
398     /// set handler for files dropped to app window
399     @property Window onFilesDropped(void delegate(string[]) handler) { _onFilesDropped = handler; return this; }
400 
401     protected bool delegate() _onCanClose;
402     /// get handler for closing of app (it must return true to allow immediate close, false to cancel close or close window later)
403     @property bool delegate() onCanClose() { return _onCanClose; }
404     /// set handler for closing of app (it must return true to allow immediate close, false to cancel close or close window later)
405     @property Window onCanClose(bool delegate() handler) { _onCanClose = handler; return this; }
406 
407     protected void delegate() _onClose;
408     /// get handler for closing of window
409     @property void delegate() onClose() { return _onClose; }
410     /// set handler for closing of window
411     @property Window onClose(void delegate() handler) { _onClose = handler; return this; }
412 
413     /// returns true if there is some modal window opened above this window, and this window should not process mouse/key input and should not allow closing
414     bool hasModalWindowsAbove() {
415         return platform.hasModalWindowsAbove(this);
416     }
417 
418     /// calls onCanClose handler if set to check if system may close window
419     bool handleCanClose() {
420         if (hasModalWindowsAbove())
421             return false;
422         if (!_onCanClose)
423             return true;
424         bool res = _onCanClose();
425         if (!res)
426             update(true); // redraw window if it was decided to not close immediately
427         return res;
428     }
429 
430 
431     /// hide tooltip if shown and cancel tooltip timer if set
432     void hideTooltip() {
433         if (_tooltip.popup) {
434             destroy(_tooltip.popup);
435             _tooltip.popup = null;
436             if (_mainWidget)
437                 _mainWidget.invalidate();
438         }
439         if (_tooltip.timerId)
440             cancelTimer(_tooltip.timerId);
441     }
442 
443     /// show tooltip immediately
444     PopupWidget showTooltip(Widget content, Widget anchor = null, uint alignment = PopupAlign.Center, int x = 0, int y = 0) {
445         hideTooltip();
446         if (!content)
447             return null;
448         PopupWidget res = new PopupWidget(content, this);
449         res.anchor.widget = anchor !is null ? anchor : _mainWidget;
450         res.anchor.alignment = alignment;
451         res.anchor.x = x;
452         res.anchor.y = y;
453         _tooltip.popup = res;
454         return res;
455     }
456 
457     /// show new popup
458     PopupWidget showPopup(Widget content, Widget anchor = null, uint alignment = PopupAlign.Center, int x = 0, int y = 0) {
459         PopupWidget res = new PopupWidget(content, this);
460         res.anchor.widget = anchor !is null ? anchor : _mainWidget;
461         res.anchor.alignment = alignment;
462         res.anchor.x = x;
463         res.anchor.y = y;
464         _popups ~= res;
465         if (_mainWidget !is null) {
466             _mainWidget.requestLayout();
467         }
468         return res;
469     }
470     /// remove popup
471     bool removePopup(PopupWidget popup) {
472         if (!popup)
473             return false;
474         for (int i = 0; i < _popups.length; i++) {
475             PopupWidget p = _popups[i];
476             if (p is popup) {
477                 for (int j = i; j < _popups.length - 1; j++)
478                     _popups[j] = _popups[j + 1];
479                 _popups.length--;
480                 p.onClose();
481                 destroy(p);
482                 // force redraw
483                 _mainWidget.invalidate();
484                 return true;
485             }
486         }
487         return false;
488     }
489 
490     /// returns last modal popup widget, or null if no modal popups opened
491     PopupWidget modalPopup() {
492         for (int i = cast(int)_popups.length - 1; i >= 0; i--) {
493             if (_popups[i].flags & PopupFlags.Modal)
494                 return _popups[i];
495         }
496         return null;
497     }
498 
499     /// returns true if widget is child of either main widget or one of popups
500     bool isChild(Widget w) {
501         if (_mainWidget !is null && _mainWidget.isChild(w))
502             return true;
503         foreach(p; _popups)
504             if (p.isChild(w))
505                 return true;
506         if (_tooltip.popup)
507             if (_tooltip.popup.isChild(w))
508                 return true;
509         return false;
510     }
511 
512     private long lastDrawTs;
513 
514     this() {
515         _eventList = new EventList();
516         _timerQueue = new TimerQueue();
517         _backgroundColor = 0xFFFFFF;
518         if (currentTheme)
519             _backgroundColor = currentTheme.customColor(STYLE_COLOR_WINDOW_BACKGROUND);
520     }
521     ~this() {
522         debug Log.d("Destroying window");
523         if (_onClose)
524             _onClose();
525         if (_tooltip.popup) {
526             destroy(_tooltip.popup);
527             _tooltip.popup = null;
528         }
529         foreach(p; _popups)
530             destroy(p);
531         _popups = null;
532         if (_mainWidget !is null) {
533             destroy(_mainWidget);
534             _mainWidget = null;
535         }
536         destroy(_eventList);
537         destroy(_timerQueue);
538         _eventList = null;
539     }
540     
541     /**
542     Allows queue destroy of widget.
543     
544     Sometimes when you have very complicated UI with dynamic create/destroy lists of widgets calling simple destroy() 
545     on widget makes segmentation fault.
546     
547     Usually because you destroy widget that on some stage call another that tries to destroy widget that calls it.
548     When the control flow returns widget not exist and you have seg. fault.
549     
550     This function use internally $(LINK2 $(DDOX_ROOT_DIR)dlangui/core/events/QueueDestroyEvent.html, QueueDestroyEvent).
551     */
552     void queueWidgetDestroy(Widget widgetToDestroy)
553     {
554         QueueDestroyEvent ev = new QueueDestroyEvent(widgetToDestroy);
555         postEvent(ev);
556     }
557     
558     private void animate(Widget root, long interval) {
559         if (root is null)
560             return;
561         if (root.visibility != Visibility.Visible)
562             return;
563         for (int i = 0; i < root.childCount; i++)
564             animate(root.child(i), interval);
565         if (root.animating)
566             root.animate(interval);
567     }
568 
569     private void animate(long interval) {
570         animate(_mainWidget, interval);
571         foreach(p; _popups)
572             p.animate(interval);
573         if (_tooltip.popup)
574             _tooltip.popup.animate(interval);
575     }
576 
577     static immutable int PERFORMANCE_LOGGING_THRESHOLD_MS = 20;
578 
579     /// set when first draw is called: don't handle mouse/key input until draw (layout) is called
580     protected bool _firstDrawCalled = false;
581     void onDraw(DrawBuf buf) {
582         _firstDrawCalled = true;
583         static import std.datetime;
584         try {
585             bool needDraw = false;
586             bool needLayout = false;
587             bool animationActive = false;
588             checkUpdateNeeded(needDraw, needLayout, animationActive);
589             if (needLayout || animationActive)
590                 needDraw = true;
591             long ts = std.datetime.Clock.currStdTime;
592             if (animationActive && lastDrawTs != 0) {
593                 animate(ts - lastDrawTs);
594                 // layout required flag could be changed during animate - check again
595                 checkUpdateNeeded(needDraw, needLayout, animationActive);
596             }
597             lastDrawTs = ts;
598             if (needLayout) {
599                 long measureStart = currentTimeMillis;
600                 measure();
601                 long measureEnd = currentTimeMillis;
602                 if (measureEnd - measureStart > PERFORMANCE_LOGGING_THRESHOLD_MS) {
603                     debug(DebugRedraw) Log.d("measure took ", measureEnd - measureStart, " ms");
604                 }
605                 layout();
606                 long layoutEnd = currentTimeMillis;
607                 if (layoutEnd - measureEnd > PERFORMANCE_LOGGING_THRESHOLD_MS) {
608                     debug(DebugRedraw) Log.d("layout took ", layoutEnd - measureEnd, " ms");
609                 }
610                 //checkUpdateNeeded(needDraw, needLayout, animationActive);
611             }
612             long drawStart = currentTimeMillis;
613             // draw main widget
614             _mainWidget.onDraw(buf);
615 
616             PopupWidget modal = modalPopup();
617 
618             // draw popups
619             foreach(p; _popups) {
620                 if (p is modal) {
621                     // TODO: get shadow color from theme
622                     buf.fillRect(Rect(0, 0, buf.width, buf.height), 0xD0404040);
623                 }
624                 p.onDraw(buf);
625             }
626 
627             if (_tooltip.popup)
628                 _tooltip.popup.onDraw(buf);
629 
630             long drawEnd = currentTimeMillis;
631             debug(DebugRedraw) {
632                 if (drawEnd - drawStart > PERFORMANCE_LOGGING_THRESHOLD_MS)
633                     Log.d("draw took ", drawEnd - drawStart, " ms");
634             }
635             if (animationActive)
636                 scheduleAnimation();
637             _actionsUpdateRequested = false;
638         } catch (Exception e) {
639             Log.e("Exception inside winfow.onDraw: ", e);
640         }
641     }
642 
643     /// after drawing, call to schedule redraw if animation is active
644     void scheduleAnimation() {
645         // override if necessary
646     }
647 
648 
649     protected void setCaptureWidget(Widget w, MouseEvent event) {
650         _mouseCaptureWidget = w;
651         _mouseCaptureButtons = event.flags & (MouseFlag.LButton|MouseFlag.RButton|MouseFlag.MButton);
652     }
653 
654     protected Widget _focusedWidget;
655     /// returns current focused widget
656     @property Widget focusedWidget() { 
657         if (!isChild(_focusedWidget))
658             _focusedWidget = null;
659         return _focusedWidget; 
660     }
661 
662     /// change focus to widget
663     Widget setFocus(Widget newFocus, FocusReason reason = FocusReason.Unspecified) {
664         if (!isChild(_focusedWidget))
665             _focusedWidget = null;
666         Widget oldFocus = _focusedWidget;
667         auto targetState = State.Focused;
668         if(reason == FocusReason.TabFocus)
669             targetState = State.Focused | State.KeyboardFocused;
670         if (oldFocus is newFocus)
671             return oldFocus;
672         if (oldFocus !is null) {
673             oldFocus.resetState(targetState);
674             if (oldFocus)
675                 oldFocus.focusGroupFocused(false);
676         }
677         if (newFocus is null || isChild(newFocus)) {
678             if (newFocus !is null) {
679                 // when calling, setState(focused), window.focusedWidget is still previously focused widget
680                 debug(DebugFocus) Log.d("new focus: ", newFocus.id);
681                 newFocus.setState(targetState);
682             }
683             _focusedWidget = newFocus;
684             if (_focusedWidget)
685                 _focusedWidget.focusGroupFocused(true);
686             // after focus change, ask for actions update automatically
687             //requestActionsUpdate();
688         }
689         return _focusedWidget;
690     }
691 
692     /// dispatch key event to widgets which have wantsKeyTracking == true
693     protected bool dispatchKeyEvent(Widget root, KeyEvent event) {
694         if (root.visibility != Visibility.Visible)
695             return false;
696         if (root.wantsKeyTracking) {
697             if (root.onKeyEvent(event))
698                 return true;
699         }
700         for (int i = 0; i < root.childCount; i++) {
701             Widget w = root.child(i);
702             if (dispatchKeyEvent(w, event))
703                 return true;
704         }
705         return false;
706     }
707 
708     /// dispatch keyboard event
709     bool dispatchKeyEvent(KeyEvent event) {
710         if (hasModalWindowsAbove() || !_firstDrawCalled)
711             return false;
712         bool res = false;
713         hideTooltip();
714         PopupWidget modal = modalPopup();
715         if (event.action == KeyAction.KeyDown || event.action == KeyAction.KeyUp) {
716             _keyboardModifiers = event.flags;
717             if (event.keyCode == KeyCode.ALT || event.keyCode == KeyCode.LALT || event.keyCode == KeyCode.RALT) {
718                 debug(DebugKeys) Log.d("ALT key: keyboardModifiers = ", _keyboardModifiers);
719                 if (_mainWidget) {
720                     _mainWidget.invalidate();
721                     res = true;
722                 }
723             }
724         }
725         if (event.action == KeyAction.Text) {
726             // filter text
727             if (event.text.length < 1)
728                 return res;
729             dchar ch = event.text[0];
730             if (ch < ' ' || ch == 0x7F) // filter out control symbols
731                 return res;
732         }
733         Widget focus = focusedWidget;
734         if (!modal || modal.isChild(focus)) {
735             while (focus) {
736                 if (focus.onKeyEvent(event))
737                     return true; // processed by focused widget
738                 if (focus.focusGroup)
739                     break;
740                 focus = focus.parent;
741             }
742         }
743         if (modal) {
744             if (dispatchKeyEvent(modal, event))
745                 return res;
746             return modal.onKeyEvent(event) || res;
747         } else if (_mainWidget) {
748             if (dispatchKeyEvent(_mainWidget, event))
749                 return res;
750             return _mainWidget.onKeyEvent(event) || res;
751         }
752         return res;
753     }
754 
755     protected bool dispatchMouseEvent(Widget root, MouseEvent event, ref bool cursorIsSet) {
756         // only route mouse events to visible widgets
757         if (root.visibility != Visibility.Visible)
758             return false;
759         if (!root.isPointInside(event.x, event.y))
760             return false;
761         // offer event to children first
762         for (int i = 0; i < root.childCount; i++) {
763             Widget child = root.child(i);
764             if (dispatchMouseEvent(child, event, cursorIsSet))
765                 return true;
766         }
767         if (event.action == MouseAction.Move && !cursorIsSet) {
768             uint cursorType = root.getCursorType(event.x, event.y);
769             if (cursorType != CursorType.Parent) {
770                 setCursorType(cursorType);
771                 cursorIsSet = true;
772             }
773         }
774         // if not processed by children, offer event to root
775         if (sendAndCheckOverride(root, event)) {
776             debug(DebugMouseEvents) Log.d("MouseEvent is processed");
777             if (event.action == MouseAction.ButtonDown && _mouseCaptureWidget is null && !event.doNotTrackButtonDown) {
778                 debug(DebugMouseEvents) Log.d("Setting active widget");
779                 setCaptureWidget(root, event);
780             } else if (event.action == MouseAction.Move) {
781                 addTracking(root);
782             }
783             return true;
784         }
785         return false;
786     }
787 
788     /// widget which tracks Move events
789     //protected Widget _mouseTrackingWidget;
790     protected Widget[] _mouseTrackingWidgets;
791     private void addTracking(Widget w) {
792         for(int i = 0; i < _mouseTrackingWidgets.length; i++)
793             if (w is _mouseTrackingWidgets[i])
794                 return;
795         //foreach(widget; _mouseTrackingWidgets)
796         //    if (widget is w)
797         //       return;
798         //Log.d("addTracking ", w.id, " items before: ", _mouseTrackingWidgets.length);
799         _mouseTrackingWidgets ~= w;
800         //Log.d("addTracking ", w.id, " items after: ", _mouseTrackingWidgets.length);
801     }
802     private bool checkRemoveTracking(MouseEvent event) {
803         bool res = false;
804         for(int i = cast(int)_mouseTrackingWidgets.length - 1; i >=0; i--) {
805             Widget w = _mouseTrackingWidgets[i];
806             if (!isChild(w)) {
807                 // std.algorithm.remove does not work for me
808                 //_mouseTrackingWidgets.remove(i);
809                 for (int j = i; j < _mouseTrackingWidgets.length - 1; j++)
810                     _mouseTrackingWidgets[j] = _mouseTrackingWidgets[j + 1];
811                 _mouseTrackingWidgets.length--;
812                 continue;
813             }
814             if (event.action == MouseAction.Leave || !w.isPointInside(event.x, event.y)) {
815                 // send Leave message
816                 MouseEvent leaveEvent = new MouseEvent(event);
817                 leaveEvent.changeAction(MouseAction.Leave);
818                 res = w.onMouseEvent(leaveEvent) || res;
819                 // std.algorithm.remove does not work for me
820                 //Log.d("removeTracking ", w.id, " items before: ", _mouseTrackingWidgets.length);
821                 //_mouseTrackingWidgets.remove(i);
822                 //_mouseTrackingWidgets.length--;
823                 for (int j = i; j < _mouseTrackingWidgets.length - 1; j++)
824                     _mouseTrackingWidgets[j] = _mouseTrackingWidgets[j + 1];
825                 _mouseTrackingWidgets.length--;
826                 //Log.d("removeTracking ", w.id, " items after: ", _mouseTrackingWidgets.length);
827             }
828         }
829         return res;
830     }
831 
832     /// widget which tracks all events after processed ButtonDown
833     protected Widget _mouseCaptureWidget;
834     protected ushort _mouseCaptureButtons;
835     protected bool _mouseCaptureFocusedOut;
836     /// does current capture widget want to receive move events even if pointer left it
837     protected bool _mouseCaptureFocusedOutTrackMovements;
838     
839     protected void clearMouseCapture() {
840         _mouseCaptureWidget = null;
841         _mouseCaptureFocusedOut = false;
842         _mouseCaptureFocusedOutTrackMovements = false;
843         _mouseCaptureButtons = 0;
844     }
845 
846     protected bool dispatchCancel(MouseEvent event) {
847         event.changeAction(MouseAction.Cancel);
848         bool res = _mouseCaptureWidget.onMouseEvent(event);
849         clearMouseCapture();
850         return res;
851     }
852     
853     protected bool sendAndCheckOverride(Widget widget, MouseEvent event) {
854         if (!isChild(widget))
855             return false;
856         bool res = widget.onMouseEvent(event);
857         if (event.trackingWidget !is null && _mouseCaptureWidget !is event.trackingWidget) {
858             setCaptureWidget(event.trackingWidget, event);
859         }
860         return res;
861     }
862 
863     /// returns true if mouse is currently captured
864     bool isMouseCaptured() {
865         return (_mouseCaptureWidget !is null && isChild(_mouseCaptureWidget));
866     }
867 
868     /// dispatch action to main widget
869     bool dispatchAction(const Action action, Widget sourceWidget = null) {
870         // try to handle by source widget
871         if(sourceWidget && isChild(sourceWidget)) {
872             if (sourceWidget.handleAction(action))
873                 return true;
874             sourceWidget = sourceWidget.parent;
875         }
876         Widget focus = focusedWidget;
877         // then offer action to focused widget
878         if (focus && isChild(focus)) {
879             if (focus.handleAction(action))
880                 return true;
881             focus = focus.parent;
882         }
883         // then offer to parent chain of source widget
884         while (sourceWidget && isChild(sourceWidget)) {
885             if (sourceWidget.handleAction(action))
886                 return true;
887             sourceWidget = sourceWidget.parent;
888         }
889         // then offer to parent chain of focused widget
890         while (focus && isChild(focus)) {
891             if (focus.handleAction(action))
892                 return true;
893             focus = focus.parent;
894         }
895         if (_mainWidget)
896             return _mainWidget.handleAction(action);
897         return false;
898     }
899 
900     /// dispatch action to main widget
901     bool dispatchActionStateRequest(const Action action, Widget sourceWidget = null) {
902         // try to handle by source widget
903         if(sourceWidget && isChild(sourceWidget)) {
904             if (sourceWidget.handleActionStateRequest(action))
905                 return true;
906             sourceWidget = sourceWidget.parent;
907         }
908         Widget focus = focusedWidget;
909         // then offer action to focused widget
910         if (focus && isChild(focus)) {
911             if (focus.handleActionStateRequest(action))
912                 return true;
913             focus = focus.parent;
914         }
915         // then offer to parent chain of source widget
916         while (sourceWidget && isChild(sourceWidget)) {
917             if (sourceWidget.handleActionStateRequest(action))
918                 return true;
919             sourceWidget = sourceWidget.parent;
920         }
921         // then offer to parent chain of focused widget
922         while (focus && isChild(focus)) {
923             if (focus.handleActionStateRequest(action))
924                 return true;
925             focus = focus.parent;
926         }
927         if (_mainWidget)
928             return _mainWidget.handleActionStateRequest(action);
929         return false;
930     }
931 
932     /// handle theme change: e.g. reload some themed resources
933     void dispatchThemeChanged() {
934         if (_mainWidget)
935             _mainWidget.onThemeChanged();
936             // draw popups
937         foreach(p; _popups) {
938             p.onThemeChanged();
939         }
940         if (_tooltip.popup)
941             _tooltip.popup.onThemeChanged();
942         if (currentTheme) {
943             _backgroundColor = currentTheme.customColor(STYLE_COLOR_WINDOW_BACKGROUND);
944         }
945         invalidate();
946     }
947 
948 
949     /// post event to handle in UI thread (this method can be used from background thread)
950     void postEvent(CustomEvent event) {
951         // override to post event into window message queue
952         _eventList.put(event);
953     }
954 
955     /// post task to execute in UI thread (this method can be used from background thread)
956     void executeInUiThread(void delegate() runnable) {
957         RunnableEvent event = new RunnableEvent(CUSTOM_RUNNABLE, null, runnable);
958         postEvent(event);
959     }
960 
961     /// Creates async socket
962     AsyncSocket createAsyncSocket(AsyncSocketCallback callback) {
963         AsyncClientConnection conn = new AsyncClientConnection(new AsyncSocketCallbackProxy(callback, &executeInUiThread));
964         return conn;
965     }
966 
967     /// remove event from queue by unique id if not yet dispatched (this method can be used from background thread)
968     void cancelEvent(uint uniqueId) {
969         CustomEvent ev = _eventList.get(uniqueId);
970         if (ev) {
971             //destroy(ev);
972         }
973     }
974 
975     /// remove event from queue by unique id if not yet dispatched and dispatch it
976     void handlePostedEvent(uint uniqueId) {
977         CustomEvent ev = _eventList.get(uniqueId);
978         if (ev) {
979             dispatchCustomEvent(ev);
980         }
981     }
982 
983     /// handle all events from queue, if any (call from UI thread only)
984     void handlePostedEvents() {
985         for(;;) {
986             CustomEvent e = _eventList.get();
987             if (!e)
988                 break;
989             dispatchCustomEvent(e);
990         }
991     }
992 
993     /// dispatch custom event
994     bool dispatchCustomEvent(CustomEvent event) {
995         if (event.destinationWidget) {
996             if (!isChild(event.destinationWidget)) {
997                 //Event is sent to widget which does not exist anymore
998                 return false;
999             }
1000             return event.destinationWidget.onEvent(event);
1001         } else {
1002             // no destination widget
1003             RunnableEvent runnable = cast(RunnableEvent)event;
1004             if (runnable) {
1005                 // handle runnable
1006                 runnable.run();
1007                 return true;
1008             }
1009         }
1010         return false;
1011     }
1012 
1013     private int _lastMouseX;
1014     private int _lastMouseY;
1015     /// dispatch mouse event to window content widgets
1016     bool dispatchMouseEvent(MouseEvent event) {
1017         if (hasModalWindowsAbove() || !_firstDrawCalled)
1018             return false;
1019         // ignore events if there is no root
1020         if (_mainWidget is null)
1021             return false;
1022 
1023         bool actualChange = true;
1024         if (event.action == MouseAction.Move) {
1025             actualChange = (_lastMouseX != event.x || _lastMouseY != event.y);
1026             _lastMouseX = event.x;
1027             _lastMouseY = event.y;
1028         }
1029         if (actualChange) hideTooltip();
1030 
1031         PopupWidget modal = modalPopup();
1032 
1033         // check if _mouseCaptureWidget and _mouseTrackingWidget still exist in child of root widget
1034         if (_mouseCaptureWidget !is null && (!isChild(_mouseCaptureWidget) || (modal && !modal.isChild(_mouseCaptureWidget)))) {
1035             clearMouseCapture();
1036         }
1037 
1038         debug(DebugMouseEvents) Log.d("dispatchMouseEvent ", event.action, "  (", event.x, ",", event.y, ")");
1039 
1040         bool res = false;
1041         ushort currentButtons = event.flags & (MouseFlag.LButton|MouseFlag.RButton|MouseFlag.MButton);
1042         if (_mouseCaptureWidget !is null) {
1043             // try to forward message directly to active widget
1044             if (event.action == MouseAction.Move) {
1045                 debug(DebugMouseEvents) Log.d("dispatchMouseEvent: Move; buttons state=", currentButtons);
1046                 if (!_mouseCaptureWidget.isPointInside(event.x, event.y)) {
1047                     if (currentButtons != _mouseCaptureButtons) {
1048                         debug(DebugMouseEvents) Log.d("dispatchMouseEvent: Move; buttons state changed from ", _mouseCaptureButtons, " to ", currentButtons, " - cancelling capture");
1049                         return dispatchCancel(event);
1050                     }
1051                     // point is no more inside of captured widget
1052                     if (!_mouseCaptureFocusedOut) {
1053                         // sending FocusOut message
1054                         event.changeAction(MouseAction.FocusOut);
1055                         _mouseCaptureFocusedOut = true;
1056                         _mouseCaptureButtons = currentButtons;
1057                         _mouseCaptureFocusedOutTrackMovements = sendAndCheckOverride(_mouseCaptureWidget, event);
1058                         return true;
1059                     } else if (_mouseCaptureFocusedOutTrackMovements) {
1060                         // pointer is outside, but we still need to track pointer
1061                         return sendAndCheckOverride(_mouseCaptureWidget, event);
1062                     }
1063                     // don't forward message
1064                     return true;
1065                 } else {
1066                     // point is inside widget
1067                     if (_mouseCaptureFocusedOut) {
1068                         _mouseCaptureFocusedOut = false;
1069                         if (currentButtons != _mouseCaptureButtons)
1070                             return dispatchCancel(event);
1071                            event.changeAction(MouseAction.FocusIn); // back in after focus out
1072                     }
1073                     return sendAndCheckOverride(_mouseCaptureWidget, event);
1074                 }
1075             } else if (event.action == MouseAction.Leave) {
1076                 if (!_mouseCaptureFocusedOut) {
1077                     // sending FocusOut message
1078                     event.changeAction(MouseAction.FocusOut);
1079                     _mouseCaptureFocusedOut = true;
1080                     _mouseCaptureButtons = event.flags & (MouseFlag.LButton|MouseFlag.RButton|MouseFlag.MButton);
1081                     return sendAndCheckOverride(_mouseCaptureWidget, event);
1082                 } else {
1083                     debug(DebugMouseEvents) Log.d("dispatchMouseEvent: mouseCaptureFocusedOut + Leave - cancelling capture");
1084                     return dispatchCancel(event);
1085                 }
1086             } else if (event.action == MouseAction.ButtonDown || event.action == MouseAction.ButtonUp) {
1087                 if (!_mouseCaptureWidget.isPointInside(event.x, event.y)) {
1088                     if (currentButtons != _mouseCaptureButtons) {
1089                         debug(DebugMouseEvents) Log.d("dispatchMouseEvent: ButtonUp/ButtonDown; buttons state changed from ", _mouseCaptureButtons, " to ", currentButtons, " - cancelling capture");
1090                         return dispatchCancel(event);
1091                     }
1092                 }
1093             }
1094             // other messages
1095             res = sendAndCheckOverride(_mouseCaptureWidget, event);
1096             if (!currentButtons) {
1097                 // usable capturing - no more buttons pressed
1098                 debug(DebugMouseEvents) Log.d("unsetting active widget");
1099                 clearMouseCapture();
1100             }
1101             return res;
1102         }
1103         bool processed = false;
1104         if (event.action == MouseAction.Move || event.action == MouseAction.Leave) {
1105             processed = checkRemoveTracking(event);
1106         }
1107         bool cursorIsSet = false;
1108         if (!res) {
1109             bool insideOneOfPopups = false;
1110             for (int i = cast(int)_popups.length - 1; i >= 0; i--) {
1111                 auto p = _popups[i];
1112                 if (p.isPointInside(event.x, event.y)) {
1113                     if (p !is modal)
1114                         insideOneOfPopups = true;
1115                 }
1116                 if (p is modal)
1117                     break;
1118             }
1119             for (int i = cast(int)_popups.length - 1; i >= 0; i--) {
1120                 auto p = _popups[i];
1121                 if (p is modal)
1122                     break;
1123                 if (!insideOneOfPopups) {
1124                     if (p.onMouseEventOutside(event)) // stop loop when true is returned, but allow other main widget to handle event
1125                         break;
1126                 } else {
1127                     if (dispatchMouseEvent(p, event, cursorIsSet))
1128                         return true;
1129                 }
1130             }
1131             if (!modal)
1132                 res = dispatchMouseEvent(_mainWidget, event, cursorIsSet);
1133             else
1134                 res = dispatchMouseEvent(modal, event, cursorIsSet);
1135         }
1136         return res || processed || _mainWidget.needDraw;
1137     }
1138 
1139     /// calls update actions recursively
1140     protected void dispatchWidgetUpdateActionStateRecursive(Widget root) {
1141         if (root is null)
1142             return;
1143         root.updateActionState(true);
1144         for (int i = 0; i < root.childCount; i++)
1145             dispatchWidgetUpdateActionStateRecursive(root.child(i));
1146     }
1147     /// checks content widgets for necessary redraw and/or layout
1148     protected void checkUpdateNeeded(Widget root, ref bool needDraw, ref bool needLayout, ref bool animationActive) {
1149         if (root is null)
1150             return;
1151         if (root.visibility != Visibility.Visible)
1152             return;
1153         needDraw = root.needDraw || needDraw;
1154         if (!needLayout) {
1155             needLayout = root.needLayout || needLayout;
1156             if (needLayout) {
1157                 debug(DebugRedraw) Log.d("need layout: ", root.classinfo.name, " id=", root.id);
1158             }
1159         }
1160         if (root.animating && root.visible)
1161             animationActive = true; // check animation only for visible widgets
1162         for (int i = 0; i < root.childCount; i++)
1163             checkUpdateNeeded(root.child(i), needDraw, needLayout, animationActive);
1164     }
1165     /// sets cursor type for window
1166     protected void setCursorType(uint cursorType) {
1167         // override to support different mouse cursors
1168     }
1169     /// update action states
1170     protected void dispatchWidgetUpdateActionStateRecursive() {
1171         if (_mainWidget !is null)
1172             dispatchWidgetUpdateActionStateRecursive(_mainWidget);
1173         foreach(p; _popups)
1174             dispatchWidgetUpdateActionStateRecursive(p);
1175     }
1176     /// checks content widgets for necessary redraw and/or layout
1177     bool checkUpdateNeeded(ref bool needDraw, ref bool needLayout, ref bool animationActive) {
1178         if (_actionsUpdateRequested) {
1179             // call update action check - as requested
1180             dispatchWidgetUpdateActionStateRecursive();
1181             _actionsUpdateRequested = false;
1182         }
1183         needDraw = needLayout = animationActive = false;
1184         if (_mainWidget is null)
1185             return false;
1186         checkUpdateNeeded(_mainWidget, needDraw, needLayout, animationActive);
1187         foreach(p; _popups)
1188             checkUpdateNeeded(p, needDraw, needLayout, animationActive);
1189         if (_tooltip.popup)
1190             checkUpdateNeeded(_tooltip.popup, needDraw, needLayout, animationActive);
1191         return needDraw || needLayout || animationActive;
1192     }
1193 
1194     protected bool _animationActive;
1195 
1196     @property bool isAnimationActive() {
1197         return _animationActive;
1198     }
1199 
1200     /// requests update for window (unless force is true, update will be performed only if layout, redraw or animation is required).
1201     void update(bool force = false) {
1202         if (_mainWidget is null)
1203             return;
1204         bool needDraw = false;
1205         bool needLayout = false;
1206         _animationActive = false;
1207         if (checkUpdateNeeded(needDraw, needLayout, _animationActive) || force) {
1208             debug(DebugRedraw) Log.d("Requesting update");
1209             invalidate();
1210         }
1211         debug(DebugRedraw) Log.d("checkUpdateNeeded returned needDraw=", needDraw, " needLayout=", needLayout, " animationActive=", _animationActive);
1212     }
1213 
1214     protected bool _actionsUpdateRequested = true;
1215 
1216     /// set action update request flag, will be cleared after redraw
1217     void requestActionsUpdate(bool immediateUpdate = false) {
1218         if (!immediateUpdate)
1219             _actionsUpdateRequested = true;
1220         else
1221             dispatchWidgetUpdateActionStateRecursive();
1222     }
1223 
1224     @property bool actionsUpdateRequested() {
1225         return _actionsUpdateRequested;
1226     }
1227 
1228     /// Show message box with specified title and message (title and message as UIString)
1229     void showMessageBox(UIString title, UIString message, const (Action)[] actions = [ACTION_OK], int defaultActionIndex = 0, bool delegate(const Action result) handler = null) {
1230         import dlangui.dialogs.msgbox;
1231         MessageBox dlg = new MessageBox(title, message, this, actions, defaultActionIndex, handler);
1232         dlg.show();
1233     }
1234 
1235     /// Show message box with specified title and message (title and message as dstring)
1236     void showMessageBox(dstring title, dstring message, const (Action)[] actions = [ACTION_OK], int defaultActionIndex = 0, bool delegate(const Action result) handler = null) {
1237         showMessageBox(UIString.fromRaw(title), UIString.fromRaw(message), actions, defaultActionIndex, handler);
1238     }
1239 
1240     static if (BACKEND_GUI) {
1241         void showInputBox(UIString title, UIString message, dstring initialText, void delegate(dstring result) handler) {
1242             import dlangui.dialogs.inputbox;
1243             InputBox dlg = new InputBox(title, message, this, initialText, handler);
1244             dlg.show();
1245         }
1246     }
1247 
1248     void showInputBox(dstring title, dstring message, dstring initialText, void delegate(dstring result) handler) {
1249         showInputBox(UIString.fromRaw(title), UIString.fromRaw(message), initialText, handler);
1250     }
1251 
1252     protected TimerQueue _timerQueue;
1253 
1254 
1255     /// schedule timer for interval in milliseconds - call window.onTimer when finished
1256     protected void scheduleSystemTimer(long intervalMillis) {
1257         //debug Log.d("override scheduleSystemTimer to support timers");
1258     }
1259 
1260     /// poll expired timers; returns true if update is needed
1261     bool pollTimers() {
1262         bool res = _timerQueue.notify();
1263         if (res)
1264             update(false);
1265         return res;
1266     }
1267 
1268     /// system timer interval expired - notify queue
1269     protected void onTimer() {
1270         //Log.d("window.onTimer");
1271         bool res = _timerQueue.notify();
1272         //Log.d("window.onTimer after notify");
1273         if (res) {
1274             // check if update needed and redraw if so
1275             //Log.d("before update");
1276             update(false);
1277             //Log.d("after update");
1278         }
1279         //Log.d("schedule next timer");
1280         long nextInterval = _timerQueue.nextIntervalMillis();
1281         if (nextInterval > 0) {
1282             scheduleSystemTimer(nextInterval);
1283         }
1284         //Log.d("schedule next timer done");
1285     }
1286 
1287     /// set timer for destination widget - destination.onTimer() will be called after interval expiration; returns timer id
1288     ulong setTimer(Widget destination, long intervalMillis) {
1289         if (!isChild(destination)) {
1290             Log.e("setTimer() is called not for child widget of window");
1291             return 0;
1292         }
1293         ulong res = _timerQueue.add(destination, intervalMillis);
1294         long nextInterval = _timerQueue.nextIntervalMillis();
1295         if (nextInterval > 0) {
1296             scheduleSystemTimer(intervalMillis);
1297         }
1298         return res;
1299     }
1300 
1301     /// cancel previously scheduled widget timer (for timerId pass value returned from setTimer)
1302     void cancelTimer(ulong timerId) {
1303         _timerQueue.cancelTimer(timerId);
1304     }
1305 
1306     /// timers queue
1307     private class TimerQueue {
1308         protected TimerInfo[] _queue;
1309         /// add new timer
1310         ulong add(Widget destination, long intervalMillis) {
1311             TimerInfo item = new TimerInfo(destination, intervalMillis);
1312             _queue ~= item;
1313             sort(_queue);
1314             return item.id;
1315         }
1316         /// cancel timer
1317         void cancelTimer(ulong timerId) {
1318             if (!_queue.length)
1319                 return;
1320             for (int i = cast(int)_queue.length - 1; i >= 0; i--) {
1321                 if (_queue[i].id == timerId) {
1322                     _queue[i].cancel();
1323                     break;
1324                 }
1325             }
1326         }
1327         /// returns interval if millis of next scheduled event or -1 if no events queued
1328         long nextIntervalMillis() {
1329             if (!_queue.length || !_queue[0].valid)
1330                 return -1;
1331             long delta = _queue[0].nextTimestamp - currentTimeMillis;
1332             if (delta < 1)
1333                 delta = 1;
1334             return delta;
1335         }
1336         private void cleanup() {
1337             if (!_queue.length)
1338                 return;
1339             sort(_queue);
1340             size_t newsize = _queue.length;
1341             for (int i = cast(int)_queue.length - 1; i >= 0; i--) {
1342                 if (!_queue[i].valid) {
1343                     newsize = i;
1344                 }
1345             }
1346             if (_queue.length > newsize)
1347                 _queue.length = newsize;
1348         }
1349         private TimerInfo[] expired() {
1350             if (!_queue.length)
1351                 return null;
1352             long ts = currentTimeMillis;
1353             TimerInfo[] res;
1354             for (int i = 0; i < _queue.length; i++) {
1355                 if (_queue[i].nextTimestamp <= ts)
1356                     res ~= _queue[i];
1357             }
1358             return res;
1359         }
1360         /// returns true if at least one widget was notified
1361         bool notify() {
1362             bool res = false;
1363             checkValidWidgets();
1364             TimerInfo[] list = expired();
1365             if (list) {
1366                 for (int i = 0; i < list.length; i++) {
1367                     if (_queue[i].id == _tooltip.timerId) {
1368                         // special case for tooltip timer
1369                         onTooltipTimer();
1370                         _queue[i].cancel();
1371                         res = true;
1372                     } else {
1373                         Widget w = _queue[i].targetWidget;
1374                         if (w && !isChild(w))
1375                             _queue[i].cancel();
1376                         else {
1377                             _queue[i].notify();
1378                             res = true;
1379                         }
1380                     }
1381                 }
1382             }
1383             cleanup();
1384             return res;
1385         }
1386         private void checkValidWidgets() {
1387             for (int i = 0; i < _queue.length; i++) {
1388                 Widget w = _queue[i].targetWidget;
1389                 if (w && !isChild(w))
1390                     _queue[i].cancel();
1391             }
1392             cleanup();
1393         }
1394     }
1395 
1396 
1397 }
1398 
1399 /**
1400  * Platform abstraction layer.
1401  * 
1402  * Represents application.
1403  * 
1404  * 
1405  * 
1406  */
1407 class Platform {
1408     static __gshared Platform _instance;
1409     static void setInstance(Platform instance) {
1410         if (_instance)
1411             destroy(_instance);
1412         _instance = instance;
1413     }
1414     @property static Platform instance() {
1415         return _instance;
1416     }
1417 
1418     /**
1419      * create window
1420      * Args:
1421      *         windowCaption = window caption text
1422      *         parent = parent Window, or null if no parent
1423      *         flags = WindowFlag bit set, combination of Resizable, Modal, Fullscreen
1424      *      width = window width 
1425      *      height = window height
1426      * 
1427      * Window w/o Resizable nor Fullscreen will be created with size based on measurement of its content widget
1428      */
1429     abstract Window createWindow(dstring windowCaption, Window parent, uint flags = WindowFlag.Resizable, uint width = 0, uint height = 0);
1430 
1431     static if (ENABLE_OPENGL) {
1432         /**
1433          * OpenGL context major version.
1434          * Note: if the version is invalid or not supported, this value will be set to supported one.
1435          */
1436         int GLVersionMajor = 3;
1437         /**
1438          * OpenGL context minor version.
1439          * Note: if the version is invalid or not supported, this value will be set to supported one.
1440          */
1441         int GLVersionMinor = 2;
1442     }
1443     /**
1444      * close window
1445      * 
1446      * Closes window earlier created with createWindow()
1447      */
1448     abstract void closeWindow(Window w);
1449     /**
1450      * Starts application message loop.
1451      * 
1452      * When returned from this method, application is shutting down.
1453      */
1454     abstract int enterMessageLoop();
1455     /// retrieves text from clipboard (when mouseBuffer == true, use mouse selection clipboard - under linux)
1456     abstract dstring getClipboardText(bool mouseBuffer = false);
1457     /// sets text to clipboard (when mouseBuffer == true, use mouse selection clipboard - under linux)
1458     abstract void setClipboardText(dstring text, bool mouseBuffer = false);
1459 
1460     /// calls request layout for all windows
1461     abstract void requestLayout();
1462 
1463     /// returns true if there is some modal window opened above this window, and this window should not process mouse/key input and should not allow closing
1464     bool hasModalWindowsAbove(Window w) {
1465         // override in platform specific class
1466         return false;
1467     }
1468 
1469 
1470     protected string _uiLanguage;
1471     /// returns currently selected UI language code
1472     @property string uiLanguage() {
1473         return _uiLanguage;
1474     }
1475     /// set UI language (e.g. "en", "fr", "ru") - will relayout content of all windows if language has been changed
1476     @property Platform uiLanguage(string langCode) {
1477         if (_uiLanguage.equal(langCode))
1478             return this;
1479         _uiLanguage = langCode;
1480 
1481         Log.v("Loading language file");
1482         if (langCode.equal("en"))
1483             i18n.load("en.ini"); //"ru.ini", "en.ini"
1484         else
1485             i18n.load(langCode ~ ".ini", "en.ini");
1486         Log.v("Calling onThemeChanged");
1487         onThemeChanged();
1488         requestLayout();
1489         return this;
1490     }
1491     protected string _themeId;
1492     @property string uiTheme() {
1493         return _themeId;
1494     }
1495     /// sets application UI theme - will relayout content of all windows if theme has been changed
1496     @property Platform uiTheme(string themeResourceId) {
1497         if (_themeId.equal(themeResourceId))
1498             return this;
1499         Log.v("uiTheme setting new theme ", themeResourceId);
1500         _themeId = themeResourceId;
1501         Theme theme = loadTheme(themeResourceId);
1502         if (!theme) {
1503             Log.e("Cannot load theme from resource ", themeResourceId, " - will use default theme");
1504             theme = createDefaultTheme();
1505         } else {
1506             Log.i("Applying loaded theme ", theme.id);
1507         }
1508         currentTheme = theme;
1509         onThemeChanged();
1510         requestLayout();
1511         return this;
1512     }
1513 
1514     /// to set uiLanguage and themeId to default (en, theme_default) if not set yet
1515     protected void setDefaultLanguageAndThemeIfNecessary() {
1516         if (!_uiLanguage) {
1517             Log.v("setDefaultLanguageAndThemeIfNecessary : setting UI language");
1518             uiLanguage = "en";
1519         }
1520         if (!_themeId) {
1521             Log.v("setDefaultLanguageAndThemeIfNecessary : setting UI theme");
1522             uiTheme = "theme_default";
1523         }
1524     }
1525 
1526     protected ulong _uiDialogDisplayMode = DialogDisplayMode.messageBoxInPopup | DialogDisplayMode.inputBoxInPopup;
1527     /// returns how dialogs should be displayed - as popup or window
1528     @property ulong uiDialogDisplayMode() {
1529         return _uiDialogDisplayMode;
1530     }
1531     
1532     // sets how dialogs should be displayed - as popup or window - use DialogDisplayMode enumeration
1533     @property Platform uiDialogDisplayMode(ulong newDialogDisplayMode) {
1534         _uiDialogDisplayMode = newDialogDisplayMode;
1535         return this;
1536     }
1537     
1538     protected string[] _resourceDirs;
1539     /// returns list of resource directories
1540     @property string[] resourceDirs() { return _resourceDirs; }
1541     /// set list of directories to load resources from
1542     @property Platform resourceDirs(string[] dirs) {
1543         // setup resource directories - will use only existing directories
1544         drawableCache.setResourcePaths(dirs);
1545         _resourceDirs = drawableCache.resourcePaths;
1546         // setup i18n - look for i18n directory inside one of passed directories
1547         i18n.findTranslationsDir(dirs);
1548         return this;
1549     }
1550 
1551     /// open url in external browser
1552     bool openURL(string url) {
1553         import std.process;
1554         browse(url);
1555         return true;
1556     }
1557 
1558     /// show directory or file in OS file manager (explorer, finder, etc...)
1559     bool showInFileManager(string pathName) {
1560         static import dlangui.core.filemanager;
1561         return dlangui.core.filemanager.showInFileManager(pathName);
1562     }
1563 
1564     /// handle theme change: e.g. reload some themed resources
1565     void onThemeChanged() {
1566         // override and call dispatchThemeChange for all windows
1567     }
1568 
1569 }
1570 
1571 /// get current platform object instance
1572 @property Platform platform() {
1573     return Platform.instance;
1574 }
1575 
1576 static if (ENABLE_OPENGL) {
1577     private __gshared bool _OPENGL_ENABLED = false;
1578     /// check if hardware acceleration is enabled
1579     @property bool openglEnabled() { return _OPENGL_ENABLED; }
1580     /// call on app initialization if OpenGL support is detected
1581     void setOpenglEnabled(bool enabled = true) {
1582         _OPENGL_ENABLED = enabled;
1583         if (enabled)
1584             glyphDestroyCallback = &onGlyphDestroyedCallback;
1585         else
1586             glyphDestroyCallback = null;
1587     }
1588 } else {
1589     @property bool openglEnabled() { return false; }
1590     void setOpenglEnabled(bool enabled = true) {
1591         // ignore
1592     }
1593 }
1594 
1595 static if (BACKEND_CONSOLE) {
1596     // to remove import
1597     extern(C) int DLANGUImain(string[] args);
1598 } else {
1599     version (Windows) {
1600         // to remove import
1601         extern(Windows) int DLANGUIWinMain(void* hInstance, void* hPrevInstance,
1602                                            char* lpCmdLine, int nCmdShow);
1603     } else {
1604         // to remove import
1605         extern(C) int DLANGUImain(string[] args);
1606     }
1607 }
1608 
1609 /// put "mixin APP_ENTRY_POINT;" to main module of your dlangui based app
1610 mixin template APP_ENTRY_POINT() {
1611     static if (BACKEND_CONSOLE) {
1612         int main(string[] args)
1613         {
1614             return DLANGUImain(args);
1615         }
1616     } else {
1617         /// workaround for link issue when WinMain is located in library
1618         version(Windows) {
1619             extern (Windows) int WinMain(void* hInstance, void* hPrevInstance,
1620                                          char* lpCmdLine, int nCmdShow)
1621             {
1622                 try {
1623                     int res = DLANGUIWinMain(hInstance, hPrevInstance,
1624                                              lpCmdLine, nCmdShow);
1625                     return res;
1626                 } catch (Exception e) {
1627                     Log.e("Exception: ", e);
1628                     return 1;
1629                 }
1630             }
1631         } else {
1632             version (Android) {
1633             } else {
1634                 int main(string[] args)
1635                 {
1636                     return DLANGUImain(args);
1637                 }
1638             }
1639         }
1640     }
1641 }
1642 
1643 /// initialize font manager on startup
1644 extern(C) bool initFontManager();
1645 /// initialize logging (for win32 - to file ui.log, for other platforms - stderr; log level is TRACE for debug builds, and WARN for release builds)
1646 extern(C) void initLogs();
1647 /// call this when all resources are supposed to be freed to report counts of non-freed resources by type
1648 extern(C) void releaseResourcesOnAppExit();
1649 /// call this on application initialization
1650 extern(C) void initResourceManagers();
1651 /// call this from shared static this()
1652 extern (C) void initSharedResourceManagers();
1653 
1654