dlangui/src/dlangui/platforms/common/platform.d

1875 lines
71 KiB
D

// Written in the D programming language.
/**
This module contains common Plaform definitions.
Platform is abstraction layer for application.
Synopsis:
----
import dlangui.platforms.common.platform;
----
Copyright: Vadim Lopatin, 2014
License: Boost License 1.0
Authors: Vadim Lopatin, coolreader.org@gmail.com
*/
module dlangui.platforms.common.platform;
public import dlangui.core.config;
public import dlangui.core.events;
import dlangui.core.collections;
import dlangui.widgets.widget;
import dlangui.widgets.popup;
import dlangui.widgets.scrollbar;
import dlangui.graphics.drawbuf;
import dlangui.core.stdaction;
import dlangui.core.asyncsocket;
static if (ENABLE_OPENGL) {
private import dlangui.graphics.gldrawbuf;
}
private import std.algorithm;
private import core.sync.mutex;
private import std.string;
/// entry point - declare such function to use as main for dlangui app
extern(C) int UIAppMain(string[] args);
// specify debug=DebugMouseEvents for logging mouse handling
// specify debug=DebugRedraw for logging drawing and layouts handling
// specify debug=DebugKeys for logging of key events
/// window creation flags
enum WindowFlag : uint {
/// window can be resized
Resizable = 1,
/// window should be shown in fullscreen mode
Fullscreen = 2,
/// modal window - grabs input focus
Modal = 4,
}
/// Window states
enum WindowState : int {
/// state is unknown (not supported by platform?), as well for using in setWindowState when only want to activate window or change its size/position
unspecified,
/// normal state
normal,
/// window is maximized
maximized,
/// window is maximized
minimized,
/// fullscreen mode (supported not on all platforms)
fullscreen,
/// application is paused (e.g. on Android)
paused,
/// window is hidden
hidden,
/// closed
closed,
}
/// Dialog display modes - used to configure dialogs should be showed as a popup or window
enum DialogDisplayMode : ulong {
/// show all types of dialogs in windows
allTypesOfDialogsInWindow = 0,
/// show file dialogs in popups
fileDialogInPopup = 1,
/// show message boxes in popups
messageBoxInPopup = 2,
/// show input boxes in popups
inputBoxInPopup = 4,
/// show settings dialogs in popups
settingsDialogInPopup = 8,
/// show user dialogs in popups - flag for user dialogs
userDialogInPopup = 16,
/// show all types of dialogs in popups
allTypesOfDialogsInPopup = fileDialogInPopup | messageBoxInPopup | inputBoxInPopup | settingsDialogInPopup | userDialogInPopup
}
/// Sets what's should be done when window content is too big
enum WindowOrContentResizeMode {
/// widgets are shrink to fit in window
shrinkWidgets,
/// resize window when window is too small
resizeWindow,
/// add scrollbars to window
scrollWindow
}
/// Window state signal listener
interface OnWindowStateHandler {
/// signal listener - called when state of window is changed
bool onWindowStateChange(Window window, WindowState winState, Rect rect);
}
/// protected event list
/// references to posted messages can be stored here at least to keep live reference and avoid GC
/// as well, on some platforms it's easy to send id to message queue, but not pointer
class EventList {
protected Mutex _mutex;
protected Collection!CustomEvent _events;
this() {
_mutex = new Mutex();
}
~this() {
destroy(_mutex);
_mutex = null;
}
/// puts event into queue, returns event's unique id
long put(CustomEvent event) {
_mutex.lock();
scope(exit) _mutex.unlock();
_events.pushBack(event);
return event.uniqueId;
}
/// return next event
CustomEvent get() {
_mutex.lock();
scope(exit) _mutex.unlock();
return _events.popFront();
}
/// return event by unique id
CustomEvent get(uint uniqueId) {
_mutex.lock();
scope(exit) _mutex.unlock();
for (int i = 0; i < _events.length; i++) {
if (_events[i].uniqueId == uniqueId) {
return _events.remove(i);
}
}
// not found
return null;
}
}
class TimerInfo {
static __gshared ulong nextId;
this(Widget targetWidget, long intervalMillis) {
_id = ++nextId;
assert(intervalMillis >= 0 && intervalMillis < 7*24*60*60*1000L);
_targetWidget = targetWidget;
_interval = intervalMillis;
_nextTimestamp = currentTimeMillis + _interval;
}
/// cancel timer
void cancel() {
_targetWidget = null;
}
/// cancel timer
void notify() {
if (_targetWidget) {
_nextTimestamp = currentTimeMillis + _interval;
if (!_targetWidget.onTimer(_id)) {
_targetWidget = null;
}
}
}
/// unique Id of timer
@property ulong id() { return _id; }
/// timer interval, milliseconds
@property long interval() { return _interval; }
/// next timestamp to invoke timer at, as per currentTimeMillis()
@property long nextTimestamp() { return _nextTimestamp; }
/// widget to route timer event to
@property Widget targetWidget() { return _targetWidget; }
/// return true if timer is not yet cancelled
@property bool valid() { return _targetWidget !is null; }
protected ulong _id;
protected long _interval;
protected long _nextTimestamp;
protected Widget _targetWidget;
override bool opEquals(Object obj) const {
TimerInfo b = cast(TimerInfo)obj;
if (!b)
return false;
return b._nextTimestamp == _nextTimestamp;
}
override int opCmp(Object obj) {
TimerInfo b = cast(TimerInfo)obj;
if (!b)
return false;
if (valid && !b.valid)
return -1;
if (!valid && b.valid)
return 1;
if (!valid && !b.valid)
return 0;
if (_nextTimestamp < b._nextTimestamp)
return -1;
if (_nextTimestamp > b._nextTimestamp)
return 1;
return 0;
}
}
/**
* Window abstraction layer. Widgets can be shown only inside window.
*
*/
class Window : CustomEventTarget {
protected int _dx;
protected int _dy;
protected uint _keyboardModifiers;
protected uint _backgroundColor;
protected Widget _mainWidget;
protected EventList _eventList;
protected uint _flags;
/// minimal good looking content width
protected int _minContentWidth;
/// minimal good looking content height
protected int _minContentHeight;
// current content width calculated using _windowOrContentResizeMode flag, usually used in measure()
protected int _currentContentWidth;
// current content height calculated using _windowOrContentResizeMode flag, usually used in measure()
protected int _currentContentHeight;
@property uint flags() { return _flags; }
@property uint backgroundColor() const { return _backgroundColor; }
@property void backgroundColor(uint color) { _backgroundColor = color; }
@property int width() const { return _dx; }
@property int height() const { return _dy; }
@property uint keyboardModifiers() const { return _keyboardModifiers; }
@property Widget mainWidget() { return _mainWidget; }
@property void mainWidget(Widget widget) {
if (_mainWidget !is null) {
_mainWidget.window = null;
destroy(_mainWidget);
}
_mainWidget = widget;
if (_mainWidget !is null)
_mainWidget.window = this;
}
protected Rect _caretRect;
/// blinking caret position (empty rect if no blinking caret)
@property void caretRect(Rect rc) { _caretRect = rc; }
@property Rect caretRect() { return _caretRect; }
protected bool _caretReplace;
/// blinking caret is in Replace mode if true, insert mode if false
@property void caretReplace(bool flg) { _caretReplace = flg; }
@property bool caretReplace() { return _caretReplace; }
// window content resize mode
//protected WindowOrContentResizeMode _windowOrContentResizeMode = WindowOrContentResizeMode.resizeWindow;
//protected WindowOrContentResizeMode _windowOrContentResizeMode = WindowOrContentResizeMode.shrinkWidgets;
protected WindowOrContentResizeMode _windowOrContentResizeMode = WindowOrContentResizeMode.scrollWindow;
@property WindowOrContentResizeMode windowOrContentResizeMode() {return _windowOrContentResizeMode; }
@property void windowOrContentResizeMode(WindowOrContentResizeMode newMode) {
_windowOrContentResizeMode = newMode;
if (_mainWidget) {
_mainWidget.measure(SIZE_UNSPECIFIED, SIZE_UNSPECIFIED);
adjustWindowOrContentSize(_mainWidget.measuredWidth, _mainWidget.measuredHeight);
}
}
// Abstract methods : override in platform implementation
/// show window
abstract void show();
/// returns window caption
abstract @property dstring windowCaption();
/// sets window caption
abstract @property void windowCaption(dstring caption);
/// sets window icon
abstract @property void windowIcon(DrawBufRef icon);
/// request window redraw
abstract void invalidate();
/// close window
abstract void close();
protected WindowState _windowState = WindowState.normal;
/// returns current window state
@property WindowState windowState() {
return _windowState;
}
protected Rect _windowRect = RECT_VALUE_IS_NOT_SET;
/// returns window rectangle on screen (includes window frame and title)
@property Rect windowRect() {
if (_windowRect != RECT_VALUE_IS_NOT_SET)
return _windowRect;
// fake window rectangle -- at position 0,0 and
return Rect(0, 0, _dx, _dy);
}
/// window state change signal
Signal!OnWindowStateHandler windowStateChanged;
/// update and signal window state and/or size/positon changes - for using in platform inplementations
protected void handleWindowStateChange(WindowState newState, Rect newWindowRect = RECT_VALUE_IS_NOT_SET) {
if (newState != WindowState.unspecified)
_windowState = newState;
if (newWindowRect != RECT_VALUE_IS_NOT_SET)
_windowRect = newWindowRect;
if (windowStateChanged.assigned)
windowStateChanged(this, newState, newWindowRect);
}
/// change window state, position, or size; returns true if successful, false if not supported by platform
bool setWindowState(WindowState newState, bool activate = false, Rect newWindowRect = RECT_VALUE_IS_NOT_SET) {
// override for particular platforms
return false;
}
/// maximize window
bool maximizeWindow(bool activate = false) { return setWindowState(WindowState.maximized, activate); }
/// minimize window
bool minimizeWindow() { return setWindowState(WindowState.minimized); }
/// restore window if maximized/minimized/hidden
bool restoreWindow(bool activate = false) { return setWindowState(WindowState.normal, activate); }
/// restore window if maximized/minimized/hidden
bool hideWindow() { return setWindowState(WindowState.hidden); }
/// just activate window
bool activateWindow() { return setWindowState(WindowState.unspecified, true); }
/// change window position only
bool moveWindow(Point topLeft, bool activate = false) { return setWindowState(WindowState.unspecified, activate, Rect(topLeft.x, topLeft.y, int.min, int.min)); }
/// change window size only
bool resizeWindow(Point sz, bool activate = false) { return setWindowState(WindowState.unspecified, activate, Rect(int.min, int.min, sz.x, sz.y)); }
/// set window rectangle
bool moveAndResizeWindow(Rect rc, bool activate = false) { return setWindowState(WindowState.unspecified, activate, rc); }
// things needed for WindowOrContentResizeMode.scrollWindow:
/// vertical scrollbar control
protected ScrollBar _vScrollBar = null;
/// horizontal scrollbar control
protected ScrollBar _hScrollBar = null;
/// Sets the minimal content size and adjust window or content should be called from window show
void adjustWindowOrContentSize(int minContentWidth, int minContentHeight) {
_minContentWidth = minContentWidth;
_minContentHeight = minContentHeight;
if (_windowOrContentResizeMode == WindowOrContentResizeMode.resizeWindow)
resizeWindow(Point(minContentWidth, minContentHeight));
updateWindowOrContentSize();
}
/// update current content size based on windowOrContentResizeMode flag, usually used when window is resized
void updateWindowOrContentSize() {
final switch (_windowOrContentResizeMode) {
case WindowOrContentResizeMode.shrinkWidgets: {
_currentContentWidth = _windowRect.right;
_currentContentHeight = _windowRect.bottom;
return;
}
case WindowOrContentResizeMode.resizeWindow: {
//maybe should not permit to resize when new window size is smaller than content size, now do nothing
_currentContentWidth = _windowRect.right;
_currentContentHeight = _windowRect.bottom;
break;
}
case WindowOrContentResizeMode.scrollWindow: {
if (_windowRect.right < _minContentWidth) {
// create scrollbar
_currentContentWidth = _minContentWidth;
if (!_hScrollBar) {
_hScrollBar = new ScrollBar(null,Orientation.Horizontal);
_hScrollBar.scrollEvent = delegate bool (AbstractSlider source, ScrollEvent event) {
if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased)
requestLayout();
else if (event.action == ScrollAction.PageUp) {
source.position = max(0, source.position - _windowRect.right * 3 / 4);
requestLayout();
} else if (event.action == ScrollAction.PageDown) {
source.position = min(source.maxValue - _windowRect.right, source.position + _windowRect.right *3 / 4);
requestLayout();
} else if (event.action == ScrollAction.LineUp) {
source.position = max(0, source.position - _windowRect.right / 10);
requestLayout();
} else if (event.action == ScrollAction.LineDown) {
source.position = min(source.maxValue - _windowRect.right, source.position + _windowRect.right / 10);
requestLayout();
}
return true;
};
}
_hScrollBar.measure(_windowRect.right, _windowRect.bottom);
if (windowRect().bottom < _minContentHeight)
_hScrollBar.setRange(0, _minContentWidth + _hScrollBar.measuredHeight);
else
_hScrollBar.setRange(0, _minContentWidth);
_hScrollBar.pageSize(_windowRect.right);
_hScrollBar.position(0);
}
else {
if (_hScrollBar) {
destroy(_hScrollBar);
_hScrollBar = null;
}
_currentContentWidth = _windowRect.right;
}
if (windowRect().bottom < _minContentHeight) {
// create scrollbar
_currentContentHeight = _minContentHeight;
if (!_vScrollBar) {
_vScrollBar = new ScrollBar(null,Orientation.Vertical);
_vScrollBar.scrollEvent = delegate bool (AbstractSlider source, ScrollEvent event) {
if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased)
requestLayout();
else if (event.action == ScrollAction.PageUp) {
source.position = max(0, source.position - _windowRect.bottom * 3 / 4);
requestLayout();
} else if (event.action == ScrollAction.PageDown) {
source.position = min(source.maxValue - _windowRect.bottom, source.position + _windowRect.bottom *3 / 4);
requestLayout();
} else if (event.action == ScrollAction.LineUp) {
source.position = max(0, source.position - _windowRect.bottom / 10);
requestLayout();
} else if (event.action == ScrollAction.LineDown) {
source.position = min(source.maxValue - _windowRect.bottom, source.position + _windowRect.bottom / 10);
requestLayout();
}
return true;
};
}
_vScrollBar.measure(_windowRect.right, _windowRect.bottom);
if (_hScrollBar)
_vScrollBar.setRange(0, _minContentHeight+_hScrollBar.measuredHeight);
else
_vScrollBar.setRange(0, _minContentHeight);
_vScrollBar.pageSize(windowRect().bottom);
_vScrollBar.position(0);
if (!_hScrollBar)
_currentContentWidth = _windowRect.right - _vScrollBar.measuredWidth;
}
else {
if (_vScrollBar) {
destroy(_vScrollBar);
_vScrollBar = null;
}
_currentContentHeight = _hScrollBar ? _windowRect.bottom - _hScrollBar.measuredHeight : _windowRect.bottom;
}
return;
}
}
}
/// requests layout for main widget and popups
void requestLayout() {
if (_mainWidget)
_mainWidget.requestLayout();
if (_hScrollBar)
_hScrollBar.requestLayout();
if (_vScrollBar)
_vScrollBar.requestLayout();
foreach(p; _popups)
p.requestLayout();
if (_tooltip.popup)
_tooltip.popup.requestLayout();
}
void measure() {
if (_hScrollBar)
_hScrollBar.measure(_dx, _dy);
if (_vScrollBar)
_vScrollBar.measure(_dx, _dy);
if (_mainWidget !is null) {
_mainWidget.measure(_currentContentWidth, _currentContentHeight);
}
foreach(p; _popups)
p.measure(_currentContentWidth, _currentContentHeight);
if (_tooltip.popup)
_tooltip.popup.measure(_currentContentWidth, _currentContentHeight);
}
void layout() {
if (_hScrollBar)
_hScrollBar.layout(Rect(0, _dy - _hScrollBar.measuredHeight, _vScrollBar ? _dx - _vScrollBar.measuredWidth : _dx, _dy));
if (_vScrollBar)
_vScrollBar.layout(Rect(_dx - _vScrollBar.measuredWidth, 0, _dx, _hScrollBar ? _dy - _hScrollBar.measuredHeight : _dy));
int deltaX = 0;
if (_hScrollBar)
deltaX = -_hScrollBar.position;
int deltaY = 0;
if (_vScrollBar)
deltaY = -_vScrollBar.position;
Rect rc = Rect(deltaX, deltaY, _currentContentWidth + deltaX, _currentContentHeight + deltaY);
if (_mainWidget !is null) {
_mainWidget.layout(rc);
}
foreach(p; _popups)
p.layout(rc);
if (_tooltip.popup)
_tooltip.popup.layout(rc);
}
void onResize(int width, int height) {
if (_dx == width && _dy == height)
return;
_dx = width;
_dy = height;
// fix window rect for platforms that don't set it yet
_windowRect.right = width;
_windowRect.bottom = height;
if (_mainWidget !is null) {
Log.d("onResize ", _dx, "x", _dy);
long measureStart = currentTimeMillis;
updateWindowOrContentSize();
measure();
//Log.d("measured size: ", _mainWidget.measuredWidth, "x", _mainWidget.measuredHeight);
long measureEnd = currentTimeMillis;
debug Log.d("resize: measure took ", measureEnd - measureStart, " ms");
layout();
long layoutEnd = currentTimeMillis;
debug Log.d("resize: layout took ", layoutEnd - measureEnd, " ms");
//Log.d("layout position: ", _mainWidget.pos);
}
update(true);
}
protected PopupWidget[] _popups;
protected static struct TooltipInfo {
PopupWidget popup;
ulong timerId;
Widget ownerWidget;
uint alignment;
int x;
int y;
}
protected TooltipInfo _tooltip;
/// schedule tooltip for widget be shown with specified delay
void scheduleTooltip(Widget ownerWidget, long delay, uint alignment = PopupAlign.Below, int x = 0, int y = 0) {
_tooltip.alignment = alignment;
_tooltip.x = x;
_tooltip.y = y;
_tooltip.ownerWidget = ownerWidget;
_tooltip.timerId = setTimer(ownerWidget, delay);
}
/// call when tooltip timer is expired
private bool onTooltipTimer() {
_tooltip.timerId = 0;
if (isChild(_tooltip.ownerWidget)) {
Widget w = _tooltip.ownerWidget.createTooltip(_lastMouseX, _lastMouseY, _tooltip.alignment, _tooltip.x, _tooltip.y);
if (w)
showTooltip(w, _tooltip.ownerWidget, _tooltip.alignment, _tooltip.x, _tooltip.y);
}
return false;
}
/// called when user dragged file(s) to application window
void handleDroppedFiles(string[] filenames) {
//Log.d("handleDroppedFiles(", filenames, ")");
if (_onFilesDropped)
_onFilesDropped(filenames);
}
protected void delegate(string[]) _onFilesDropped;
/// get handler for files dropped to app window
@property void delegate(string[]) onFilesDropped() { return _onFilesDropped; }
/// set handler for files dropped to app window
@property Window onFilesDropped(void delegate(string[]) handler) { _onFilesDropped = handler; return this; }
protected bool delegate() _onCanClose;
/// get handler for closing of app (it must return true to allow immediate close, false to cancel close or close window later)
@property bool delegate() onCanClose() { return _onCanClose; }
/// set handler for closing of app (it must return true to allow immediate close, false to cancel close or close window later)
@property Window onCanClose(bool delegate() handler) { _onCanClose = handler; return this; }
protected void delegate() _onClose;
/// get handler for closing of window
@property void delegate() onClose() { return _onClose; }
/// set handler for closing of window
@property Window onClose(void delegate() handler) { _onClose = handler; return this; }
/// 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
bool hasModalWindowsAbove() {
return platform.hasModalWindowsAbove(this);
}
/// calls onCanClose handler if set to check if system may close window
bool handleCanClose() {
if (hasModalWindowsAbove())
return false;
if (!_onCanClose)
return true;
bool res = _onCanClose();
if (!res)
update(true); // redraw window if it was decided to not close immediately
return res;
}
/// hide tooltip if shown and cancel tooltip timer if set
void hideTooltip() {
if (_tooltip.popup) {
destroy(_tooltip.popup);
_tooltip.popup = null;
if (_mainWidget)
_mainWidget.invalidate();
}
if (_tooltip.timerId)
cancelTimer(_tooltip.timerId);
}
/// show tooltip immediately
PopupWidget showTooltip(Widget content, Widget anchor = null, uint alignment = PopupAlign.Center, int x = 0, int y = 0) {
hideTooltip();
if (!content)
return null;
PopupWidget res = new PopupWidget(content, this);
res.anchor.widget = anchor !is null ? anchor : _mainWidget;
res.anchor.alignment = alignment;
res.anchor.x = x;
res.anchor.y = y;
_tooltip.popup = res;
return res;
}
/// show new popup
PopupWidget showPopup(Widget content, Widget anchor = null, uint alignment = PopupAlign.Center, int x = 0, int y = 0) {
PopupWidget res = new PopupWidget(content, this);
res.anchor.widget = anchor !is null ? anchor : _mainWidget;
res.anchor.alignment = alignment;
res.anchor.x = x;
res.anchor.y = y;
_popups ~= res;
if (_mainWidget !is null) {
_mainWidget.requestLayout();
}
return res;
}
/// remove popup
bool removePopup(PopupWidget popup) {
if (!popup)
return false;
for (int i = 0; i < _popups.length; i++) {
PopupWidget p = _popups[i];
if (p is popup) {
for (int j = i; j < _popups.length - 1; j++)
_popups[j] = _popups[j + 1];
_popups.length--;
p.onClose();
destroy(p);
// force redraw
_mainWidget.invalidate();
return true;
}
}
return false;
}
/// returns last modal popup widget, or null if no modal popups opened
PopupWidget modalPopup() {
for (int i = cast(int)_popups.length - 1; i >= 0; i--) {
if (_popups[i].flags & PopupFlags.Modal)
return _popups[i];
}
return null;
}
/// returns true if widget is child of either main widget, one of popups or window scrollbar
bool isChild(Widget w) {
if (_mainWidget !is null && _mainWidget.isChild(w))
return true;
foreach(p; _popups)
if (p.isChild(w))
return true;
if (_tooltip.popup)
if (_tooltip.popup.isChild(w))
return true;
if (_hScrollBar !is null && _hScrollBar.isChild(w))
return true;
if (_vScrollBar !is null && _vScrollBar.isChild(w))
return true;
return false;
}
private long lastDrawTs;
this() {
_eventList = new EventList();
_timerQueue = new TimerQueue();
_backgroundColor = 0xFFFFFF;
if (currentTheme)
_backgroundColor = currentTheme.customColor(STYLE_COLOR_WINDOW_BACKGROUND);
}
~this() {
debug Log.d("Destroying window");
if (_onClose)
_onClose();
if (_tooltip.popup) {
destroy(_tooltip.popup);
_tooltip.popup = null;
}
foreach(p; _popups)
destroy(p);
_popups = null;
if (_mainWidget !is null) {
destroy(_mainWidget);
_mainWidget = null;
}
if (_hScrollBar) {
destroy(_hScrollBar);
_hScrollBar = null;
}
if (_vScrollBar) {
destroy(_vScrollBar);
_vScrollBar = null;
}
destroy(_eventList);
destroy(_timerQueue);
_eventList = null;
}
/**
Allows queue destroy of widget.
Sometimes when you have very complicated UI with dynamic create/destroy lists of widgets calling simple destroy()
on widget makes segmentation fault.
Usually because you destroy widget that on some stage call another that tries to destroy widget that calls it.
When the control flow returns widget not exist and you have seg. fault.
This function use internally $(LINK2 $(DDOX_ROOT_DIR)dlangui/core/events/QueueDestroyEvent.html, QueueDestroyEvent).
*/
void queueWidgetDestroy(Widget widgetToDestroy)
{
QueueDestroyEvent ev = new QueueDestroyEvent(widgetToDestroy);
postEvent(ev);
}
private void animate(Widget root, long interval) {
if (root is null)
return;
if (root.visibility != Visibility.Visible)
return;
for (int i = 0; i < root.childCount; i++)
animate(root.child(i), interval);
if (root.animating)
root.animate(interval);
}
private void animate(long interval) {
animate(_mainWidget, interval);
foreach(p; _popups)
p.animate(interval);
if (_tooltip.popup)
_tooltip.popup.animate(interval);
}
static immutable int PERFORMANCE_LOGGING_THRESHOLD_MS = 20;
/// set when first draw is called: don't handle mouse/key input until draw (layout) is called
protected bool _firstDrawCalled = false;
void onDraw(DrawBuf buf) {
_firstDrawCalled = true;
static import std.datetime;
try {
bool needDraw = false;
bool needLayout = false;
bool animationActive = false;
checkUpdateNeeded(needDraw, needLayout, animationActive);
if (needLayout || animationActive)
needDraw = true;
long ts = std.datetime.Clock.currStdTime;
if (animationActive && lastDrawTs != 0) {
animate(ts - lastDrawTs);
// layout required flag could be changed during animate - check again
checkUpdateNeeded(needDraw, needLayout, animationActive);
}
lastDrawTs = ts;
if (needLayout) {
long measureStart = currentTimeMillis;
measure();
long measureEnd = currentTimeMillis;
if (measureEnd - measureStart > PERFORMANCE_LOGGING_THRESHOLD_MS) {
debug(DebugRedraw) Log.d("measure took ", measureEnd - measureStart, " ms");
}
layout();
long layoutEnd = currentTimeMillis;
if (layoutEnd - measureEnd > PERFORMANCE_LOGGING_THRESHOLD_MS) {
debug(DebugRedraw) Log.d("layout took ", layoutEnd - measureEnd, " ms");
}
//checkUpdateNeeded(needDraw, needLayout, animationActive);
}
long drawStart = currentTimeMillis;
// draw main widget
_mainWidget.onDraw(buf);
PopupWidget modal = modalPopup();
// draw popups
foreach(p; _popups) {
if (p is modal) {
// TODO: get shadow color from theme
buf.fillRect(Rect(0, 0, buf.width, buf.height), 0xD0404040);
}
p.onDraw(buf);
}
if (_tooltip.popup)
_tooltip.popup.onDraw(buf);
if (_hScrollBar)
_hScrollBar.onDraw(buf);
if (_vScrollBar)
_vScrollBar.onDraw(buf);
if (_hScrollBar && _vScrollBar)
buf.fillRect(Rect(_vScrollBar.left, _hScrollBar.top, buf.width, buf.height), _backgroundColor);
long drawEnd = currentTimeMillis;
debug(DebugRedraw) {
if (drawEnd - drawStart > PERFORMANCE_LOGGING_THRESHOLD_MS)
Log.d("draw took ", drawEnd - drawStart, " ms");
}
if (animationActive)
scheduleAnimation();
_actionsUpdateRequested = false;
} catch (Exception e) {
Log.e("Exception inside winfow.onDraw: ", e);
}
}
/// after drawing, call to schedule redraw if animation is active
void scheduleAnimation() {
// override if necessary
}
protected void setCaptureWidget(Widget w, MouseEvent event) {
_mouseCaptureWidget = w;
_mouseCaptureButtons = event.flags & (MouseFlag.LButton|MouseFlag.RButton|MouseFlag.MButton);
}
protected Widget _focusedWidget;
/// returns current focused widget
@property Widget focusedWidget() {
if (!isChild(_focusedWidget))
_focusedWidget = null;
return _focusedWidget;
}
/// change focus to widget
Widget setFocus(Widget newFocus, FocusReason reason = FocusReason.Unspecified) {
if (!isChild(_focusedWidget))
_focusedWidget = null;
Widget oldFocus = _focusedWidget;
auto targetState = State.Focused;
if(reason == FocusReason.TabFocus)
targetState = State.Focused | State.KeyboardFocused;
if (oldFocus is newFocus)
return oldFocus;
if (oldFocus !is null) {
oldFocus.resetState(targetState);
if (oldFocus)
oldFocus.focusGroupFocused(false);
}
if (newFocus is null || isChild(newFocus)) {
if (newFocus !is null) {
// when calling, setState(focused), window.focusedWidget is still previously focused widget
debug(DebugFocus) Log.d("new focus: ", newFocus.id);
newFocus.setState(targetState);
}
_focusedWidget = newFocus;
if (_focusedWidget)
_focusedWidget.focusGroupFocused(true);
// after focus change, ask for actions update automatically
//requestActionsUpdate();
}
return _focusedWidget;
}
/// dispatch key event to widgets which have wantsKeyTracking == true
protected bool dispatchKeyEvent(Widget root, KeyEvent event) {
if (root.visibility != Visibility.Visible)
return false;
if (root.wantsKeyTracking) {
if (root.onKeyEvent(event))
return true;
}
for (int i = 0; i < root.childCount; i++) {
Widget w = root.child(i);
if (dispatchKeyEvent(w, event))
return true;
}
return false;
}
/// dispatch keyboard event
bool dispatchKeyEvent(KeyEvent event) {
if (hasModalWindowsAbove() || !_firstDrawCalled)
return false;
bool res = false;
hideTooltip();
PopupWidget modal = modalPopup();
if (event.action == KeyAction.KeyDown || event.action == KeyAction.KeyUp) {
_keyboardModifiers = event.flags;
if (event.keyCode == KeyCode.ALT || event.keyCode == KeyCode.LALT || event.keyCode == KeyCode.RALT) {
debug(DebugKeys) Log.d("ALT key: keyboardModifiers = ", _keyboardModifiers);
if (_mainWidget) {
_mainWidget.invalidate();
res = true;
}
}
}
if (event.action == KeyAction.Text) {
// filter text
if (event.text.length < 1)
return res;
dchar ch = event.text[0];
if (ch < ' ' || ch == 0x7F) // filter out control symbols
return res;
}
Widget focus = focusedWidget;
if (!modal || modal.isChild(focus)) {
while (focus) {
if (focus.onKeyEvent(event))
return true; // processed by focused widget
if (focus.focusGroup)
break;
focus = focus.parent;
}
}
if (modal) {
if (dispatchKeyEvent(modal, event))
return res;
return modal.onKeyEvent(event) || res;
} else if (_mainWidget) {
if (dispatchKeyEvent(_mainWidget, event))
return res;
return _mainWidget.onKeyEvent(event) || res;
}
return res;
}
protected bool dispatchMouseEvent(Widget root, MouseEvent event, ref bool cursorIsSet) {
// only route mouse events to visible widgets
if (root.visibility != Visibility.Visible)
return false;
if (!root.isPointInside(event.x, event.y))
return false;
// offer event to children first
for (int i = 0; i < root.childCount; i++) {
Widget child = root.child(i);
if (dispatchMouseEvent(child, event, cursorIsSet))
return true;
}
if (event.action == MouseAction.Move && !cursorIsSet) {
uint cursorType = root.getCursorType(event.x, event.y);
if (cursorType != CursorType.Parent) {
setCursorType(cursorType);
cursorIsSet = true;
}
}
// if not processed by children, offer event to root
if (sendAndCheckOverride(root, event)) {
debug(DebugMouseEvents) Log.d("MouseEvent is processed");
if (event.action == MouseAction.ButtonDown && _mouseCaptureWidget is null && !event.doNotTrackButtonDown) {
debug(DebugMouseEvents) Log.d("Setting active widget");
setCaptureWidget(root, event);
} else if (event.action == MouseAction.Move) {
addTracking(root);
}
return true;
}
return false;
}
/// widget which tracks Move events
//protected Widget _mouseTrackingWidget;
protected Widget[] _mouseTrackingWidgets;
private void addTracking(Widget w) {
for(int i = 0; i < _mouseTrackingWidgets.length; i++)
if (w is _mouseTrackingWidgets[i])
return;
//foreach(widget; _mouseTrackingWidgets)
// if (widget is w)
// return;
//Log.d("addTracking ", w.id, " items before: ", _mouseTrackingWidgets.length);
_mouseTrackingWidgets ~= w;
//Log.d("addTracking ", w.id, " items after: ", _mouseTrackingWidgets.length);
}
private bool checkRemoveTracking(MouseEvent event) {
bool res = false;
for(int i = cast(int)_mouseTrackingWidgets.length - 1; i >=0; i--) {
Widget w = _mouseTrackingWidgets[i];
if (!isChild(w)) {
// std.algorithm.remove does not work for me
//_mouseTrackingWidgets.remove(i);
for (int j = i; j < _mouseTrackingWidgets.length - 1; j++)
_mouseTrackingWidgets[j] = _mouseTrackingWidgets[j + 1];
_mouseTrackingWidgets.length--;
continue;
}
if (event.action == MouseAction.Leave || !w.isPointInside(event.x, event.y)) {
// send Leave message
MouseEvent leaveEvent = new MouseEvent(event);
leaveEvent.changeAction(MouseAction.Leave);
res = w.onMouseEvent(leaveEvent) || res;
// std.algorithm.remove does not work for me
//Log.d("removeTracking ", w.id, " items before: ", _mouseTrackingWidgets.length);
//_mouseTrackingWidgets.remove(i);
//_mouseTrackingWidgets.length--;
for (int j = i; j < _mouseTrackingWidgets.length - 1; j++)
_mouseTrackingWidgets[j] = _mouseTrackingWidgets[j + 1];
_mouseTrackingWidgets.length--;
//Log.d("removeTracking ", w.id, " items after: ", _mouseTrackingWidgets.length);
}
}
return res;
}
/// widget which tracks all events after processed ButtonDown
protected Widget _mouseCaptureWidget;
protected ushort _mouseCaptureButtons;
protected bool _mouseCaptureFocusedOut;
/// does current capture widget want to receive move events even if pointer left it
protected bool _mouseCaptureFocusedOutTrackMovements;
protected void clearMouseCapture() {
_mouseCaptureWidget = null;
_mouseCaptureFocusedOut = false;
_mouseCaptureFocusedOutTrackMovements = false;
_mouseCaptureButtons = 0;
}
protected bool dispatchCancel(MouseEvent event) {
event.changeAction(MouseAction.Cancel);
bool res = _mouseCaptureWidget.onMouseEvent(event);
clearMouseCapture();
return res;
}
protected bool sendAndCheckOverride(Widget widget, MouseEvent event) {
if (!isChild(widget))
return false;
bool res = widget.onMouseEvent(event);
if (event.trackingWidget !is null && _mouseCaptureWidget !is event.trackingWidget) {
setCaptureWidget(event.trackingWidget, event);
}
return res;
}
/// returns true if mouse is currently captured
bool isMouseCaptured() {
return (_mouseCaptureWidget !is null && isChild(_mouseCaptureWidget));
}
/// dispatch action to main widget
bool dispatchAction(const Action action, Widget sourceWidget = null) {
// try to handle by source widget
if(sourceWidget && isChild(sourceWidget)) {
if (sourceWidget.handleAction(action))
return true;
sourceWidget = sourceWidget.parent;
}
Widget focus = focusedWidget;
// then offer action to focused widget
if (focus && isChild(focus)) {
if (focus.handleAction(action))
return true;
focus = focus.parent;
}
// then offer to parent chain of source widget
while (sourceWidget && isChild(sourceWidget)) {
if (sourceWidget.handleAction(action))
return true;
sourceWidget = sourceWidget.parent;
}
// then offer to parent chain of focused widget
while (focus && isChild(focus)) {
if (focus.handleAction(action))
return true;
focus = focus.parent;
}
if (_mainWidget)
return _mainWidget.handleAction(action);
return false;
}
/// dispatch action to main widget
bool dispatchActionStateRequest(const Action action, Widget sourceWidget = null) {
// try to handle by source widget
if(sourceWidget && isChild(sourceWidget)) {
if (sourceWidget.handleActionStateRequest(action))
return true;
sourceWidget = sourceWidget.parent;
}
Widget focus = focusedWidget;
// then offer action to focused widget
if (focus && isChild(focus)) {
if (focus.handleActionStateRequest(action))
return true;
focus = focus.parent;
}
// then offer to parent chain of source widget
while (sourceWidget && isChild(sourceWidget)) {
if (sourceWidget.handleActionStateRequest(action))
return true;
sourceWidget = sourceWidget.parent;
}
// then offer to parent chain of focused widget
while (focus && isChild(focus)) {
if (focus.handleActionStateRequest(action))
return true;
focus = focus.parent;
}
if (_mainWidget)
return _mainWidget.handleActionStateRequest(action);
return false;
}
/// handle theme change: e.g. reload some themed resources
void dispatchThemeChanged() {
if (_hScrollBar)
_hScrollBar.onThemeChanged();
if (_vScrollBar)
_vScrollBar.onThemeChanged();
if (_mainWidget)
_mainWidget.onThemeChanged();
// draw popups
foreach(p; _popups) {
p.onThemeChanged();
}
if (_tooltip.popup)
_tooltip.popup.onThemeChanged();
if (currentTheme) {
_backgroundColor = currentTheme.customColor(STYLE_COLOR_WINDOW_BACKGROUND);
}
invalidate();
}
/// post event to handle in UI thread (this method can be used from background thread)
void postEvent(CustomEvent event) {
// override to post event into window message queue
_eventList.put(event);
}
/// post task to execute in UI thread (this method can be used from background thread)
void executeInUiThread(void delegate() runnable) {
RunnableEvent event = new RunnableEvent(CUSTOM_RUNNABLE, null, runnable);
postEvent(event);
}
/// Creates async socket
AsyncSocket createAsyncSocket(AsyncSocketCallback callback) {
AsyncClientConnection conn = new AsyncClientConnection(new AsyncSocketCallbackProxy(callback, &executeInUiThread));
return conn;
}
/// remove event from queue by unique id if not yet dispatched (this method can be used from background thread)
void cancelEvent(uint uniqueId) {
CustomEvent ev = _eventList.get(uniqueId);
if (ev) {
//destroy(ev);
}
}
/// remove event from queue by unique id if not yet dispatched and dispatch it
void handlePostedEvent(uint uniqueId) {
CustomEvent ev = _eventList.get(uniqueId);
if (ev) {
dispatchCustomEvent(ev);
}
}
/// handle all events from queue, if any (call from UI thread only)
void handlePostedEvents() {
for(;;) {
CustomEvent e = _eventList.get();
if (!e)
break;
dispatchCustomEvent(e);
}
}
/// dispatch custom event
bool dispatchCustomEvent(CustomEvent event) {
if (event.destinationWidget) {
if (!isChild(event.destinationWidget)) {
//Event is sent to widget which does not exist anymore
return false;
}
return event.destinationWidget.onEvent(event);
} else {
// no destination widget
RunnableEvent runnable = cast(RunnableEvent)event;
if (runnable) {
// handle runnable
runnable.run();
return true;
}
}
return false;
}
private int _lastMouseX;
private int _lastMouseY;
/// dispatch mouse event to window content widgets
bool dispatchMouseEvent(MouseEvent event) {
if (hasModalWindowsAbove() || !_firstDrawCalled)
return false;
// ignore events if there is no root
if (_mainWidget is null)
return false;
bool actualChange = true;
if (event.action == MouseAction.Move) {
actualChange = (_lastMouseX != event.x || _lastMouseY != event.y);
_lastMouseX = event.x;
_lastMouseY = event.y;
}
if (actualChange) hideTooltip();
PopupWidget modal = modalPopup();
// check if _mouseCaptureWidget and _mouseTrackingWidget still exist in child of root widget
if (_mouseCaptureWidget !is null && (!isChild(_mouseCaptureWidget) || (modal && !modal.isChild(_mouseCaptureWidget)))) {
clearMouseCapture();
}
debug(DebugMouseEvents) Log.d("dispatchMouseEvent ", event.action, " (", event.x, ",", event.y, ")");
bool res = false;
ushort currentButtons = event.flags & (MouseFlag.LButton|MouseFlag.RButton|MouseFlag.MButton);
if (_mouseCaptureWidget !is null) {
// try to forward message directly to active widget
if (event.action == MouseAction.Move) {
debug(DebugMouseEvents) Log.d("dispatchMouseEvent: Move; buttons state=", currentButtons);
if (!_mouseCaptureWidget.isPointInside(event.x, event.y)) {
if (currentButtons != _mouseCaptureButtons) {
debug(DebugMouseEvents) Log.d("dispatchMouseEvent: Move; buttons state changed from ", _mouseCaptureButtons, " to ", currentButtons, " - cancelling capture");
return dispatchCancel(event);
}
// point is no more inside of captured widget
if (!_mouseCaptureFocusedOut) {
// sending FocusOut message
event.changeAction(MouseAction.FocusOut);
_mouseCaptureFocusedOut = true;
_mouseCaptureButtons = currentButtons;
_mouseCaptureFocusedOutTrackMovements = sendAndCheckOverride(_mouseCaptureWidget, event);
return true;
} else if (_mouseCaptureFocusedOutTrackMovements) {
// pointer is outside, but we still need to track pointer
return sendAndCheckOverride(_mouseCaptureWidget, event);
}
// don't forward message
return true;
} else {
// point is inside widget
if (_mouseCaptureFocusedOut) {
_mouseCaptureFocusedOut = false;
if (currentButtons != _mouseCaptureButtons)
return dispatchCancel(event);
event.changeAction(MouseAction.FocusIn); // back in after focus out
}
return sendAndCheckOverride(_mouseCaptureWidget, event);
}
} else if (event.action == MouseAction.Leave) {
if (!_mouseCaptureFocusedOut) {
// sending FocusOut message
event.changeAction(MouseAction.FocusOut);
_mouseCaptureFocusedOut = true;
_mouseCaptureButtons = event.flags & (MouseFlag.LButton|MouseFlag.RButton|MouseFlag.MButton);
return sendAndCheckOverride(_mouseCaptureWidget, event);
} else {
debug(DebugMouseEvents) Log.d("dispatchMouseEvent: mouseCaptureFocusedOut + Leave - cancelling capture");
return dispatchCancel(event);
}
} else if (event.action == MouseAction.ButtonDown || event.action == MouseAction.ButtonUp) {
if (!_mouseCaptureWidget.isPointInside(event.x, event.y)) {
if (currentButtons != _mouseCaptureButtons) {
debug(DebugMouseEvents) Log.d("dispatchMouseEvent: ButtonUp/ButtonDown; buttons state changed from ", _mouseCaptureButtons, " to ", currentButtons, " - cancelling capture");
return dispatchCancel(event);
}
}
}
// other messages
res = sendAndCheckOverride(_mouseCaptureWidget, event);
if (!currentButtons) {
// usable capturing - no more buttons pressed
debug(DebugMouseEvents) Log.d("unsetting active widget");
clearMouseCapture();
}
return res;
}
bool processed = false;
if (event.action == MouseAction.Move || event.action == MouseAction.Leave) {
processed = checkRemoveTracking(event);
}
bool cursorIsSet = false;
if (!res) {
bool insideOneOfPopups = false;
for (int i = cast(int)_popups.length - 1; i >= 0; i--) {
auto p = _popups[i];
if (p.isPointInside(event.x, event.y)) {
if (p !is modal)
insideOneOfPopups = true;
}
if (p is modal)
break;
}
for (int i = cast(int)_popups.length - 1; i >= 0; i--) {
auto p = _popups[i];
if (p is modal)
break;
if (!insideOneOfPopups) {
if (p.onMouseEventOutside(event)) // stop loop when true is returned, but allow other main widget to handle event
break;
} else {
if (dispatchMouseEvent(p, event, cursorIsSet))
return true;
}
}
if (!modal) {
res = false;
if (_hScrollBar)
res = dispatchMouseEvent(_hScrollBar, event, cursorIsSet);
if (!res && _vScrollBar)
res = dispatchMouseEvent(_vScrollBar, event, cursorIsSet);
if (!res)
res = dispatchMouseEvent(_mainWidget, event, cursorIsSet);
}
else {
if (_hScrollBar)
res = dispatchMouseEvent(_hScrollBar, event, cursorIsSet);
if (!res && _vScrollBar)
res = dispatchMouseEvent(_vScrollBar, event, cursorIsSet);
if (!res)
res = dispatchMouseEvent(modal, event, cursorIsSet);
}
}
return res || processed || _mainWidget.needDraw;
}
/// calls update actions recursively
protected void dispatchWidgetUpdateActionStateRecursive(Widget root) {
if (root is null)
return;
root.updateActionState(true);
for (int i = 0; i < root.childCount; i++)
dispatchWidgetUpdateActionStateRecursive(root.child(i));
}
/// checks content widgets for necessary redraw and/or layout
protected void checkUpdateNeeded(Widget root, ref bool needDraw, ref bool needLayout, ref bool animationActive) {
if (root is null)
return;
if (root.visibility != Visibility.Visible)
return;
needDraw = root.needDraw || needDraw;
if (!needLayout) {
needLayout = root.needLayout || needLayout;
if (needLayout) {
debug(DebugRedraw) Log.d("need layout: ", root.classinfo.name, " id=", root.id);
}
}
if (root.animating && root.visible)
animationActive = true; // check animation only for visible widgets
for (int i = 0; i < root.childCount; i++)
checkUpdateNeeded(root.child(i), needDraw, needLayout, animationActive);
}
/// sets cursor type for window
protected void setCursorType(uint cursorType) {
// override to support different mouse cursors
}
/// update action states
protected void dispatchWidgetUpdateActionStateRecursive() {
if (_mainWidget !is null)
dispatchWidgetUpdateActionStateRecursive(_mainWidget);
foreach(p; _popups)
dispatchWidgetUpdateActionStateRecursive(p);
}
/// checks content widgets for necessary redraw and/or layout
bool checkUpdateNeeded(ref bool needDraw, ref bool needLayout, ref bool animationActive) {
if (_actionsUpdateRequested) {
// call update action check - as requested
dispatchWidgetUpdateActionStateRecursive();
_actionsUpdateRequested = false;
}
needDraw = needLayout = animationActive = false;
if (_mainWidget is null)
return false;
checkUpdateNeeded(_mainWidget, needDraw, needLayout, animationActive);
if (_hScrollBar)
checkUpdateNeeded(_hScrollBar, needDraw, needLayout, animationActive);
if (_vScrollBar)
checkUpdateNeeded(_vScrollBar, needDraw, needLayout, animationActive);
foreach(p; _popups)
checkUpdateNeeded(p, needDraw, needLayout, animationActive);
if (_tooltip.popup)
checkUpdateNeeded(_tooltip.popup, needDraw, needLayout, animationActive);
return needDraw || needLayout || animationActive;
}
protected bool _animationActive;
@property bool isAnimationActive() {
return _animationActive;
}
/// requests update for window (unless force is true, update will be performed only if layout, redraw or animation is required).
void update(bool force = false) {
if (_mainWidget is null)
return;
bool needDraw = false;
bool needLayout = false;
_animationActive = false;
if (checkUpdateNeeded(needDraw, needLayout, _animationActive) || force) {
debug(DebugRedraw) Log.d("Requesting update");
invalidate();
}
debug(DebugRedraw) Log.d("checkUpdateNeeded returned needDraw=", needDraw, " needLayout=", needLayout, " animationActive=", _animationActive);
}
protected bool _actionsUpdateRequested = true;
/// set action update request flag, will be cleared after redraw
void requestActionsUpdate(bool immediateUpdate = false) {
if (!immediateUpdate)
_actionsUpdateRequested = true;
else
dispatchWidgetUpdateActionStateRecursive();
}
@property bool actionsUpdateRequested() {
return _actionsUpdateRequested;
}
/// Show message box with specified title and message (title and message as UIString)
void showMessageBox(UIString title, UIString message, const (Action)[] actions = [ACTION_OK], int defaultActionIndex = 0, bool delegate(const Action result) handler = null) {
import dlangui.dialogs.msgbox;
MessageBox dlg = new MessageBox(title, message, this, actions, defaultActionIndex, handler);
dlg.show();
}
/// Show message box with specified title and message (title and message as dstring)
void showMessageBox(dstring title, dstring message, const (Action)[] actions = [ACTION_OK], int defaultActionIndex = 0, bool delegate(const Action result) handler = null) {
showMessageBox(UIString.fromRaw(title), UIString.fromRaw(message), actions, defaultActionIndex, handler);
}
static if (BACKEND_GUI) {
void showInputBox(UIString title, UIString message, dstring initialText, void delegate(dstring result) handler) {
import dlangui.dialogs.inputbox;
InputBox dlg = new InputBox(title, message, this, initialText, handler);
dlg.show();
}
}
void showInputBox(dstring title, dstring message, dstring initialText, void delegate(dstring result) handler) {
showInputBox(UIString.fromRaw(title), UIString.fromRaw(message), initialText, handler);
}
protected TimerQueue _timerQueue;
/// schedule timer for interval in milliseconds - call window.onTimer when finished
protected void scheduleSystemTimer(long intervalMillis) {
//debug Log.d("override scheduleSystemTimer to support timers");
}
/// poll expired timers; returns true if update is needed
bool pollTimers() {
bool res = _timerQueue.notify();
if (res)
update(false);
return res;
}
/// system timer interval expired - notify queue
protected void onTimer() {
//Log.d("window.onTimer");
bool res = _timerQueue.notify();
//Log.d("window.onTimer after notify");
if (res) {
// check if update needed and redraw if so
//Log.d("before update");
update(false);
//Log.d("after update");
}
//Log.d("schedule next timer");
long nextInterval = _timerQueue.nextIntervalMillis();
if (nextInterval > 0) {
scheduleSystemTimer(nextInterval);
}
//Log.d("schedule next timer done");
}
/// set timer for destination widget - destination.onTimer() will be called after interval expiration; returns timer id
ulong setTimer(Widget destination, long intervalMillis) {
if (!isChild(destination)) {
Log.e("setTimer() is called not for child widget of window");
return 0;
}
ulong res = _timerQueue.add(destination, intervalMillis);
long nextInterval = _timerQueue.nextIntervalMillis();
if (nextInterval > 0) {
scheduleSystemTimer(intervalMillis);
}
return res;
}
/// cancel previously scheduled widget timer (for timerId pass value returned from setTimer)
void cancelTimer(ulong timerId) {
_timerQueue.cancelTimer(timerId);
}
/// timers queue
private class TimerQueue {
protected TimerInfo[] _queue;
/// add new timer
ulong add(Widget destination, long intervalMillis) {
TimerInfo item = new TimerInfo(destination, intervalMillis);
_queue ~= item;
sort(_queue);
return item.id;
}
/// cancel timer
void cancelTimer(ulong timerId) {
if (!_queue.length)
return;
for (int i = cast(int)_queue.length - 1; i >= 0; i--) {
if (_queue[i].id == timerId) {
_queue[i].cancel();
break;
}
}
}
/// returns interval if millis of next scheduled event or -1 if no events queued
long nextIntervalMillis() {
if (!_queue.length || !_queue[0].valid)
return -1;
long delta = _queue[0].nextTimestamp - currentTimeMillis;
if (delta < 1)
delta = 1;
return delta;
}
private void cleanup() {
if (!_queue.length)
return;
sort(_queue);
size_t newsize = _queue.length;
for (int i = cast(int)_queue.length - 1; i >= 0; i--) {
if (!_queue[i].valid) {
newsize = i;
}
}
if (_queue.length > newsize)
_queue.length = newsize;
}
private TimerInfo[] expired() {
if (!_queue.length)
return null;
long ts = currentTimeMillis;
TimerInfo[] res;
for (int i = 0; i < _queue.length; i++) {
if (_queue[i].nextTimestamp <= ts)
res ~= _queue[i];
}
return res;
}
/// returns true if at least one widget was notified
bool notify() {
bool res = false;
checkValidWidgets();
TimerInfo[] list = expired();
if (list) {
for (int i = 0; i < list.length; i++) {
if (_queue[i].id == _tooltip.timerId) {
// special case for tooltip timer
onTooltipTimer();
_queue[i].cancel();
res = true;
} else {
Widget w = _queue[i].targetWidget;
if (w && !isChild(w))
_queue[i].cancel();
else {
_queue[i].notify();
res = true;
}
}
}
}
cleanup();
return res;
}
private void checkValidWidgets() {
for (int i = 0; i < _queue.length; i++) {
Widget w = _queue[i].targetWidget;
if (w && !isChild(w))
_queue[i].cancel();
}
cleanup();
}
}
}
/**
* Platform abstraction layer.
*
* Represents application.
*
*
*
*/
class Platform {
static __gshared Platform _instance;
static void setInstance(Platform instance) {
if (_instance)
destroy(_instance);
_instance = instance;
}
@property static Platform instance() {
return _instance;
}
/**
* create window
* Args:
* windowCaption = window caption text
* parent = parent Window, or null if no parent
* flags = WindowFlag bit set, combination of Resizable, Modal, Fullscreen
* width = window width
* height = window height
*
* Window w/o Resizable nor Fullscreen will be created with size based on measurement of its content widget
*/
abstract Window createWindow(dstring windowCaption, Window parent, uint flags = WindowFlag.Resizable, uint width = 0, uint height = 0);
static if (ENABLE_OPENGL) {
/**
* OpenGL context major version.
* Note: if the version is invalid or not supported, this value will be set to supported one.
*/
int GLVersionMajor = 3;
/**
* OpenGL context minor version.
* Note: if the version is invalid or not supported, this value will be set to supported one.
*/
int GLVersionMinor = 2;
}
/**
* close window
*
* Closes window earlier created with createWindow()
*/
abstract void closeWindow(Window w);
/**
* Starts application message loop.
*
* When returned from this method, application is shutting down.
*/
abstract int enterMessageLoop();
/// retrieves text from clipboard (when mouseBuffer == true, use mouse selection clipboard - under linux)
abstract dstring getClipboardText(bool mouseBuffer = false);
/// sets text to clipboard (when mouseBuffer == true, use mouse selection clipboard - under linux)
abstract void setClipboardText(dstring text, bool mouseBuffer = false);
/// calls request layout for all windows
abstract void requestLayout();
/// 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
bool hasModalWindowsAbove(Window w) {
// override in platform specific class
return false;
}
protected string _uiLanguage;
/// returns currently selected UI language code
@property string uiLanguage() {
return _uiLanguage;
}
/// set UI language (e.g. "en", "fr", "ru") - will relayout content of all windows if language has been changed
@property Platform uiLanguage(string langCode) {
if (_uiLanguage.equal(langCode))
return this;
_uiLanguage = langCode;
Log.v("Loading language file");
if (langCode.equal("en"))
i18n.load("en.ini"); //"ru.ini", "en.ini"
else
i18n.load(langCode ~ ".ini", "en.ini");
Log.v("Calling onThemeChanged");
onThemeChanged();
requestLayout();
return this;
}
protected string _themeId;
@property string uiTheme() {
return _themeId;
}
/// sets application UI theme - will relayout content of all windows if theme has been changed
@property Platform uiTheme(string themeResourceId) {
if (_themeId.equal(themeResourceId))
return this;
Log.v("uiTheme setting new theme ", themeResourceId);
_themeId = themeResourceId;
Theme theme = loadTheme(themeResourceId);
if (!theme) {
Log.e("Cannot load theme from resource ", themeResourceId, " - will use default theme");
theme = createDefaultTheme();
} else {
Log.i("Applying loaded theme ", theme.id);
}
currentTheme = theme;
onThemeChanged();
requestLayout();
return this;
}
/// to set uiLanguage and themeId to default (en, theme_default) if not set yet
protected void setDefaultLanguageAndThemeIfNecessary() {
if (!_uiLanguage) {
Log.v("setDefaultLanguageAndThemeIfNecessary : setting UI language");
uiLanguage = "en";
}
if (!_themeId) {
Log.v("setDefaultLanguageAndThemeIfNecessary : setting UI theme");
uiTheme = "theme_default";
}
}
protected ulong _uiDialogDisplayMode = DialogDisplayMode.messageBoxInPopup | DialogDisplayMode.inputBoxInPopup;
/// returns how dialogs should be displayed - as popup or window
@property ulong uiDialogDisplayMode() {
return _uiDialogDisplayMode;
}
// sets how dialogs should be displayed - as popup or window - use DialogDisplayMode enumeration
@property Platform uiDialogDisplayMode(ulong newDialogDisplayMode) {
_uiDialogDisplayMode = newDialogDisplayMode;
return this;
}
protected string[] _resourceDirs;
/// returns list of resource directories
@property string[] resourceDirs() { return _resourceDirs; }
/// set list of directories to load resources from
@property Platform resourceDirs(string[] dirs) {
// setup resource directories - will use only existing directories
drawableCache.setResourcePaths(dirs);
_resourceDirs = drawableCache.resourcePaths;
// setup i18n - look for i18n directory inside one of passed directories
i18n.findTranslationsDir(dirs);
return this;
}
/// open url in external browser
bool openURL(string url) {
import std.process;
browse(url);
return true;
}
/// show directory or file in OS file manager (explorer, finder, etc...)
bool showInFileManager(string pathName) {
static import dlangui.core.filemanager;
return dlangui.core.filemanager.showInFileManager(pathName);
}
/// handle theme change: e.g. reload some themed resources
void onThemeChanged() {
// override and call dispatchThemeChange for all windows
}
}
/// get current platform object instance
@property Platform platform() {
return Platform.instance;
}
static if (ENABLE_OPENGL) {
private __gshared bool _OPENGL_ENABLED = false;
/// check if hardware acceleration is enabled
@property bool openglEnabled() { return _OPENGL_ENABLED; }
/// call on app initialization if OpenGL support is detected
void setOpenglEnabled(bool enabled = true) {
_OPENGL_ENABLED = enabled;
if (enabled)
glyphDestroyCallback = &onGlyphDestroyedCallback;
else
glyphDestroyCallback = null;
}
} else {
@property bool openglEnabled() { return false; }
void setOpenglEnabled(bool enabled = true) {
// ignore
}
}
static if (BACKEND_CONSOLE) {
// to remove import
extern(C) int DLANGUImain(string[] args);
} else {
version (Windows) {
// to remove import
extern(Windows) int DLANGUIWinMain(void* hInstance, void* hPrevInstance,
char* lpCmdLine, int nCmdShow);
} else {
// to remove import
extern(C) int DLANGUImain(string[] args);
}
}
/// put "mixin APP_ENTRY_POINT;" to main module of your dlangui based app
mixin template APP_ENTRY_POINT() {
static if (BACKEND_CONSOLE) {
int main(string[] args)
{
return DLANGUImain(args);
}
} else {
/// workaround for link issue when WinMain is located in library
version(Windows) {
extern (Windows) int WinMain(void* hInstance, void* hPrevInstance,
char* lpCmdLine, int nCmdShow)
{
try {
int res = DLANGUIWinMain(hInstance, hPrevInstance,
lpCmdLine, nCmdShow);
return res;
} catch (Exception e) {
Log.e("Exception: ", e);
return 1;
}
}
} else {
version (Android) {
} else {
int main(string[] args)
{
return DLANGUImain(args);
}
}
}
}
}
/// initialize font manager on startup
extern(C) bool initFontManager();
/// initialize logging (for win32 - to file ui.log, for other platforms - stderr; log level is TRACE for debug builds, and WARN for release builds)
extern(C) void initLogs();
/// call this when all resources are supposed to be freed to report counts of non-freed resources by type
extern(C) void releaseResourcesOnAppExit();
/// call this on application initialization
extern(C) void initResourceManagers();
/// call this from shared static this()
extern (C) void initSharedResourceManagers();