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