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 }