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 }