1 /** 2 Copyright: Copyright (c) 2014 Andrey Penechko. 3 License: a$(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 7 8 module anchovy.gui.behaviors.editbehavior; 9 10 import std.algorithm; 11 import std.stdio; 12 import std.utf : count, toUTFindex; 13 14 import anchovy.gui; 15 import anchovy.gui.interfaces.iwidgetbehavior; 16 import anchovy.gui.behaviors.labelbehavior; 17 18 class EditBehavior : LabelBehavior 19 { 20 override void attachTo(Widget widget) 21 { 22 super.attachTo(widget); 23 _widget = widget; 24 25 widget.addEventHandler(&keyPressed); 26 widget.addEventHandler(&keyReleased); 27 widget.addEventHandler(&charEntered); 28 widget.addEventHandler(&focusGained); 29 widget.addEventHandler(&focusLost); 30 widget.addEventHandler(&pointerPressed); 31 widget.addEventHandler(&pointerReleased); 32 widget.addEventHandler(&pointerMoved); 33 34 _context = widget.getPropertyAs!("context", GuiContext); 35 36 _textLine = widget.getPropertyAs!("line", TextLine); 37 widget["style"] = "edit"; 38 39 widget.property("size").valueChanged.connect((FlexibleObject obj, Variant value){calcTextXPos();}); 40 41 widget.setProperty!"isFocusable"(true); 42 _contentOffset = RectOffset(2); 43 44 _isEditable = true; 45 46 } 47 48 override bool handleDraw(Widget widget, DrawEvent event) 49 { 50 if (event.sinking) 51 { 52 ivec2 staticPos = widget.getPropertyAs!("staticPosition", ivec2); 53 Rect staticRect = widget.getPropertyAs!("staticRect", Rect); 54 55 event.guiRenderer.drawControlBack(widget, staticRect); 56 assert(_textLine); 57 58 event.guiRenderer.pushClientArea(staticRect); 59 event.guiRenderer.renderer.setColor(Color(0,0,0)); 60 event.guiRenderer.drawTextLine(_textLine, ivec2(staticPos.x + _textPos.x + _contentOffset.left, staticPos.y), AlignmentType.LEFT_TOP); 61 62 if (_isFocused && _isCursorVisible && _isCursorBlinkVisible) 63 { 64 event.guiRenderer.renderer.fillRect(Rect(staticPos.x + _cursorRenderPos + _textPos.x + _contentOffset.left, 65 staticPos.y + staticRect.size.y/2 - _textLine.height/2, 66 1, _textLine.height)); 67 } 68 if (_hasSelectedText) 69 { 70 event.guiRenderer.renderer.setColor(Color(0,0,255, 64)); 71 size_t selectionStartX = calcCharOffset(cast(uint)_selectionStart); 72 event.guiRenderer.renderer.fillRect(Rect(staticPos.x + _textPos.x + _contentOffset.left + cast(uint)selectionStartX, 73 staticPos.y + staticRect.size.y/2 - _textLine.height/2, 74 calcCharOffset(cast(uint)_selectionEnd) - cast(uint)selectionStartX, _textLine.height)); 75 } 76 event.guiRenderer.popClientArea; 77 } 78 79 return true; 80 } 81 82 83 bool keyPressed(Widget widget, KeyPressEvent event) 84 { 85 if (!_isEditable) return true; 86 87 bool doTextUpdate = true; 88 bool doDeselect = true; 89 90 if (event.modifiers & KeyModifiers.CONTROL) 91 { 92 if (event.keyCode == KeyCode.KEY_C) 93 { 94 event.context.clipboardString = to!string(copy()); 95 doDeselect = false; 96 } 97 else if (event.keyCode == KeyCode.KEY_V) 98 { 99 paste(to!dstring(event.context.clipboardString)); 100 } 101 else if (event.keyCode == KeyCode.KEY_X) 102 { 103 event.context.clipboardString = to!string(copy()); 104 removeSelectedText(); 105 } 106 else 107 { 108 doTextUpdate = false; 109 } 110 } 111 else if (event.keyCode == KeyCode.KEY_BACKSPACE) 112 { 113 if (_hasSelectedText) 114 { 115 removeSelectedText(); 116 } 117 else if (_textLine.text.length > 0 && _cursorPos > 0) 118 { 119 _cursorRenderPos -= _textLine.font.getGlyph(_textLine.text[_cursorPos-1]).metrics.advanceX; 120 _textLine.text = _textLine.text[0.._cursorPos-1] ~ _textLine.text[_cursorPos..$]; 121 --_cursorPos; 122 onCursorMove(); 123 } 124 } 125 else if (event.keyCode == KeyCode.KEY_LEFT) 126 { 127 moveCursorLeft(); 128 } 129 else if (event.keyCode == KeyCode.KEY_RIGHT) 130 { 131 moveCursorRight(); 132 } 133 else if (event.keyCode == KeyCode.KEY_DELETE) 134 { 135 if (_hasSelectedText) 136 { 137 removeSelectedText(); 138 } 139 else if (_cursorPos < _textLine.text.length) 140 { 141 _textLine.text = _textLine.text[0.._cursorPos]~_textLine.text[_cursorPos+1..$]; 142 onCursorMove(); 143 } 144 145 } 146 else if (event.keyCode == KeyCode.KEY_HOME) 147 { 148 setCursorPos(0); 149 } 150 else if (event.keyCode == KeyCode.KEY_END) 151 { 152 setCursorPos(_textLine.text.length); 153 } 154 else if (event.keyCode == KeyCode.KEY_ENTER) 155 { 156 widget.setProperty!"text"(_textLine.text); 157 } 158 else 159 { 160 doTextUpdate = false; 161 } 162 163 if (doTextUpdate) 164 { 165 calcTextXPos(); 166 if (doDeselect) 167 { 168 deselect(); 169 } 170 } 171 172 return true; 173 } 174 175 bool keyReleased(Widget widget, KeyReleaseEvent) 176 { 177 return true; 178 } 179 180 bool charEntered(Widget widget, CharEnterEvent event) 181 { 182 if (_isEditable) 183 { 184 normalizeSelection(); 185 _textLine.text = _textLine.text[0.._selectionStart] ~ event.character ~ _textLine.text[_selectionEnd..$]; 186 setCursorPos(_selectionStart+1); 187 deselect(); 188 calcTextXPos(); 189 } 190 191 return true; 192 } 193 194 bool pointerPressed(Widget widget, PointerPressEvent event) 195 { 196 if (event.button == PointerButton.PB_LEFT) 197 { 198 moveCursorToClickPos(event.pointerPosition); 199 200 _selectionStart = _cursorPos; 201 _selectionEnd = _cursorPos; 202 return true; 203 } 204 205 return true; 206 } 207 208 bool pointerReleased(Widget widget, PointerReleaseEvent event) 209 { 210 return true; 211 } 212 213 bool pointerMoved(Widget widget, PointerMoveEvent event) 214 { 215 if (event.context.eventDispatcher.pressedWidget is widget ) 216 { 217 moveCursorToClickPos(event.pointerPosition); 218 _selectionEnd = _cursorPos; 219 updateSelection(); 220 } 221 222 return true; 223 } 224 225 bool focusGained(Widget widget, FocusGainEvent event) 226 out 227 { 228 assert(_blinkTimer); 229 } 230 body 231 { 232 assert(_blinkTimer is null); 233 assert(_isCursorBlinkVisible); 234 235 widget.setProperty!"state"("focused"); 236 _isFocused = true; 237 238 _blinkTimer = event.context.timerManager.addTimer(_blinkInterval, &onCursorBlink, double.nan, TimerTickType.PROCESS_LAST); 239 240 return true; 241 } 242 243 bool focusLost(Widget widget, FocusLoseEvent event) 244 { 245 widget.setProperty!"state"("normal"); 246 _isFocused = false; 247 248 event.context.timerManager.stopTimer(_blinkTimer); 249 _blinkTimer = null; 250 251 _isCursorBlinkVisible = true; 252 253 widget.setProperty!"text"(_textLine.text); 254 255 return true; 256 } 257 258 259 /// Set current cursor blink interval in seconds. 260 /// newInterval must be greater than zero. 261 void blinkInterval(double newInterval) @property 262 in 263 { 264 assert(newInterval > 0); 265 } 266 body 267 { 268 _blinkInterval = newInterval; 269 if (_blinkTimer) _blinkTimer.delay = newInterval; 270 } 271 272 /// Get current cursor blink interval in seconds. 273 double blinkInterval() @property 274 { 275 return _blinkInterval; 276 } 277 278 void paste(dstring text) 279 { 280 removeSelectedText(); 281 _textLine.text = _textLine.text[0.._cursorPos] ~ text ~ _textLine.text[_cursorPos..$]; 282 setCursorPos(_cursorPos + text.length); 283 } 284 285 dstring copy() 286 { 287 return selectedText(); 288 } 289 290 /// Used as a callback to blink timer. 291 protected double onCursorBlink(double timesUpdated) 292 { 293 if ((timesUpdated % 2) > 0) 294 _isCursorBlinkVisible = !_isCursorBlinkVisible; 295 296 return 0; 297 } 298 299 dstring text() @property 300 { 301 if (_textLine is null) return ""; 302 return _textLine.text; 303 } 304 305 dstring text(dstring newText) @property 306 { 307 if (_textLine is null) return ""; 308 _textLine.text = newText; 309 _widget.setProperty!"text"(_textLine.text); 310 311 setCursorPos(0); 312 deselect(); 313 314 return _textLine.text; 315 } 316 317 dstring selectedText() @property 318 { 319 if (_selectionStart > _selectionEnd) 320 { 321 return _textLine.text[_selectionEnd.._selectionStart]; 322 } 323 else 324 { 325 return _textLine.text[_selectionStart.._selectionEnd]; 326 } 327 } 328 329 void isEditable(bool editable) @property 330 { 331 _isEditable = editable; 332 } 333 334 bool isEditable() @property 335 { 336 return _isEditable = true; 337 } 338 339 void removeSelectedText() 340 { 341 normalizeSelection(); 342 _textLine.text = _textLine.text[0.._selectionStart] ~ _textLine.text[_selectionEnd..$]; 343 setCursorPos(_selectionStart); 344 deselect(); 345 } 346 347 void deselect() 348 { 349 _selectionStart = _cursorPos; 350 _selectionEnd = _cursorPos; 351 _hasSelectedText = false; 352 } 353 354 void select(size_t start, size_t end) 355 { 356 _selectionStart = start; 357 _selectionEnd = end; 358 359 normalizeSelection(); 360 updateSelection(); 361 } 362 363 protected: 364 365 /// Swaps _selectionStart and _selectionEnd if _selectionStart > _selectionEnd. 366 /// Should be used before text editing. 367 void normalizeSelection() 368 { 369 if (_selectionStart > _selectionEnd) 370 { 371 size_t temp = _selectionEnd; 372 _selectionEnd = _selectionStart; 373 _selectionStart = temp; 374 } 375 } 376 377 void updateSelection() 378 { 379 _selectionStart = clamp!size_t(_selectionStart, 0, _textLine.text.length); 380 _selectionEnd = clamp!size_t(_selectionEnd, 0, _textLine.text.length); 381 382 if (_selectionEnd - _selectionStart > 0) 383 _hasSelectedText = true; 384 else 385 _hasSelectedText = false; 386 } 387 388 void moveCursorToClickPos(ivec2 pointerPosition) 389 in 390 { 391 assert(_textLine); 392 } 393 body 394 { 395 if (_textLine.text.length > 0) 396 { 397 ivec2 staticPos = _widget.getPropertyAs!("staticPosition", ivec2); 398 int clickX = pointerPosition.x - (staticPos.x + _contentOffset.left + _textPos.x); 399 400 Font textFont = _textLine.font; 401 int charCenter; 402 int charX = 0; 403 uint charIndex = 0; 404 405 while (true) 406 { 407 charCenter = charX + (textFont.getGlyph(_textLine.text[charIndex]).metrics.advanceX/2); 408 if (charCenter > clickX) break; 409 410 charX += textFont.getGlyph(_textLine.text[charIndex]).metrics.advanceX; 411 ++charIndex; 412 413 if (charIndex == _textLine.text.length) break; 414 } 415 416 if (_cursorPos != charIndex) 417 { 418 _cursorPos = charIndex; 419 _cursorRenderPos = charX; 420 onCursorMove(); 421 } 422 } 423 } 424 425 /// If cursor changes its position the blinking delay must be reset. 426 void onCursorMove() 427 { 428 if (_blinkTimer) 429 { 430 _context.timerManager.resetTimer(_blinkTimer); 431 _isCursorBlinkVisible = true; 432 } 433 434 calcTextXPos(); 435 } 436 437 void moveCursorRight() 438 { 439 if (_cursorPos < _textLine.text.length) 440 { 441 _cursorRenderPos += _textLine.font.getGlyph(_textLine.text[_cursorPos]).metrics.advanceX; 442 ++_cursorPos; 443 onCursorMove(); 444 } 445 } 446 447 void moveCursorLeft() 448 { 449 if (_cursorPos > 0) 450 { 451 _cursorRenderPos -= _textLine.font.getGlyph(_textLine.text[_cursorPos-1]).metrics.advanceX; 452 --_cursorPos; 453 onCursorMove(); 454 } 455 } 456 457 void setCursorPos(size_t position) 458 { 459 scope(exit) onCursorMove(); 460 461 if (position > _textLine.text.length) 462 { 463 _cursorPos = cast(uint)_textLine.text.length; 464 _cursorRenderPos = _textLine.width; 465 return; 466 } 467 else if (position < 0) 468 { 469 _cursorPos = 0; 470 _cursorRenderPos = 0; 471 return; 472 } 473 474 Font textFont = _textLine.font; 475 int charX = 0; 476 uint charIndex = 0; 477 478 while (true) 479 { 480 if (charIndex == position) break; 481 482 Glyph* glyph = textFont.getGlyph(_textLine.text[charIndex]); 483 assert(glyph !is null); 484 charX += glyph.metrics.advanceX; 485 ++charIndex; 486 487 if (charIndex == _textLine.text.length) break; 488 } 489 490 _cursorPos = charIndex; 491 _cursorRenderPos = charX; 492 } 493 494 /// Returns offset in pixels from the begining of text 495 uint calcCharOffset(uint index) 496 { 497 if (index > _textLine.text.length) 498 return _textLine.width; 499 500 int charX = 0; 501 uint charIndex = 0; 502 503 while (true) 504 { 505 if (charIndex == index) break; 506 507 charX += _textLine.font.getGlyph(_textLine.text[charIndex]).metrics.advanceX; 508 ++charIndex; 509 } 510 511 return charX; 512 } 513 514 void calcTextXPos() 515 { 516 int contentWidth = _widget.getPropertyAs!("size", ivec2).x - _contentOffset.horizontal; 517 518 if (_textLine.width < contentWidth) 519 { 520 _textPos.x = 0; 521 } 522 else 523 { 524 if (_cursorRenderPos + _textPos.x > contentWidth || _cursorPos == _textLine.text.length) 525 { 526 _textPos.x = contentWidth - _cursorRenderPos; 527 } 528 else if (_cursorRenderPos + _textPos.x < 0) 529 { 530 _textPos.x = -_cursorRenderPos; 531 } 532 else if (_textPos.x + _textLine.width < contentWidth) 533 { 534 _textPos.x = contentWidth - _textLine.width; 535 } 536 } 537 538 //writeln("contentWidth ", contentWidth, " _cursorRenderPos ", _cursorRenderPos, " x ", _textPos.x); 539 } 540 541 protected: 542 TextLine _textLine; 543 GuiContext _context; 544 545 private: 546 547 RectOffset _contentOffset; 548 bool _isEditable = true; 549 bool _isCursorVisible = true; 550 bool _hasSelectedText = false; 551 552 /// When blinking is true and _isCursorVisible is true, then cursor will be visible. 553 bool _isCursorBlinkVisible = true; 554 bool _isFocused = false; 555 556 /// if there is no current selection _selectionStart and _selectionEnd are equal to _cursorPos. 557 size_t _selectionStart, _selectionEnd; 558 559 int _cursorPos = 0; 560 int _cursorRenderPos = 0; 561 ivec2 _textPos; 562 563 double _blinkInterval = 0.25f; 564 565 /// Used for cursor blinking. 566 Timer _blinkTimer; 567 568 Widget _widget; 569 }