1 /++
2 	Some widgets that are included in the package.
3 +/
4 module qui.widgets;
5 
6 import qui.qui;
7 import qui.utils;
8 import utils.misc;
9 import utils.lists;
10 
11 /// Displays some text
12 ///
13 /// And it can't handle new-line characters
14 ///
15 /// If the text doesn't fit in width, it will move left-right
16 class TextLabelWidget : QWidget{
17 private:
18 	/// number of chars not displayed on left
19 	uinteger xOffset = 0;
20 	/// max xOffset
21 	uinteger maxXOffset;
22 	/// the text to display
23 	dstring _caption;
24 	/// if in the last timerEvent, xOffset was increased
25 	bool increasedXOffset = true;
26 
27 	/// calculates the maxXOffset, and changes xOffset if it's above it
28 	void calculateMaxXOffset(){
29 		if (_caption.length <= size.width){
30 			maxXOffset = 0;
31 			xOffset = 0;
32 		}else{
33 			maxXOffset = _caption.length - _size.width;
34 			if (xOffset > maxXOffset)
35 				xOffset = maxXOffset;
36 		}
37 	}
38 protected:
39 	override void timerEvent(uinteger msecs){
40 		static uinteger accumulatedTime;
41 		if (maxXOffset > 0){
42 			accumulatedTime += msecs;
43 			if (xOffset >= maxXOffset)
44 				increasedXOffset = false;
45 			else if (xOffset == 0)
46 				increasedXOffset = true;
47 			while (accumulatedTime >= scrollTimer){
48 				accumulatedTime -= scrollTimer;
49 				if (increasedXOffset){
50 					if (xOffset < maxXOffset)
51 						xOffset ++;
52 				}else if (xOffset > 0)
53 					xOffset --;
54 				// request update
55 				requestUpdate();
56 			}
57 		}
58 	}
59 	
60 	override void resizeEvent(){
61 		calculateMaxXOffset;
62 		requestUpdate();
63 	}
64 	
65 	override void update(){
66 		_display.write(_caption.scrollHorizontal(xOffset, _size.width), textColor, backgroundColor);
67 	}
68 
69 public:
70 	/// text and background colors
71 	Color textColor, backgroundColor;
72 	/// milliseconds after it scrolls 1 pixel, in case text too long to fit in 1 line
73 	uinteger scrollTimer;
74 	/// constructor
75 	this(dstring newCaption = ""){
76 		this.caption = newCaption;
77 		textColor = DEFAULT_FG;
78 		backgroundColor = DEFAULT_BG;
79 		this._size.maxHeight = 1;
80 		scrollTimer = 500;
81 	}
82 
83 	/// the text to display
84 	@property dstring caption(){
85 		return _caption;
86 	}
87 	/// ditto
88 	@property dstring caption(dstring newCaption){
89 		_caption = newCaption;
90 		calculateMaxXOffset;
91 		// request update
92 		requestUpdate();
93 		return _caption;
94 	}
95 }
96 
97 /// Displays a left-to-right progress bar.
98 /// 
99 /// Can also display text (just like TextLabelWidget)
100 class ProgressbarWidget : TextLabelWidget{
101 private:
102 	uinteger _max, _progress;
103 protected:
104 	override void update(){
105 		// if caption fits in width, center align it
106 		dstring text;
107 		if (_caption.length < _size.width)
108 			text = centerAlignText(_caption, _size.width);
109 		else
110 			text = _caption.scrollHorizontal(xOffset, _size.width);
111 		// number of chars to be colored in barColor
112 		uinteger fillCharCount = (_progress * _size.width) / _max;
113 		// line number on which the caption will be written
114 		for (uinteger i = 0,captionLineNumber = this._size.height / 2; i < this._size.height; i ++){
115 			_display.cursor = Position(0, i);
116 			if (i == captionLineNumber){
117 				_display.write(cast(dchar[])text[0 .. fillCharCount], backgroundColor, barColor);
118 				_display.write(cast(dchar[])text[fillCharCount .. text.length], barColor, backgroundColor);
119 			}else{
120 				if (fillCharCount)
121 					_display.fillLine(' ', backgroundColor, barColor, fillCharCount);
122 				if (fillCharCount < this._size.width)
123 					_display.fillLine(' ', barColor, backgroundColor);
124 			}
125 		}
126 		// write till _progress
127 		_display.write(cast(dchar[])text[0 .. fillCharCount], backgroundColor, barColor);
128 		// write the empty bar
129 		_display.write(cast(dchar[])text[fillCharCount .. text.length], barColor, backgroundColor);
130 	}
131 public:
132 	/// background color, and bar's color
133 	Color backgroundColor, barColor;
134 	/// constructor
135 	this(uinteger max = 100, uinteger progress = 0){
136 		_caption = null;
137 		this.max = max;
138 		this.progress = progress;
139 		// no max height limit on this one
140 		this._size.maxHeight = 0;
141 
142 		barColor = DEFAULT_FG;
143 		backgroundColor = DEFAULT_BG;
144 	}
145 	/// The 'total', or the max-progress. getter
146 	@property uinteger max(){
147 		return _max;
148 	}
149 	/// The 'total' or the max-progress. setter
150 	@property uinteger max(uinteger newMax){
151 		_max = newMax;
152 		requestUpdate();
153 		return _max;
154 	}
155 	/// the amount of progress. getter
156 	@property uinteger progress(){
157 		return _progress;
158 	}
159 	/// the amount of progress. setter
160 	@property uinteger progress(uinteger newProgress){
161 		_progress = newProgress;
162 		requestUpdate();
163 		return _progress;
164 	}
165 }
166 
167 /// To get single-line input from keyboard
168 class EditLineWidget : QWidget{
169 private:
170 	/// text that's been input-ed
171 	dchar[] _text;
172 	/// position of cursor
173 	uinteger _x;
174 	/// how many chars wont be displayed on left
175 	uinteger _scrollX;
176 
177 	/// called to fix _scrollX and _x when input is changed or _x is changed
178 	void reScroll(){
179 		adjustScrollingOffset(_x, _size.width, _text.length, _scrollX);
180 	}
181 protected:
182 	/// override resize to re-scroll
183 	override void resizeEvent(){
184 		requestUpdate();
185 		reScroll;
186 	}
187 	override void mouseEvent(MouseEvent mouse){
188 		if (mouse.button == MouseEvent.Button.Left){
189 			_x = mouse.x + _scrollX;
190 		}
191 		requestUpdate();
192 		reScroll;
193 	}
194 	override void keyboardEvent(KeyboardEvent key){
195 		if (key.isChar){
196 			//insert that key
197 			if (key.key == '\b'){
198 				//backspace
199 				if (_x > 0){
200 					if (_x == _text.length){
201 						_text.length --;
202 					}else{
203 						_text = _text.deleteElement(_x-1);
204 					}
205 					_x --;
206 				}
207 			}else if (key.key != '\n'){
208 				if (_x == _text.length){
209 					//insert at end
210 					_text ~= cast(dchar)key.key;
211 				}else{
212 					_text = _text.insertElement([cast(dchar)key.key], _x);
213 				}
214 				_x ++;
215 			}
216 		}else{
217 			if (key.key == Key.LeftArrow && _x > 0){
218 				_x --;
219 			}else if (key.key == Key.RightArrow && _x < _text.length){
220 				_x ++;
221 			}else if (key.key == Key.Delete && _x < _text.length){
222 				_text = _text.deleteElement(_x);
223 			}
224 		}
225 		requestUpdate();
226 		reScroll;
227 	}
228 	override void update(){
229 		_display.write(cast(dstring)this._text.scrollHorizontal(cast(integer)_scrollX, _size.width),textColor,backgroundColor);
230 	}
231 public:
232 	/// background, text, caption, and caption's background colors
233 	Color backgroundColor, textColor;
234 	/// constructor
235 	this(dstring text = ""){
236 		this._text = cast(dchar[])text.dup;
237 		//specify min/max
238 		_size.minHeight = 1;
239 		_size.maxHeight = 1;
240 		// don't want tab key by default
241 		_wantsTab = false;
242 		// and input too, obvious
243 		_wantsInput = true;
244 
245 		textColor = DEFAULT_FG;
246 		backgroundColor = DEFAULT_BG;
247 	}
248 
249 	///The text that has been input-ed.
250 	@property dstring text(){
251 		return cast(dstring)_text.dup;
252 	}
253 	///The text that has been input-ed.
254 	@property dstring text(dstring newText){
255 		_text = cast(dchar[])newText.dup;
256 		// request update
257 		requestUpdate();
258 		return cast(dstring)newText;
259 	}
260 	// override cursor position
261 	override @property Position cursorPosition(){
262 		return Position(_x - _scrollX, 0);
263 	}
264 }
265 
266 /// Can be used as a simple text editor, or to just display text
267 class MemoWidget : QWidget{
268 private:
269 	List!dstring _lines;
270 	/// how many characters/lines are skipped
271 	uinteger _scrollX, _scrollY;
272 	/// whether the cursor is, relative to line#0 character#0
273 	uinteger _cursorX, _cursorY;
274 	/// whether the text in it will be editable
275 	bool _enableEditing = true;
276 	/// used by widget itself to recalculate scrolling
277 	void reScroll(){
278 		// _scrollY
279 		adjustScrollingOffset(_cursorY, this._size.height, lineCount, _scrollY);
280 		// _scrollX
281 		adjustScrollingOffset(_cursorX, this._size.width, readLine(_cursorY).length, _scrollX);
282 	}
283 	/// used by widget itself to move cursor
284 	void moveCursor(uinteger x, uinteger y){
285 		_cursorX = x;
286 		_cursorY = y;
287 		
288 		if (_cursorY > lineCount)
289 			_cursorY = lineCount-1;
290 		if (_cursorX > readLine(_cursorY).length)
291 			_cursorX = readLine(_cursorY).length;
292 	}
293 	/// Reads a line from widgetLines
294 	dstring readLine(uinteger index){
295 		if (index >= _lines.length)
296 			return "";
297 		return _lines.read(index);
298 	}
299 	/// overwrites a line
300 	void overwriteLine(uinteger index, dstring line){
301 		if (index == _lines.length)
302 			_lines.append(line);
303 		else
304 			_lines.set(index,line);
305 		
306 	}
307 	/// deletes a line
308 	void removeLine(uinteger index){
309 		if (index < _lines.length)
310 			_lines.remove(index);
311 	}
312 	/// inserts a line
313 	void insertLine(uinteger index, dstring line){
314 		if (index == _lines.length)
315 			_lines.append(line);
316 		else
317 			_lines.insert(index, line);
318 	}
319 	/// adds a line
320 	void addLine(dstring line){
321 		_lines.append(line);
322 	}
323 	/// returns lines count
324 	@property uinteger lineCount(){
325 		return _lines.length+1;
326 	}
327 protected:
328 	override void update(){
329 		const uinteger count = lineCount;
330 		if (count > 0){
331 			//write lines to memo
332 			for (uinteger i = _scrollY; i < count && _display.cursor.y < _size.height; i++){
333 				_display.write(readLine(i).scrollHorizontal(_scrollX, this._size.width), 
334 					textColor, backgroundColor);
335 			}
336 		}
337 		_display.fill(' ', textColor, backgroundColor);
338 	}
339 
340 	override void resizeEvent(){
341 		requestUpdate();
342 		reScroll();
343 	}
344 	
345 	override void mouseEvent(MouseEvent mouse){
346 		//calculate mouse position, relative to scroll
347 		mouse.x = mouse.x + cast(int)_scrollX;
348 		mouse.y = mouse.y + cast(int)_scrollY;
349 		if (mouse.button == mouse.Button.Left){
350 			requestUpdate();
351 			moveCursor(mouse.x, mouse.y);
352 		}else if (mouse.button == mouse.Button.ScrollDown){
353 			if (_cursorY+1 < lineCount){
354 				requestUpdate();
355 				moveCursor(_cursorX, _cursorY + 4);
356 				reScroll();
357 			}
358 		}else if (mouse.button == mouse.Button.ScrollUp){
359 			if (_cursorY > 0){
360 				requestUpdate();
361 				if (_cursorY < 4){
362 					moveCursor(_cursorX, 0);
363 				}else{
364 					moveCursor(_cursorX, _cursorY - 4);
365 				}
366 				reScroll();
367 			}
368 		}
369 	}
370 	// too big of a mess to be dealt with right now, TODO try to make this shorter
371 	override void keyboardEvent(KeyboardEvent key){
372 		if (key.isChar){
373 			if (_enableEditing){
374 				requestUpdate();
375 				dstring currentLine = readLine(_cursorY);
376 				//check if backspace
377 				if (key.key == '\b'){
378 					//make sure that it's not the first line, first line cannot be removed
379 					if (_cursorY > 0){
380 						//check if has to remove a '\n'
381 						if (_cursorX == 0){
382 							_cursorY --;
383 							//if line's not empty, append it to previous line
384 							_cursorX = readLine(_cursorY).length;
385 							if (currentLine != ""){
386 								//else, append this line to previous
387 								overwriteLine(_cursorY, readLine(_cursorY)~currentLine);
388 							}
389 							removeLine(_cursorY+1);
390 						}else{
391 							overwriteLine(_cursorY, cast(dstring)deleteElement(cast(dchar[])currentLine,_cursorX-1));
392 							_cursorX --;
393 						}
394 					}else if (_cursorX > 0){
395 						overwriteLine(_cursorY, cast(dstring)deleteElement(cast(dchar[])currentLine,_cursorX-1));
396 						_cursorX --;
397 					}
398 					
399 				}else if (key.key == '\n'){
400 					//insert a newline
401 					if (_cursorX == readLine(_cursorY).length){
402 						if (_cursorY >= lineCount - 1){
403 							_lines.append("");
404 						}else{
405 							insertLine(_cursorY + 1,"");
406 						}
407 					}else{
408 						dstring[2] line;
409 						line[0] = readLine(_cursorY);
410 						line[1] = line[0][_cursorX .. line[0].length];
411 						line[0] = line[0][0 .. _cursorX];
412 						overwriteLine(_cursorY, line[0]);
413 						if (_cursorY >= lineCount - 1){
414 							_lines.append(line[1]);
415 						}else{
416 							insertLine(_cursorY + 1, line[1]);
417 						}
418 					}
419 					_cursorY ++;
420 					_cursorX = 0;
421 				}else if (key.key == '\t'){
422 					//convert it to 4 spaces
423 					overwriteLine(_cursorY, cast(dstring)insertElement(cast(dchar[])currentLine,cast(dchar[])"    ",_cursorX));
424 					_cursorX += 4;
425 				}else{
426 					//insert that char
427 					overwriteLine(_cursorY, cast(dstring)insertElement(cast(dchar[])currentLine,[cast(dchar)key.key],_cursorX));
428 					_cursorX ++;
429 				}
430 			}
431 		}else{
432 			if (key.key == Key.Delete && _enableEditing){
433 				requestUpdate();
434 				//check if is deleting \n
435 				if (_cursorX == readLine(_cursorY).length && _cursorY+1 < lineCount){
436 					//merge next line with this one
437 					dchar[] line = cast(dchar[])readLine(_cursorY)~readLine(_cursorY+1);
438 					overwriteLine(_cursorY, cast(dstring)line);
439 					//remove next line
440 					removeLine(_cursorY+1);
441 				}else if (_cursorX < readLine(_cursorY).length){
442 					dchar[] line = cast(dchar[])readLine(_cursorY);
443 					line = line.deleteElement(_cursorX);
444 					overwriteLine(_cursorY, cast(dstring)line);
445 				}
446 			}else if (key.key == Key.DownArrow){
447 				if (_cursorY+1 < lineCount){
448 					requestUpdate();
449 					_cursorY ++;
450 				}
451 			}else if (key.key == Key.UpArrow){
452 				if (_cursorY > 0){
453 					requestUpdate();
454 					_cursorY --;
455 				}
456 			}else if (key.key == Key.LeftArrow){
457 				if ((_cursorY >= 0 && _cursorX > 0) || (_cursorY > 0 && _cursorX == 0)){
458 					requestUpdate();
459 					if (_cursorX == 0){
460 						_cursorY --;
461 						_cursorX = readLine(_cursorY).length;
462 					}else{
463 						_cursorX --;
464 					}
465 				}
466 			}else if (key.key == Key.RightArrow){
467 				requestUpdate();
468 				if (_cursorX == readLine(_cursorY).length){
469 					if (_cursorY+1 < lineCount){
470 						_cursorX = 0;
471 						_cursorY ++;
472 						_scrollX = 0;
473 					}
474 				}else{
475 					_cursorX ++;
476 				}
477 			}
478 		}
479 		// I'll use this this time not to move the cursor, but to fix the cursor position
480 		moveCursor(_cursorX,_cursorY);
481 		reScroll();
482 	}
483 public:
484 	/// background and text colors
485 	Color backgroundColor, textColor;
486 	/// constructor
487 	this(bool allowEditing = true){
488 		_lines = new List!dstring;
489 		_scrollX = 0;
490 		_scrollY = 0;
491 		_cursorX = 0;
492 		_cursorY = 0;
493 		this.editable = allowEditing;
494 
495 		textColor = DEFAULT_FG;
496 		backgroundColor = DEFAULT_BG;
497 	}
498 	~this(){
499 		.destroy(_lines);
500 	}
501 	
502 	///returns a list of lines in memo
503 	///
504 	///To modify the content, just modify it in the returned list
505 	///
506 	///class `List` is defined in `utils.lists.d`
507 	@property List!dstring lines(){
508 		return _lines;
509 	}
510 	///Returns true if memo's contents cannot be modified, by user
511 	@property bool editable(){
512 		return _enableEditing;
513 	}
514 	///sets whether to allow modifying of contents (false) or not (true)
515 	@property bool editable(bool newPermission){
516 		_wantsTab = newPermission;
517 		_wantsInput = newPermission;
518 		return _enableEditing = newPermission;
519 	}
520 	/// override cursor position
521 	override @property Position cursorPosition(){
522 		return	_enableEditing ? Position(_cursorX - _scrollX, _cursorY - _scrollY) : Position(-1,-1);
523 	}
524 }
525 
526 /// Displays an un-scrollable log
527 /// 
528 /// It's content cannot be modified by user, like a MemoWidget with editing disabled, but automatically scrolls down as new lines 
529 /// are added, and it wraps long lines.
530 class LogWidget : QWidget{
531 private:
532 	/// stores the logs
533 	List!dstring _logs;
534 	/// The index in _logs where the oldest added line is
535 	uinteger _startIndex;
536 	/// the maximum number of lines to store
537 	uinteger _maxLines;
538 	/// Returns: line at an index
539 	dstring getLine(uinteger index){
540 		if (index >= _logs.length)
541 			return "";
542 		return _logs.read((index + _startIndex) % _maxLines);
543 	}
544 	/// wrap a line
545 	dstring[] wrapLine(dstring str){
546 		dstring[] r;
547 		str = str.dup;
548 		while (str.length > 0){
549 			r ~= _size.width > str.length ? str : str[0 .. _size.width];
550 			str = _size.width < str.length ? str[_size.width .. $] : [];
551 		}
552 		return r;
553 	}
554 protected:
555 	override void update(){
556 		_display.colors(textColor, backgroundColor);
557 		integer lastY = _size.height;
558 		for (integer i = _logs.length-1; i >= 0; i --){
559 			dstring line = getLine(i);
560 			dstring[] wrappedLine = wrapLine(line);
561 			if (wrappedLine.length == 0)
562 				continue;
563 			if (lastY < wrappedLine.length)
564 				wrappedLine = wrappedLine[wrappedLine.length - lastY .. $];
565 			immutable integer startY = lastY - wrappedLine.length;
566 			foreach (lineno, currentLine; wrappedLine){
567 				_display.cursor = Position(0, lineno + startY);
568 				_display.write(currentLine);
569 				if (currentLine.length < _size.width)
570 					_display.fillLine(' ', textColor, backgroundColor);
571 			}
572 			lastY = startY;
573 		}
574 		_display.cursor = Position(0, 0);
575 		foreach (y; 0 .. lastY)
576 			_display.fillLine(' ', textColor, backgroundColor);
577 	}
578 	
579 	override void resizeEvent() {
580 		requestUpdate();
581 	}
582 public:
583 	/// background and text color
584 	Color backgroundColor, textColor;
585 	/// constructor
586 	this(uinteger maxLen=100){
587 		_maxLines = maxLen;
588 		_logs = new List!dstring;
589 		_startIndex = 0;
590 
591 		textColor = DEFAULT_FG;
592 		backgroundColor = DEFAULT_BG;
593 	}
594 	~this(){
595 		_logs.destroy;
596 	}
597 	
598 	///adds string to the log, and scrolls down to it.
599 	/// newline character is not allowed
600 	void add(dstring item){
601 		//check if needs to overwrite
602 		if (_logs.length > _maxLines){
603 			_startIndex = (_startIndex + 1) % _maxLines;
604 			_logs.set(_startIndex, item);
605 		}else
606 			_logs.append(item);
607 		requestUpdate();
608 	}
609 	///clears the log
610 	void clear(){
611 		_logs.clear;
612 		requestUpdate();
613 	}
614 }
615 
616 /// Just occupies some space. Use this to put space between widgets
617 /// 
618 /// To specify the size, use the minHeight, maxHeight, minWidth, and maxWidth. only specifying the width and/or height will have no effect
619 class SplitterWidget : QWidget{
620 protected:
621 	override void resizeEvent() {
622 		requestUpdate();
623 	}
624 	
625 	override void update(){
626 		_display.fill(' ',DEFAULT_FG, color);
627 	}
628 public:
629 	/// color of this widget
630 	Color color;
631 	/// constructor
632 	this(){
633 		this.color = DEFAULT_BG;
634 	}
635 }