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