diff --git a/dlangui-msvc.visualdproj b/dlangui-msvc.visualdproj
index 1e132160..ca70008d 100644
--- a/dlangui-msvc.visualdproj
+++ b/dlangui-msvc.visualdproj
@@ -982,6 +982,7 @@
+
diff --git a/examples/example1/src/example1.d b/examples/example1/src/example1.d
index f23b965e..11174d90 100644
--- a/examples/example1/src/example1.d
+++ b/examples/example1/src/example1.d
@@ -569,8 +569,11 @@ extern (C) int UIAppMain(string[] args) {
LinearLayout layout = new LinearLayout("tab1");
+
layout.addChild((new TextWidget()).textColor(0x00802000).text("Text widget 0"));
layout.addChild((new TextWidget()).textColor(0x40FF4000).text("Text widget"));
+ layout.addChild(new ProgressBarWidget().progress(300).animationInterval(50));
+ layout.addChild(new ProgressBarWidget().progress(-1).animationInterval(50));
layout.addChild((new Button("BTN1")).textResource("EXIT")); //.textColor(0x40FF4000)
layout.addChild(new TimerTest());
diff --git a/src/dlangui/package.d b/src/dlangui/package.d
index 7727d6e2..6e55264a 100644
--- a/src/dlangui/package.d
+++ b/src/dlangui/package.d
@@ -60,6 +60,7 @@ public {
import dlangui.widgets.widget;
import dlangui.widgets.controls;
import dlangui.widgets.scrollbar;
+ import dlangui.widgets.progressbar;
import dlangui.widgets.layouts;
import dlangui.widgets.groupbox;
import dlangui.widgets.lists;
diff --git a/src/dlangui/platforms/common/startup.d b/src/dlangui/platforms/common/startup.d
index f3603261..f43027ee 100644
--- a/src/dlangui/platforms/common/startup.d
+++ b/src/dlangui/platforms/common/startup.d
@@ -327,14 +327,17 @@ void registerStandardWidgets() {
import dlangui.widgets.editors;
import dlangui.widgets.grid;
import dlangui.widgets.groupbox;
+ import dlangui.widgets.progressbar;
import dlangui.dialogs.filedlg;
import dlangui.widgets.menu;
mixin(registerWidgets!(FileNameEditLine, DirEditLine, //dlangui.dialogs.filedlg
ComboBox, ComboEdit, //dlangui.widgets.combobox
Widget, TextWidget, MultilineTextWidget, Button, ImageWidget, ImageButton, ImageCheckButton, ImageTextButton,
- SwitchButton, RadioButton, CheckBox, ScrollBar, SliderWidget, HSpacer, VSpacer, CanvasWidget, // dlangui.widgets.controls
+ SwitchButton, RadioButton, CheckBox, HSpacer, VSpacer, CanvasWidget, // dlangui.widgets.controls
+ ScrollBar, SliderWidget, // dlangui.widgets.scrollbar
EditLine, EditBox, LogWidget,//dlangui.widgets.editors
GroupBox, // dlangui.widgets.groupbox
+ ProgressBarWidget, // dlangui.widgets.progressbar
StringGridWidget, //dlangui.widgets.grid
VerticalLayout, HorizontalLayout, TableLayout, FrameLayout, // dlangui.widgets.layouts
ListWidget, StringListWidget,//dlangui.widgets.lists
diff --git a/src/dlangui/widgets/progressbar.d b/src/dlangui/widgets/progressbar.d
new file mode 100644
index 00000000..d0a9a0b4
--- /dev/null
+++ b/src/dlangui/widgets/progressbar.d
@@ -0,0 +1,192 @@
+// Written in the D programming language.
+
+/**
+This module contains progress bar controls implementation.
+
+ProgressBarWidget - progeress bar control
+
+
+Synopsis:
+
+----
+import dlangui.widgets.progressbar;
+
+----
+
+Copyright: Vadim Lopatin, 2016
+License: Boost License 1.0
+Authors: Vadim Lopatin, coolreader.org@gmail.com
+*/
+module dlangui.widgets.progressbar;
+
+import dlangui.widgets.widget;
+
+enum PROGRESS_INDETERMINATE = -1;
+enum PROGRESS_HIDDEN = -2;
+enum PROGRESS_ANIMATION_OFF = 0;
+enum PROGRESS_MAX = 1000;
+
+/// Base for different progress bar controls
+class AbstractProgressBar : Widget {
+ this(string ID = null, int progress = PROGRESS_INDETERMINATE) {
+ super(ID);
+ _progress = progress;
+ }
+
+ protected int _progress = PROGRESS_INDETERMINATE;
+
+ /// Set current progress value, 0 .. 1000; -1 == indeterminate, -2 == hidden
+ @property AbstractProgressBar progress(int progress) {
+ if (progress < -2)
+ progress = -2;
+ if (progress > 1000)
+ progress = 1000;
+ if (_progress != progress) {
+ _progress = progress;
+ invalidate();
+ }
+ requestLayout();
+ return this;
+ }
+ /// Get current progress value, 0 .. 1000; -1 == indeterminate
+ @property int progress() {
+ return _progress;
+ }
+ /// returns true if progress bar is in indeterminate state
+ @property bool indeterminate() { return _progress == PROGRESS_INDETERMINATE; }
+
+ protected int _animationInterval = 0; // no animation by default
+ /// get animation interval in milliseconds, if 0 - no animation
+ @property int animationInterval() { return _animationInterval; }
+ /// set animation interval in milliseconds, if 0 - no animation
+ @property AbstractProgressBar animationInterval(int animationIntervalMillis) {
+ if (animationIntervalMillis < 0)
+ animationIntervalMillis = 0;
+ if (animationIntervalMillis > 5000)
+ animationIntervalMillis = 5000;
+ if (_animationInterval != animationIntervalMillis) {
+ _animationInterval = animationIntervalMillis;
+ if (!animationIntervalMillis)
+ stopAnimation();
+ else
+ scheduleAnimation();
+ }
+ return this;
+ }
+
+ protected ulong _animationTimerId;
+ protected void scheduleAnimation() {
+ if (!visible || !_animationInterval) {
+ if (_animationTimerId)
+ stopAnimation();
+ return;
+ }
+ stopAnimation();
+ _animationTimerId = setTimer(_animationInterval);
+ invalidate();
+ }
+
+ protected void stopAnimation() {
+ if (_animationTimerId) {
+ cancelTimer(_animationTimerId);
+ _animationTimerId = 0;
+ }
+ _lastAnimationTs = 0;
+ }
+
+ protected int _animationSpeedPixelsPerSecond = 20;
+ protected long _animationPhase;
+ protected long _lastAnimationTs;
+ /// called on animation timer
+ protected void onAnimationTimer(long millisElapsed) {
+ _animationPhase += millisElapsed;
+ invalidate();
+ }
+
+ /// handle timer; return true to repeat timer event after next interval, false cancel timer
+ override bool onTimer(ulong id) {
+ if (id == _animationTimerId) {
+ if (!visible || _progress == PROGRESS_HIDDEN) {
+ stopAnimation();
+ return false;
+ }
+ long elapsed = 0;
+ long ts = currentTimeMillis;
+ if (_lastAnimationTs) {
+ elapsed = ts - _lastAnimationTs;
+ if (elapsed < 0)
+ elapsed = 0;
+ else if (elapsed > 5000)
+ elapsed = 5000;
+ }
+ _lastAnimationTs = ts;
+ onAnimationTimer(elapsed);
+ return _animationInterval != 0;
+ }
+ // return true to repeat after the same interval, false to stop timer
+ return super.onTimer(id);
+ }
+}
+
+/// Progress bar widget
+class ProgressBarWidget : AbstractProgressBar {
+ this(string ID = null, int progress = PROGRESS_INDETERMINATE) {
+ super(ID, progress);
+ styleId = STYLE_PROGRESS_BAR;
+ }
+
+ /**
+ Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
+
+ */
+ override void measure(int parentWidth, int parentHeight) {
+ int h = 0;
+ int w = 0;
+ DrawableRef gaugeDrawable = style.customDrawable("progress_bar_gauge");
+ DrawableRef indeterminateDrawable = style.customDrawable("progress_bar_indeterminate");
+ if (!gaugeDrawable.isNull) {
+ if (h < gaugeDrawable.height)
+ h = gaugeDrawable.height;
+ }
+ if (!indeterminateDrawable.isNull) {
+ if (h < indeterminateDrawable.height)
+ h = indeterminateDrawable.height;
+ }
+ measuredContent(parentWidth, parentHeight, w, h);
+ }
+
+
+ /// Draw widget at its position to buffer
+ override void onDraw(DrawBuf buf) {
+ if (visibility != Visibility.Visible)
+ return;
+ super.onDraw(buf);
+ Rect rc = _pos;
+ applyMargins(rc);
+ applyPadding(rc);
+ DrawableRef animDrawable;
+ if (_progress >= 0) {
+ DrawableRef gaugeDrawable = style.customDrawable("progress_bar_gauge");
+ animDrawable = style.customDrawable("progress_bar_gauge_animation");
+ int x = rc.left + _progress * rc.width / PROGRESS_MAX;
+ if (!gaugeDrawable.isNull) {
+ gaugeDrawable.drawTo(buf, Rect(rc.left, rc.top, x, rc.bottom));
+ } else {
+ }
+ } else {
+ DrawableRef indeterminateDrawable = style.customDrawable("progress_bar_indeterminate");
+ if (!indeterminateDrawable.isNull) {
+ indeterminateDrawable.drawTo(buf, rc);
+ }
+ animDrawable = style.customDrawable("progress_bar_indeterminate_animation");
+ }
+ if (!animDrawable.isNull && _animationInterval) {
+ if (!_animationTimerId)
+ scheduleAnimation();
+ int w = animDrawable.width;
+ _animationPhase %= w * 1000;
+ animDrawable.drawTo(buf, rc, 0, cast(int)(_animationPhase * _animationSpeedPixelsPerSecond / 1000), 0);
+ Log.d("progress animation draw ", _animationPhase, " rc=", rc);
+ }
+ }
+}
diff --git a/src/dlangui/widgets/scrollbar.d b/src/dlangui/widgets/scrollbar.d
new file mode 100644
index 00000000..432e2f29
--- /dev/null
+++ b/src/dlangui/widgets/scrollbar.d
@@ -0,0 +1,995 @@
+// Written in the D programming language.
+
+/**
+This module contains simple scrollbar-like controls implementation.
+
+ScrollBar - scrollbar control
+
+SliderWidget - slider control
+
+
+
+Synopsis:
+
+----
+import dlangui.widgets.scrollbar;
+
+----
+
+Copyright: Vadim Lopatin, 2014
+License: Boost License 1.0
+Authors: Vadim Lopatin, coolreader.org@gmail.com
+*/
+module dlangui.widgets.scrollbar;
+
+
+import dlangui.widgets.widget;
+import dlangui.widgets.layouts;
+import dlangui.widgets.controls;
+import dlangui.core.events;
+import dlangui.core.stdaction;
+
+private import std.algorithm;
+private import std.conv : to;
+private import std.utf : toUTF32;
+
+/// scroll event handler interface
+interface OnScrollHandler {
+ /// handle scroll event
+ bool onScrollEvent(AbstractSlider source, ScrollEvent event);
+}
+
+/// base class for widgets like scrollbars and sliders
+class AbstractSlider : WidgetGroup {
+ protected int _minValue = 0;
+ protected int _maxValue = 100;
+ protected int _pageSize = 30;
+ protected int _position = 20;
+
+ /// create with ID parameter
+ this(string ID) {
+ super(ID);
+ }
+
+ /// scroll event listeners
+ Signal!OnScrollHandler scrollEvent;
+
+ /// returns slider position
+ @property int position() const { return _position; }
+ /// sets new slider position
+ @property AbstractSlider position(int newPosition) {
+ if (_position != newPosition) {
+ _position = newPosition;
+ onPositionChanged();
+ }
+ return this;
+ }
+ protected void onPositionChanged() {
+ requestLayout();
+ }
+ /// returns slider range min value
+ @property int minValue() const { return _minValue; }
+ /// sets slider range min value
+ @property AbstractSlider minValue(int v) { _minValue = v; return this; }
+ /// returns slider range max value
+ @property int maxValue() const { return _maxValue; }
+ /// sets slider range max value
+ @property AbstractSlider maxValue(int v) { _maxValue = v; return this; }
+
+
+
+ /// page size (visible area size)
+ @property int pageSize() const { return _pageSize; }
+ /// set page size (visible area size)
+ @property AbstractSlider pageSize(int size) {
+ if (_pageSize != size) {
+ _pageSize = size;
+ //requestLayout();
+ }
+ return this;
+ }
+
+ /// set int property value, for ML loaders
+ //mixin(generatePropertySettersMethodOverride("setIntProperty", "int",
+ // "minValue", "maxValue", "pageSize", "position"));
+ /// set int property value, for ML loaders
+ override bool setIntProperty(string name, int value) {
+ if (name.equal("orientation")) { // use same value for all sides
+ orientation = cast(Orientation)value;
+ return true;
+ }
+ mixin(generatePropertySetters("minValue", "maxValue", "pageSize", "position"));
+ return super.setIntProperty(name, value);
+ }
+
+ /// set new range (min and max values for slider)
+ AbstractSlider setRange(int min, int max) {
+ if (_minValue != min || _maxValue != max) {
+ _minValue = min;
+ _maxValue = max;
+ //requestLayout();
+ }
+ return this;
+ }
+
+ bool sendScrollEvent(ScrollAction action) {
+ return sendScrollEvent(action, _position);
+ }
+
+ bool sendScrollEvent(ScrollAction action, int position) {
+ if (!scrollEvent.assigned)
+ return false;
+ ScrollEvent event = new ScrollEvent(action, _minValue, _maxValue, _pageSize, position);
+ bool res = scrollEvent(this, event);
+ if (event.positionChanged) {
+ _position = event.position;
+ if (_position > _maxValue)
+ _position = _maxValue;
+ if (_position < _minValue)
+ _position = _minValue;
+ onPositionChanged();
+ }
+ return true;
+ }
+
+ protected Orientation _orientation = Orientation.Vertical;
+ /// returns scrollbar orientation (Vertical, Horizontal)
+ @property Orientation orientation() { return _orientation; }
+ /// sets scrollbar orientation
+ @property AbstractSlider orientation(Orientation value) {
+ if (_orientation != value) {
+ _orientation = value;
+ requestLayout();
+ }
+ return this;
+ }
+
+}
+
+/// scroll bar - either vertical or horizontal
+class ScrollBar : AbstractSlider, OnClickHandler {
+ protected ImageButton _btnBack;
+ protected ImageButton _btnForward;
+ protected SliderButton _indicator;
+ protected PageScrollButton _pageUp;
+ protected PageScrollButton _pageDown;
+ protected Rect _scrollArea;
+ protected int _btnSize;
+ protected int _minIndicatorSize;
+
+
+
+ class PageScrollButton : Widget {
+ this(string ID) {
+ super(ID);
+ styleId = STYLE_PAGE_SCROLL;
+ trackHover = true;
+ clickable = true;
+ }
+ }
+
+ class SliderButton : ImageButton {
+ Point _dragStart;
+ int _dragStartPosition;
+ bool _dragging;
+ Rect _dragStartRect;
+
+ this(string resourceId) {
+ super("SLIDER", resourceId);
+ styleId = STYLE_SCROLLBAR_BUTTON;
+ trackHover = true;
+ }
+
+ /// process mouse event; return true if event is processed by widget.
+ override bool onMouseEvent(MouseEvent event) {
+ // support onClick
+ if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
+ setState(State.Pressed);
+ _dragging = true;
+ _dragStart.x = event.x;
+ _dragStart.y = event.y;
+ _dragStartPosition = _position;
+ _dragStartRect = _pos;
+ sendScrollEvent(ScrollAction.SliderPressed, _position);
+ return true;
+ }
+ if (event.action == MouseAction.FocusOut && _dragging) {
+ debug(scrollbar) Log.d("ScrollBar slider dragging - FocusOut");
+ return true;
+ }
+ if (event.action == MouseAction.FocusIn && _dragging) {
+ debug(scrollbar) Log.d("ScrollBar slider dragging - FocusIn");
+ return true;
+ }
+ if (event.action == MouseAction.Move && _dragging) {
+ int delta = _orientation == Orientation.Vertical ? event.y - _dragStart.y : event.x - _dragStart.x;
+ debug(scrollbar) Log.d("ScrollBar slider dragging - Move delta=", delta);
+ Rect rc = _dragStartRect;
+ int offset;
+ int space;
+ if (_orientation == Orientation.Vertical) {
+ rc.top += delta;
+ rc.bottom += delta;
+ if (rc.top < _scrollArea.top) {
+ rc.top = _scrollArea.top;
+ rc.bottom = _scrollArea.top + _dragStartRect.height;
+ } else if (rc.bottom > _scrollArea.bottom) {
+ rc.top = _scrollArea.bottom - _dragStartRect.height;
+ rc.bottom = _scrollArea.bottom;
+ }
+ offset = rc.top - _scrollArea.top;
+ space = _scrollArea.height - rc.height;
+ } else {
+ rc.left += delta;
+ rc.right += delta;
+ if (rc.left < _scrollArea.left) {
+ rc.left = _scrollArea.left;
+ rc.right = _scrollArea.left + _dragStartRect.width;
+ } else if (rc.right > _scrollArea.right) {
+ rc.left = _scrollArea.right - _dragStartRect.width;
+ rc.right = _scrollArea.right;
+ }
+ offset = rc.left - _scrollArea.left;
+ space = _scrollArea.width - rc.width;
+ }
+ layoutButtons(rc);
+ //_pos = rc;
+ int position = cast(int)(space > 0 ? _minValue + cast(long)offset * (_maxValue - _minValue - _pageSize) / space : 0);
+ invalidate();
+ onIndicatorDragging(_dragStartPosition, position);
+ return true;
+ }
+ if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) {
+ resetState(State.Pressed);
+ if (_dragging) {
+ sendScrollEvent(ScrollAction.SliderReleased, _position);
+ _dragging = false;
+ }
+ return true;
+ }
+ if (event.action == MouseAction.Move && trackHover) {
+ if (!(state & State.Hovered)) {
+ debug(scrollbar) Log.d("Hover ", id);
+ setState(State.Hovered);
+ }
+ return true;
+ }
+ if (event.action == MouseAction.Leave && trackHover) {
+ debug(scrollbar) Log.d("Leave ", id);
+ resetState(State.Hovered);
+ return true;
+ }
+ if (event.action == MouseAction.Cancel && trackHover) {
+ debug(scrollbar) Log.d("Cancel ? trackHover", id);
+ resetState(State.Hovered);
+ resetState(State.Pressed);
+ _dragging = false;
+ return true;
+ }
+ if (event.action == MouseAction.Cancel) {
+ debug(scrollbar) Log.d("SliderButton.onMouseEvent event.action == MouseAction.Cancel");
+ resetState(State.Pressed);
+ _dragging = false;
+ return true;
+ }
+ return false;
+ }
+
+ }
+
+ protected bool onIndicatorDragging(int initialPosition, int currentPosition) {
+ _position = currentPosition;
+ return sendScrollEvent(ScrollAction.SliderMoved, currentPosition);
+ }
+
+ private bool calcButtonSizes(int availableSize, ref int spaceBackSize, ref int spaceForwardSize, ref int indicatorSize) {
+ int dv = _maxValue - _minValue;
+ if (_pageSize >= dv) {
+ // full size
+ spaceBackSize = spaceForwardSize = 0;
+ indicatorSize = availableSize;
+ return false;
+ }
+ if (dv < 0)
+ dv = 0;
+ indicatorSize = dv ? _pageSize * availableSize / dv : _minIndicatorSize;
+ if (indicatorSize < _minIndicatorSize)
+ indicatorSize = _minIndicatorSize;
+ if (indicatorSize >= availableSize) {
+ // full size
+ spaceBackSize = spaceForwardSize = 0;
+ indicatorSize = availableSize;
+ return false;
+ }
+ int spaceLeft = availableSize - indicatorSize;
+ int topv = _position - _minValue;
+ int bottomv = _position + _pageSize - _minValue;
+ if (topv < 0)
+ topv = 0;
+ if (bottomv > dv)
+ bottomv = dv;
+ bottomv = dv - bottomv;
+ spaceBackSize = cast(int)(cast(long)spaceLeft * topv / (topv + bottomv));
+ spaceForwardSize = spaceLeft - spaceBackSize;
+ return true;
+ }
+
+ /// returns scrollbar orientation (Vertical, Horizontal)
+ override @property Orientation orientation() { return _orientation; }
+ /// sets scrollbar orientation
+ override @property AbstractSlider orientation(Orientation value) {
+ if (_orientation != value) {
+ _orientation = value;
+ _btnBack.drawableId = style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_BUTTON_UP : ATTR_SCROLLBAR_BUTTON_LEFT);
+ _btnForward.drawableId = style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_BUTTON_DOWN : ATTR_SCROLLBAR_BUTTON_RIGHT);
+ _indicator.drawableId = style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_INDICATOR_VERTICAL : ATTR_SCROLLBAR_INDICATOR_HORIZONTAL);
+ requestLayout();
+ }
+ return this;
+ }
+
+ /// set string property value, for ML loaders
+ override bool setStringProperty(string name, string value) {
+ if (name.equal("orientation")) {
+ if (value.equal("Vertical") || value.equal("vertical"))
+ orientation = Orientation.Vertical;
+ else
+ orientation = Orientation.Horizontal;
+ return true;
+ }
+ return super.setStringProperty(name, value);
+ }
+
+
+ /// empty parameter list constructor - for usage by factory
+ this() {
+ this(null, Orientation.Vertical);
+ }
+ /// create with ID parameter
+ this(string ID, Orientation orient = Orientation.Vertical) {
+ super(ID);
+ styleId = STYLE_SCROLLBAR;
+ _orientation = orient;
+ _btnBack = new ImageButton("BACK", style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_BUTTON_UP : ATTR_SCROLLBAR_BUTTON_LEFT));
+ _btnForward = new ImageButton("FORWARD", style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_BUTTON_DOWN : ATTR_SCROLLBAR_BUTTON_RIGHT));
+ _pageUp = new PageScrollButton("PAGE_UP");
+ _pageDown = new PageScrollButton("PAGE_DOWN");
+ _btnBack.styleId = STYLE_SCROLLBAR_BUTTON_TRANSPARENT;
+ _btnForward.styleId = STYLE_SCROLLBAR_BUTTON_TRANSPARENT;
+ _indicator = new SliderButton(style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_INDICATOR_VERTICAL : ATTR_SCROLLBAR_INDICATOR_HORIZONTAL));
+ addChild(_btnBack);
+ addChild(_btnForward);
+ addChild(_indicator);
+ addChild(_pageUp);
+ addChild(_pageDown);
+ _btnBack.focusable = false;
+ _btnForward.focusable = false;
+ _indicator.focusable = false;
+ _pageUp.focusable = false;
+ _pageDown.focusable = false;
+ _btnBack.click = &onClick;
+ _btnForward.click = &onClick;
+ _pageUp.click = &onClick;
+ _pageDown.click = &onClick;
+ }
+
+ override void measure(int parentWidth, int parentHeight) {
+ Point sz;
+ _btnBack.measure(parentWidth, parentHeight);
+ _btnForward.measure(parentWidth, parentHeight);
+ _indicator.measure(parentWidth, parentHeight);
+ _pageUp.measure(parentWidth, parentHeight);
+ _pageDown.measure(parentWidth, parentHeight);
+ _btnSize = _btnBack.measuredWidth;
+ _minIndicatorSize = _orientation == Orientation.Vertical ? _indicator.measuredHeight : _indicator.measuredWidth;
+ if (_btnSize < _minIndicatorSize)
+ _btnSize = _minIndicatorSize;
+ if (_btnSize < _btnForward.measuredWidth)
+ _btnSize = _btnForward.measuredWidth;
+ if (_btnSize < _btnForward.measuredHeight)
+ _btnSize = _btnForward.measuredHeight;
+ if (_btnSize < _btnBack.measuredHeight)
+ _btnSize = _btnBack.measuredHeight;
+ static if (BACKEND_GUI) {
+ if (_btnSize < 16)
+ _btnSize = 16;
+ }
+ if (_orientation == Orientation.Vertical) {
+ // vertical
+ sz.x = _btnSize;
+ sz.y = _btnSize * 5; // min height
+ } else {
+ // horizontal
+ sz.y = _btnSize;
+ sz.x = _btnSize * 5; // min height
+ }
+ measuredContent(parentWidth, parentHeight, sz.x, sz.y);
+ }
+
+ override protected void onPositionChanged() {
+ if (!needLayout)
+ layoutButtons();
+ }
+
+ /// hide controls when scroll is not possible
+ protected void updateState() {
+ bool canScroll = _maxValue - _minValue > _pageSize;
+ if (canScroll) {
+ _btnBack.setState(State.Enabled);
+ _btnForward.setState(State.Enabled);
+ _indicator.visibility = Visibility.Visible;
+ _pageUp.visibility = Visibility.Visible;
+ _pageDown.visibility = Visibility.Visible;
+ } else {
+ _btnBack.resetState(State.Enabled);
+ _btnForward.resetState(State.Enabled);
+ _indicator.visibility = Visibility.Gone;
+ _pageUp.visibility = Visibility.Gone;
+ _pageDown.visibility = Visibility.Gone;
+ }
+ cancelLayout();
+ }
+
+ override void cancelLayout() {
+ _btnBack.cancelLayout();
+ _btnForward.cancelLayout();
+ _indicator.cancelLayout();
+ _pageUp.cancelLayout();
+ _pageDown.cancelLayout();
+ super.cancelLayout();
+ }
+
+ protected void layoutButtons() {
+ Rect irc = _scrollArea;
+ if (_orientation == Orientation.Vertical) {
+ // vertical
+ int spaceBackSize, spaceForwardSize, indicatorSize;
+ bool indicatorVisible = calcButtonSizes(_scrollArea.height, spaceBackSize, spaceForwardSize, indicatorSize);
+ irc.top += spaceBackSize;
+ irc.bottom -= spaceForwardSize;
+ layoutButtons(irc);
+ } else {
+ // horizontal
+ int spaceBackSize, spaceForwardSize, indicatorSize;
+ bool indicatorVisible = calcButtonSizes(_scrollArea.width, spaceBackSize, spaceForwardSize, indicatorSize);
+ irc.left += spaceBackSize;
+ irc.right -= spaceForwardSize;
+ layoutButtons(irc);
+ }
+ updateState();
+ cancelLayout();
+ }
+
+ protected void layoutButtons(Rect irc) {
+ Rect r;
+ _indicator.visibility = Visibility.Visible;
+ if (_orientation == Orientation.Vertical) {
+ _indicator.layout(irc);
+ if (_scrollArea.top < irc.top) {
+ r = _scrollArea;
+ r.bottom = irc.top;
+ _pageUp.layout(r);
+ _pageUp.visibility = Visibility.Visible;
+ } else {
+ _pageUp.visibility = Visibility.Invisible;
+ }
+ if (_scrollArea.bottom > irc.bottom) {
+ r = _scrollArea;
+ r.top = irc.bottom;
+ _pageDown.layout(r);
+ _pageDown.visibility = Visibility.Visible;
+ } else {
+ _pageDown.visibility = Visibility.Invisible;
+ }
+ } else {
+ _indicator.layout(irc);
+ if (_scrollArea.left < irc.left) {
+ r = _scrollArea;
+ r.right = irc.left;
+ _pageUp.layout(r);
+ _pageUp.visibility = Visibility.Visible;
+ } else {
+ _pageUp.visibility = Visibility.Invisible;
+ }
+ if (_scrollArea.right > irc.right) {
+ r = _scrollArea;
+ r.left = irc.right;
+ _pageDown.layout(r);
+ _pageDown.visibility = Visibility.Visible;
+ } else {
+ _pageDown.visibility = Visibility.Invisible;
+ }
+ }
+ }
+
+ override void layout(Rect rc) {
+ _needLayout = false;
+ applyMargins(rc);
+ applyPadding(rc);
+ Rect r;
+ if (_orientation == Orientation.Vertical) {
+ // vertical
+ // buttons
+ int backbtnpos = rc.top + _btnSize;
+ int fwdbtnpos = rc.bottom - _btnSize;
+ r = rc;
+ r.bottom = backbtnpos;
+ _btnBack.layout(r);
+ r = rc;
+ r.top = fwdbtnpos;
+ _btnForward.layout(r);
+ // indicator
+ r = rc;
+ r.top = backbtnpos;
+ r.bottom = fwdbtnpos;
+ _scrollArea = r;
+ } else {
+ // horizontal
+ int backbtnpos = rc.left + _btnSize;
+ int fwdbtnpos = rc.right - _btnSize;
+ r = rc;
+ r.right = backbtnpos;
+ _btnBack.layout(r);
+ r = rc;
+ r.left = fwdbtnpos;
+ _btnForward.layout(r);
+ // indicator
+ r = rc;
+ r.left = backbtnpos;
+ r.right = fwdbtnpos;
+ _scrollArea = r;
+ }
+ layoutButtons();
+ _pos = rc;
+ }
+
+ override bool onClick(Widget source) {
+ Log.d("Scrollbar.onClick ", source.id);
+ if (source.compareId("BACK"))
+ return sendScrollEvent(ScrollAction.LineUp, position);
+ if (source.compareId("FORWARD"))
+ return sendScrollEvent(ScrollAction.LineDown, position);
+ if (source.compareId("PAGE_UP"))
+ return sendScrollEvent(ScrollAction.PageUp, position);
+ if (source.compareId("PAGE_DOWN"))
+ return sendScrollEvent(ScrollAction.PageDown, position);
+ return true;
+ }
+
+ /// handle mouse wheel events
+ override bool onMouseEvent(MouseEvent event) {
+ if (visibility != Visibility.Visible)
+ return false;
+ if (event.action == MouseAction.Wheel) {
+ int delta = event.wheelDelta;
+ if (delta > 0)
+ sendScrollEvent(ScrollAction.LineUp, position);
+ else if (delta < 0)
+ sendScrollEvent(ScrollAction.LineDown, position);
+ return true;
+ }
+ return super.onMouseEvent(event);
+ }
+
+ /// Draw widget at its position to buffer
+ override void onDraw(DrawBuf buf) {
+ if (visibility != Visibility.Visible && !buf.isClippedOut(_pos))
+ return;
+ super.onDraw(buf);
+ Rect rc = _pos;
+ applyMargins(rc);
+ applyPadding(rc);
+ auto saver = ClipRectSaver(buf, rc, alpha);
+ _btnForward.onDraw(buf);
+ _btnBack.onDraw(buf);
+ _pageUp.onDraw(buf);
+ _pageDown.onDraw(buf);
+ _indicator.onDraw(buf);
+ }
+}
+
+/// scroll bar - either vertical or horizontal
+class SliderWidget : AbstractSlider, OnClickHandler {
+ protected SliderButton _indicator;
+ protected PageScrollButton _pageUp;
+ protected PageScrollButton _pageDown;
+ protected Rect _scrollArea;
+ protected int _btnSize;
+ protected int _minIndicatorSize;
+
+ class PageScrollButton : Widget {
+ this(string ID) {
+ super(ID);
+ styleId = STYLE_PAGE_SCROLL;
+ trackHover = true;
+ clickable = true;
+ }
+ }
+
+ class SliderButton : ImageButton {
+ Point _dragStart;
+ int _dragStartPosition;
+ bool _dragging;
+ Rect _dragStartRect;
+
+ this(string resourceId) {
+ super("SLIDER", resourceId);
+ styleId = STYLE_SCROLLBAR_BUTTON;
+ trackHover = true;
+ }
+
+ /// process mouse event; return true if event is processed by widget.
+ override bool onMouseEvent(MouseEvent event) {
+ // support onClick
+ if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
+ setState(State.Pressed);
+ _dragging = true;
+ _dragStart.x = event.x;
+ _dragStart.y = event.y;
+ _dragStartPosition = _position;
+ _dragStartRect = _pos;
+ sendScrollEvent(ScrollAction.SliderPressed, _position);
+ return true;
+ }
+ if (event.action == MouseAction.FocusOut && _dragging) {
+ debug(scrollbar) Log.d("ScrollBar slider dragging - FocusOut");
+ return true;
+ }
+ if (event.action == MouseAction.FocusIn && _dragging) {
+ debug(scrollbar) Log.d("ScrollBar slider dragging - FocusIn");
+ return true;
+ }
+ if (event.action == MouseAction.Move && _dragging) {
+ int delta = _orientation == Orientation.Vertical ? event.y - _dragStart.y : event.x - _dragStart.x;
+ debug(scrollbar) Log.d("ScrollBar slider dragging - Move delta=", delta);
+ Rect rc = _dragStartRect;
+ int offset;
+ int space;
+ if (_orientation == Orientation.Vertical) {
+ rc.top += delta;
+ rc.bottom += delta;
+ if (rc.top < _scrollArea.top) {
+ rc.top = _scrollArea.top;
+ rc.bottom = _scrollArea.top + _dragStartRect.height;
+ } else if (rc.bottom > _scrollArea.bottom) {
+ rc.top = _scrollArea.bottom - _dragStartRect.height;
+ rc.bottom = _scrollArea.bottom;
+ }
+ offset = rc.top - _scrollArea.top;
+ space = _scrollArea.height - rc.height;
+ } else {
+ rc.left += delta;
+ rc.right += delta;
+ if (rc.left < _scrollArea.left) {
+ rc.left = _scrollArea.left;
+ rc.right = _scrollArea.left + _dragStartRect.width;
+ } else if (rc.right > _scrollArea.right) {
+ rc.left = _scrollArea.right - _dragStartRect.width;
+ rc.right = _scrollArea.right;
+ }
+ offset = rc.left - _scrollArea.left;
+ space = _scrollArea.width - rc.width;
+ }
+ layoutButtons(rc);
+ //_pos = rc;
+ int position = cast(int)(space > 0 ? _minValue + cast(long)offset * (_maxValue - _minValue - _pageSize) / space : 0);
+ invalidate();
+ onIndicatorDragging(_dragStartPosition, position);
+ return true;
+ }
+ if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) {
+ resetState(State.Pressed);
+ if (_dragging) {
+ sendScrollEvent(ScrollAction.SliderReleased, _position);
+ _dragging = false;
+ }
+ return true;
+ }
+ if (event.action == MouseAction.Move && trackHover) {
+ if (!(state & State.Hovered)) {
+ debug(scrollbar) Log.d("Hover ", id);
+ setState(State.Hovered);
+ }
+ return true;
+ }
+ if (event.action == MouseAction.Leave && trackHover) {
+ debug(scrollbar) Log.d("Leave ", id);
+ resetState(State.Hovered);
+ return true;
+ }
+ if (event.action == MouseAction.Cancel && trackHover) {
+ debug(scrollbar) Log.d("Cancel ? trackHover", id);
+ resetState(State.Hovered);
+ resetState(State.Pressed);
+ _dragging = false;
+ return true;
+ }
+ if (event.action == MouseAction.Cancel) {
+ debug(scrollbar) Log.d("SliderButton.onMouseEvent event.action == MouseAction.Cancel");
+ resetState(State.Pressed);
+ _dragging = false;
+ return true;
+ }
+ return false;
+ }
+
+ }
+
+ protected bool onIndicatorDragging(int initialPosition, int currentPosition) {
+ _position = currentPosition;
+ return sendScrollEvent(ScrollAction.SliderMoved, currentPosition);
+ }
+
+ private bool calcButtonSizes(int availableSize, ref int spaceBackSize, ref int spaceForwardSize, ref int indicatorSize) {
+ int dv = _maxValue - _minValue;
+ if (_pageSize >= dv) {
+ // full size
+ spaceBackSize = spaceForwardSize = 0;
+ indicatorSize = availableSize;
+ return false;
+ }
+ if (dv < 0)
+ dv = 0;
+ indicatorSize = dv ? _pageSize * availableSize / dv : _minIndicatorSize;
+ if (indicatorSize < _minIndicatorSize)
+ indicatorSize = _minIndicatorSize;
+ if (indicatorSize >= availableSize) {
+ // full size
+ spaceBackSize = spaceForwardSize = 0;
+ indicatorSize = availableSize;
+ return false;
+ }
+ int spaceLeft = availableSize - indicatorSize;
+ int topv = _position - _minValue;
+ int bottomv = _position + _pageSize - _minValue;
+ if (topv < 0)
+ topv = 0;
+ if (bottomv > dv)
+ bottomv = dv;
+ bottomv = dv - bottomv;
+ spaceBackSize = cast(int)(cast(long)spaceLeft * topv / (topv + bottomv));
+ spaceForwardSize = spaceLeft - spaceBackSize;
+ return true;
+ }
+
+ /// returns scrollbar orientation (Vertical, Horizontal)
+ override @property Orientation orientation() { return _orientation; }
+ /// sets scrollbar orientation
+ override @property AbstractSlider orientation(Orientation value) {
+ if (_orientation != value) {
+ _orientation = value;
+ _indicator.drawableId = style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_INDICATOR_VERTICAL : ATTR_SCROLLBAR_INDICATOR_HORIZONTAL);
+ requestLayout();
+ }
+ return this;
+ }
+
+ /// set string property value, for ML loaders
+ override bool setStringProperty(string name, string value) {
+ if (name.equal("orientation")) {
+ if (value.equal("Vertical") || value.equal("vertical"))
+ orientation = Orientation.Vertical;
+ else
+ orientation = Orientation.Horizontal;
+ return true;
+ }
+ return super.setStringProperty(name, value);
+ }
+
+
+ /// empty parameter list constructor - for usage by factory
+ this() {
+ this(null, Orientation.Horizontal);
+ }
+ /// create with ID parameter
+ this(string ID, Orientation orient = Orientation.Horizontal) {
+ super(ID);
+ styleId = STYLE_SLIDER;
+ _orientation = orient;
+ _pageSize = 1;
+ _pageUp = new PageScrollButton("PAGE_UP");
+ _pageDown = new PageScrollButton("PAGE_DOWN");
+ _indicator = new SliderButton(style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_INDICATOR_VERTICAL : ATTR_SCROLLBAR_INDICATOR_HORIZONTAL));
+ addChild(_indicator);
+ addChild(_pageUp);
+ addChild(_pageDown);
+ _indicator.focusable = false;
+ _pageUp.focusable = false;
+ _pageDown.focusable = false;
+ _pageUp.click = &onClick;
+ _pageDown.click = &onClick;
+ }
+
+ override void measure(int parentWidth, int parentHeight) {
+ Point sz;
+ _indicator.measure(parentWidth, parentHeight);
+ _pageUp.measure(parentWidth, parentHeight);
+ _pageDown.measure(parentWidth, parentHeight);
+ _minIndicatorSize = _orientation == Orientation.Vertical ? _indicator.measuredHeight : _indicator.measuredWidth;
+ _btnSize = _minIndicatorSize;
+ if (_btnSize < _minIndicatorSize)
+ _btnSize = _minIndicatorSize;
+ static if (BACKEND_GUI) {
+ if (_btnSize < 16)
+ _btnSize = 16;
+ }
+ if (_orientation == Orientation.Vertical) {
+ // vertical
+ sz.x = _btnSize;
+ sz.y = _btnSize * 5; // min height
+ } else {
+ // horizontal
+ sz.y = _btnSize;
+ sz.x = _btnSize * 5; // min height
+ }
+ measuredContent(parentWidth, parentHeight, sz.x, sz.y);
+ }
+
+ override protected void onPositionChanged() {
+ if (!needLayout)
+ layoutButtons();
+ }
+
+ /// hide controls when scroll is not possible
+ protected void updateState() {
+ bool canScroll = _maxValue - _minValue > _pageSize;
+ if (canScroll) {
+ _indicator.visibility = Visibility.Visible;
+ _pageUp.visibility = Visibility.Visible;
+ _pageDown.visibility = Visibility.Visible;
+ } else {
+ _indicator.visibility = Visibility.Gone;
+ _pageUp.visibility = Visibility.Gone;
+ _pageDown.visibility = Visibility.Gone;
+ }
+ cancelLayout();
+ }
+
+ override void cancelLayout() {
+ _indicator.cancelLayout();
+ _pageUp.cancelLayout();
+ _pageDown.cancelLayout();
+ super.cancelLayout();
+ }
+
+ protected void layoutButtons() {
+ Rect irc = _scrollArea;
+ if (_orientation == Orientation.Vertical) {
+ // vertical
+ int spaceBackSize, spaceForwardSize, indicatorSize;
+ bool indicatorVisible = calcButtonSizes(_scrollArea.height, spaceBackSize, spaceForwardSize, indicatorSize);
+ irc.top += spaceBackSize;
+ irc.bottom -= spaceForwardSize;
+ layoutButtons(irc);
+ } else {
+ // horizontal
+ int spaceBackSize, spaceForwardSize, indicatorSize;
+ bool indicatorVisible = calcButtonSizes(_scrollArea.width, spaceBackSize, spaceForwardSize, indicatorSize);
+ irc.left += spaceBackSize;
+ irc.right -= spaceForwardSize;
+ layoutButtons(irc);
+ }
+ updateState();
+ cancelLayout();
+ }
+
+ protected void layoutButtons(Rect irc) {
+ Rect r;
+ _indicator.visibility = Visibility.Visible;
+ if (_orientation == Orientation.Vertical) {
+ _indicator.layout(irc);
+ if (_scrollArea.top < irc.top) {
+ r = _scrollArea;
+ r.bottom = irc.top;
+ _pageUp.layout(r);
+ _pageUp.visibility = Visibility.Visible;
+ } else {
+ _pageUp.visibility = Visibility.Invisible;
+ }
+ if (_scrollArea.bottom > irc.bottom) {
+ r = _scrollArea;
+ r.top = irc.bottom;
+ _pageDown.layout(r);
+ _pageDown.visibility = Visibility.Visible;
+ } else {
+ _pageDown.visibility = Visibility.Invisible;
+ }
+ } else {
+ _indicator.layout(irc);
+ if (_scrollArea.left < irc.left) {
+ r = _scrollArea;
+ r.right = irc.left;
+ _pageUp.layout(r);
+ _pageUp.visibility = Visibility.Visible;
+ } else {
+ _pageUp.visibility = Visibility.Invisible;
+ }
+ if (_scrollArea.right > irc.right) {
+ r = _scrollArea;
+ r.left = irc.right;
+ _pageDown.layout(r);
+ _pageDown.visibility = Visibility.Visible;
+ } else {
+ _pageDown.visibility = Visibility.Invisible;
+ }
+ }
+ }
+
+ override void layout(Rect rc) {
+ _needLayout = false;
+ applyMargins(rc);
+ applyPadding(rc);
+ Rect r;
+ if (_orientation == Orientation.Vertical) {
+ // vertical
+ // buttons
+ // indicator
+ r = rc;
+ _scrollArea = r;
+ } else {
+ // horizontal
+ // indicator
+ r = rc;
+ _scrollArea = r;
+ }
+ layoutButtons();
+ _pos = rc;
+ }
+
+ override bool onClick(Widget source) {
+ Log.d("Scrollbar.onClick ", source.id);
+ if (source.compareId("PAGE_UP"))
+ return sendScrollEvent(ScrollAction.PageUp, position);
+ if (source.compareId("PAGE_DOWN"))
+ return sendScrollEvent(ScrollAction.PageDown, position);
+ return true;
+ }
+
+ /// handle mouse wheel events
+ override bool onMouseEvent(MouseEvent event) {
+ if (visibility != Visibility.Visible)
+ return false;
+ if (event.action == MouseAction.Wheel) {
+ int delta = event.wheelDelta;
+ if (delta > 0)
+ sendScrollEvent(ScrollAction.LineUp, position);
+ else if (delta < 0)
+ sendScrollEvent(ScrollAction.LineDown, position);
+ return true;
+ }
+ return super.onMouseEvent(event);
+ }
+
+ /// Draw widget at its position to buffer
+ override void onDraw(DrawBuf buf) {
+ if (visibility != Visibility.Visible && !buf.isClippedOut(_pos))
+ return;
+ Rect rc = _pos;
+ applyMargins(rc);
+ auto saver = ClipRectSaver(buf, rc, alpha);
+ DrawableRef bg = backgroundDrawable;
+ if (!bg.isNull) {
+ Rect r = rc;
+ if (_orientation == Orientation.Vertical) {
+ int dw = bg.width;
+ r.left += (rc.width - dw)/2;
+ r.right = r.left + dw;
+ } else {
+ int dw = bg.height;
+ r.top += (rc.height - dw)/2;
+ r.bottom = r.top + dw;
+ }
+ bg.drawTo(buf, r, state);
+ }
+ applyPadding(rc);
+ if (state & State.Focused) {
+ rc.expand(FOCUS_RECT_PADDING, FOCUS_RECT_PADDING);
+ drawFocusRect(buf, rc);
+ }
+ _needDraw = false;
+ _pageUp.onDraw(buf);
+ _pageDown.onDraw(buf);
+ _indicator.onDraw(buf);
+ }
+}
+
diff --git a/src/dlangui/widgets/styles.d b/src/dlangui/widgets/styles.d
index 8f76f293..d1a3585d 100644
--- a/src/dlangui/widgets/styles.d
+++ b/src/dlangui/widgets/styles.d
@@ -119,6 +119,8 @@ immutable string STYLE_TRANSPARENT_BUTTON_BACKGROUND = "TRANSPARENT_BUTTON_BACKG
immutable string STYLE_GROUP_BOX = "GROUP_BOX";
/// standard style id for GroupBox caption
immutable string STYLE_GROUP_BOX_CAPTION = "GROUP_BOX_CAPTION";
+/// standard style id for ProgressBarWidget caption
+immutable string STYLE_PROGRESS_BAR = "PROGRESS_BAR";
/// standard style id for tree item
immutable string STYLE_TREE_ITEM = "TREE_ITEM";
diff --git a/src/dlangui/widgets/widget.d b/src/dlangui/widgets/widget.d
index 75ed2ceb..f7741d25 100644
--- a/src/dlangui/widgets/widget.d
+++ b/src/dlangui/widgets/widget.d
@@ -1143,12 +1143,15 @@ public:
/// set new timer to call onTimer() after specified interval (for recurred notifications, return true from onTimer)
ulong setTimer(long intervalMillis) {
- return window.setTimer(this, intervalMillis);
+ if (auto w = window)
+ return w.setTimer(this, intervalMillis);
+ return 0; // no window - no timer
}
/// cancel timer - pass value returned from setTimer() as timerId parameter
void cancelTimer(ulong timerId) {
- window.cancelTimer(timerId);
+ if (auto w = window)
+ w.cancelTimer(timerId);
}
/// handle timer; return true to repeat timer event after next interval, false cancel timer
diff --git a/views/res/progress_bar_gauge_animation.png b/views/res/progress_bar_gauge_animation.png
new file mode 100644
index 00000000..7c333382
Binary files /dev/null and b/views/res/progress_bar_gauge_animation.png differ
diff --git a/views/res/theme_default.xml b/views/res/theme_default.xml
index 68ef8289..2a2d7d0d 100644
--- a/views/res/theme_default.xml
+++ b/views/res/theme_default.xml
@@ -505,5 +505,17 @@
padding="1pt,1pt,1pt,1pt">
+
+
diff --git a/views/standard_resources.list b/views/standard_resources.list
index c9c766a6..9b3d4676 100644
--- a/views/standard_resources.list
+++ b/views/standard_resources.list
@@ -55,6 +55,7 @@ res/group_box_frame_bottom_dark.9.png
res/group_box_frame_up_left_dark.9.png
res/group_box_frame_up_right_dark.9.png
res/slider_background_dark.9.png
+res/progress_bar_gauge_animation.png
res/i18n/std_en.ini
res/i18n/std_ru.ini
res/list_item_background.xml