From 7c435d772a64c707e759e0980413e37a65e46a46 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Tue, 26 Jan 2016 15:17:11 +0300 Subject: [PATCH] editor: text hover timeout (tooltip) support --- src/dlangui/core/events.d | 3 ++ src/dlangui/core/types.d | 6 ++++ src/dlangui/graphics/fonts.d | 2 +- src/dlangui/widgets/editors.d | 64 +++++++++++++++++++++++++++++++++-- src/dlangui/widgets/popup.d | 26 ++++++++++++++ src/dlangui/widgets/scroll.d | 7 ++++ 6 files changed, 104 insertions(+), 4 deletions(-) diff --git a/src/dlangui/core/events.d b/src/dlangui/core/events.d index 8635110c..c9422e22 100644 --- a/src/dlangui/core/events.d +++ b/src/dlangui/core/events.d @@ -627,6 +627,9 @@ class MouseEvent { /// y coordinate of mouse pointer (relative to window client area) @property short y() { return _y; } + /// returns point for mouse cursor position + @property Point pos() { return Point(_x, _y); } + /// Returns true for ButtonDown event when button is pressed second time in short interval after pressing first time @property bool doubleClick() { if (_action != MouseAction.ButtonDown) diff --git a/src/dlangui/core/types.d b/src/dlangui/core/types.d index 697b8cb6..d50e0757 100644 --- a/src/dlangui/core/types.d +++ b/src/dlangui/core/types.d @@ -73,6 +73,12 @@ struct Rect { @property int middley() { return (top + bottom) / 2; } /// returns middle point @property Point middle() { return Point(middlex, middley); } + + /// returns top left point of rectangle + @property Point topLeft() { return Point(left, top); } + /// returns bottom right point of rectangle + @property Point bottomRight() { return Point(right, bottom); } + /// add offset to horizontal and vertical coordinates void offset(int dx, int dy) { left += dx; diff --git a/src/dlangui/graphics/fonts.d b/src/dlangui/graphics/fonts.d index 658e989f..c8db5fa2 100644 --- a/src/dlangui/graphics/fonts.d +++ b/src/dlangui/graphics/fonts.d @@ -520,7 +520,7 @@ struct SimpleTextFormatter { lineEndX = widths[lastWordEnd - 1]; } // add line - dstring line = cast(dstring)text[lineStart .. lastWordEnd]; + dstring line = cast(dstring)text[lineStart .. lineEnd]; //lastWordEnd]; int lineWidth = lineEndX - lineStartX; sz.y += lineHeight; if (sz.x < lineWidth) diff --git a/src/dlangui/widgets/editors.d b/src/dlangui/widgets/editors.d index cdccd61e..9704e2e8 100644 --- a/src/dlangui/widgets/editors.d +++ b/src/dlangui/widgets/editors.d @@ -862,6 +862,7 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction protected ulong _caretTimerId; protected bool _caretBlinkingPhase; protected long _lastBlinkStartTs; + protected void startCaretBlinking() { if (window) { long ts = currentTimeMillis; @@ -876,6 +877,7 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction invalidate(); } } + protected void stopCaretBlinking() { if (window) { if (_caretTimerId) { @@ -884,6 +886,7 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction } } } + /// handle timer; return true to repeat timer event after next interval, false cancel timer override bool onTimer(ulong id) { if (id == _caretTimerId) { @@ -893,18 +896,25 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction invalidate(); return focused; } + if (id == _hoverTimer) { + cancelHoverTimer(); + onHoverTimeout(_hoverMousePosition, _hoverTextPosition); + return false; + } return super.onTimer(id); } + /// override to handle focus changes override protected void handleFocusChange(bool focused) { if (focused) startCaretBlinking(); - else + else { stopCaretBlinking(); + cancelHoverTimer(); + } super.handleFocusChange(focused); } - /// returns cursor rectangle protected Rect caretRect() { Rect caretRc = textPosToClient(_caretPos); @@ -1546,6 +1556,7 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction /// handle keys override bool onKeyEvent(KeyEvent event) { if (focused) startCaretBlinking(); + cancelHoverTimer(); bool ctrlOrAltPressed = false; //(event.flags & (KeyFlag.Control /* | KeyFlag.Alt */)); if (event.action == KeyAction.Text && event.text.length && !ctrlOrAltPressed) { Log.d("text entered: ", event.text); @@ -1568,20 +1579,53 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction /// Handle Ctrl + Left mouse click on text protected void onControlClick() { + // override to do something useful on Ctrl + Left mouse click in text + } + + protected TextPosition _hoverTextPosition; + protected Point _hoverMousePosition; + protected ulong _hoverTimer; + protected long _hoverTimeoutMillis = 800; + + /// override to handle mouse hover timeout in text + protected void onHoverTimeout(Point pt, TextPosition pos) { + // override to do something useful on hover timeout + } + + protected void onHover(Point pos) { + if (_hoverMousePosition == pos) + return; + Log.d("onHover ", pos); + int x = pos.x - left - _leftPaneWidth; + int y = pos.y - top; + _hoverMousePosition = pos; + _hoverTextPosition = clientToTextPos(Point(x, y)); + cancelHoverTimer(); + _hoverTimer = setTimer(_hoverTimeoutMillis); + } + + protected void cancelHoverTimer() { + if (_hoverTimer) { + cancelTimer(_hoverTimer); + _hoverTimer = 0; + } } /// process mouse event; return true if event is processed by widget. override bool onMouseEvent(MouseEvent event) { //Log.d("onMouseEvent ", id, " ", event.action, " (", event.x, ",", event.y, ")"); // support onClick - if (event.action == MouseAction.ButtonDown && event.x < _clientRect.left && event.x >= _clientRect.left - _leftPaneWidth) { + bool insideLeftPane = event.x < _clientRect.left && event.x >= _clientRect.left - _leftPaneWidth; + if (event.action == MouseAction.ButtonDown && insideLeftPane) { setFocus(); + cancelHoverTimer(); if (onLeftPaneMouseClick(event)) return true; } if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) { setFocus(); startCaretBlinking(); + cancelHoverTimer(); if (event.doubleClick) { selectWordByMouse(event.x - _clientRect.left, event.y - _clientRect.top); } else { @@ -1596,16 +1640,29 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, true); return true; } + if (event.action == MouseAction.Move && event.flags == 0) { + // hover + if (focused && !insideLeftPane) { + onHover(event.pos); + } else { + cancelHoverTimer(); + } + return true; + } if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) { + cancelHoverTimer(); return true; } if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) { + cancelHoverTimer(); return true; } if (event.action == MouseAction.FocusIn) { + cancelHoverTimer(); return true; } if (event.action == MouseAction.Wheel) { + cancelHoverTimer(); uint keyFlags = event.flags & (MouseFlag.Shift | MouseFlag.Control | MouseFlag.Alt); if (event.wheelDelta < 0) { if (keyFlags == MouseFlag.Shift) @@ -1621,6 +1678,7 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction return handleAction(new Action(EditorActions.ScrollLineUp)); } } + cancelHoverTimer(); return super.onMouseEvent(event); } diff --git a/src/dlangui/widgets/popup.d b/src/dlangui/widgets/popup.d index 5e3f56bd..8f71c7b6 100644 --- a/src/dlangui/widgets/popup.d +++ b/src/dlangui/widgets/popup.d @@ -31,6 +31,8 @@ enum PopupAlign : uint { Center = 1, /// place popup below anchor widget close to lower bound Below = 2, + /// place popup below anchor widget close to lower bound + Above = 16, /// place popup below anchor widget close to right bound (when no space enough, align near left bound) Right = 4, /// align to specified point @@ -52,6 +54,8 @@ enum PopupFlags : uint { CloseOnClickOutside = 1, /// modal popup - keypresses and mouse events can be routed to this popup only Modal = 2, + /// close popup when mouse is moved outside this popup + CloseOnMouseMoveOutside = 4, } /** interface - slot for onPopupCloseListener */ @@ -124,6 +128,15 @@ class PopupWidget : LinearLayout { if (anchor.alignment & PopupAlign.Point) { r.left = anchor.x; r.top = anchor.y; + if (anchor.alignment & PopupAlign.Center) { + // center around center of anchor widget + r.left -= w / 2; + r.top -= h / 2; + } else if (anchor.alignment & PopupAlign.Below) { + } else if (anchor.alignment & PopupAlign.Above) { + r.top -= h; + } else if (anchor.alignment & PopupAlign.Right) { + } } else { if (anchor.alignment & PopupAlign.Center) { // center around center of anchor widget @@ -132,6 +145,9 @@ class PopupWidget : LinearLayout { } else if (anchor.alignment & PopupAlign.Below) { r.left = anchorrc.left; r.top = anchorrc.bottom; + } else if (anchor.alignment & PopupAlign.Above) { + r.left = anchorrc.left; + r.top = anchorrc.top - h; } else if (anchor.alignment & PopupAlign.Right) { r.left = anchorrc.right; r.top = anchorrc.top; @@ -164,6 +180,16 @@ class PopupWidget : LinearLayout { return false; } } + if (_flags & PopupFlags.CloseOnMouseMoveOutside) { + if (event.action == MouseAction.Move || event.action == MouseAction.Wheel) { + int threshold = 3; + if (event.x < _pos.left - threshold || event.x > _pos.right + threshold || event.y < _pos.top - threshold || event.y > _pos.bottom + threshold) { + Log.d("Closing popup due to PopupFlags.CloseOnMouseMoveOutside flag"); + close(); + return false; + } + } + } return false; } } diff --git a/src/dlangui/widgets/scroll.d b/src/dlangui/widgets/scroll.d index be3dbede..a055689f 100644 --- a/src/dlangui/widgets/scroll.d +++ b/src/dlangui/widgets/scroll.d @@ -121,6 +121,13 @@ class ScrollWidgetBase : WidgetGroup, OnScrollHandler { } } + /// vertical scrollbar mode + @property ScrollBarMode vscrollbarMode() { return _vscrollbarMode; } + @property void vscrollbarMode(ScrollBarMode m) { _vscrollbarMode = m; } + /// horizontal scrollbar mode + @property ScrollBarMode hscrollbarMode() { return _hscrollbarMode; } + @property void hscrollbarMode(ScrollBarMode m) { _hscrollbarMode = m; } + /// returns client area rectangle @property Rect clientRect() { return _clientRect; }