1 /** @file abstractlineeditor.cpp Abstract line editor.
2 *
3 * @authors Copyright (c) 2013-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4 *
5 * @par License
6 * LGPL: http://www.gnu.org/licenses/lgpl.html
7 *
8 * <small>This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU Lesser General Public License as published by
10 * the Free Software Foundation; either version 3 of the License, or (at your
11 * option) any later version. This program is distributed in the hope that it
12 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
14 * General Public License for more details. You should have received a copy of
15 * the GNU Lesser General Public License along with this program; if not, see:
16 * http://www.gnu.org/licenses</small>
17 */
18
19 #include "de/shell/AbstractLineEditor"
20 #include "de/shell/Lexicon"
21 #include <de/ConstantRule>
22
23 #include <QList>
24 #include <QStringList>
25 #include <QScopedPointer>
26
27 namespace de { namespace shell {
28
DENG2_PIMPL(AbstractLineEditor)29 DENG2_PIMPL(AbstractLineEditor)
30 {
31 String prompt;
32 String text;
33 int cursor; ///< Index in range [0...text.size()]
34 Lexicon lexicon;
35 EchoMode echoMode;
36
37 QScopedPointer<ILineWrapping> wraps;
38
39 struct Completion {
40 int pos;
41 int size;
42 int ordinal; ///< Ordinal within list of possible completions.
43
44 void reset() {
45 pos = size = ordinal = 0;
46 }
47 Rangei range() const {
48 return Rangei(pos, pos + size);
49 }
50 };
51 Completion completion;
52 QStringList suggestions;
53 bool suggesting;
54 bool completionNotified;
55
56 Impl(Public *i, ILineWrapping *lineWraps)
57 : Base(i),
58 cursor(0),
59 echoMode(NormalEchoMode),
60 wraps(lineWraps),
61 suggesting(false),
62 completionNotified(false)
63 {
64 // Initialize line wrapping.
65 completion.reset();
66 }
67
68 WrappedLine lineSpan(int line) const
69 {
70 DENG2_ASSERT(line < wraps->height());
71 return wraps->line(line);
72 }
73
74 void rewrapLater()
75 {
76 wraps->clear();
77 self().contentChanged();
78 }
79
80 void rewrapNow()
81 {
82 updateWraps();
83 self().contentChanged();
84 }
85
86 /**
87 * Determines where word wrapping needs to occur and updates the height of
88 * the widget to accommodate all the needed lines.
89 */
90 void updateWraps()
91 {
92 wraps->wrapTextToWidth(text, de::max(1, self().maximumWidth()));
93
94 if (wraps->height() > 0)
95 {
96 self().numberOfLinesChanged(wraps->height());
97 }
98 else
99 {
100 self().numberOfLinesChanged(1);
101 }
102 }
103
104 de::Vector2i lineCursorPos() const
105 {
106 return linePos(cursor);
107 }
108
109 de::Vector2i linePos(int mark) const
110 {
111 de::Vector2i pos(mark);
112 for (pos.y = 0; pos.y < wraps->height(); ++pos.y)
113 {
114 WrappedLine span = lineSpan(pos.y);
115 if (!span.isFinal) span.range.end--;
116 if (mark >= span.range.start && mark <= span.range.end)
117 {
118 // Stop here. Mark is on this line.
119 break;
120 }
121 pos.x -= span.range.end - span.range.start + 1;
122 }
123 return pos;
124 }
125
126 /**
127 * Attemps to move the cursor up or down by a line.
128 *
129 * @return @c true, if cursor was moved. @c false, if there were no more
130 * lines available in that direction.
131 */
132 bool moveCursorByLine(int lineOff)
133 {
134 acceptCompletion();
135
136 DENG2_ASSERT(lineOff == 1 || lineOff == -1);
137
138 Vector2i const linePos = lineCursorPos();
139 int const destWidth = wraps->rangeWidth(Rangei(lineSpan(linePos.y).range.start, cursor));
140
141 // Check for no room.
142 if (!linePos.y && lineOff < 0) return false;
143 if (linePos.y == wraps->height() - 1 && lineOff > 0) return false;
144
145 // Move cursor onto the adjacent line.
146 WrappedLine span = lineSpan(linePos.y + lineOff);
147 cursor = wraps->indexAtWidth(span.range, destWidth);
148 if (!span.isFinal) span.range.end--;
149 if (cursor > span.range.end) cursor = span.range.end;
150
151 self().cursorMoved();
152 return true;
153 }
154
155 void insert(String const &str)
156 {
157 acceptCompletion();
158 text.insert(cursor, str);
159 cursor += str.size();
160 rewrapNow();
161 }
162
163 void doBackspace()
164 {
165 if (rejectCompletion())
166 return;
167
168 if (!text.isEmpty() && cursor > 0)
169 {
170 text.remove(--cursor, 1);
171 rewrapNow();
172 }
173 }
174
175 void doWordBackspace()
176 {
177 rejectCompletion();
178
179 if (!text.isEmpty() && cursor > 0)
180 {
181 int to = wordJumpLeft(cursor);
182 text.remove(to, cursor - to);
183 cursor = to;
184 rewrapNow();
185 }
186 }
187
188 void doDelete()
189 {
190 if (text.size() > cursor)
191 {
192 text.remove(cursor, 1);
193 rewrapNow();
194 }
195 }
196
197 bool doLeft()
198 {
199 acceptCompletion();
200
201 if (cursor > 0)
202 {
203 --cursor;
204 self().cursorMoved();
205 return true;
206 }
207 return false;
208 }
209
210 bool doRight()
211 {
212 acceptCompletion();
213
214 if (cursor < text.size())
215 {
216 ++cursor;
217 self().cursorMoved();
218 return true;
219 }
220 return false;
221 }
222
223 int wordJumpLeft(int pos) const
224 {
225 pos = de::min(pos, text.size() - 1);
226
227 // First jump over any non-word chars.
228 while (pos > 0 && !text[pos].isLetterOrNumber()) pos--;
229
230 // At least move one character.
231 if (pos > 0) pos--;
232
233 // We're inside a word, jump to its beginning.
234 while (pos > 0 && text[pos - 1].isLetterOrNumber()) pos--;
235
236 return pos;
237 }
238
239 void doWordLeft()
240 {
241 acceptCompletion();
242 cursor = wordJumpLeft(cursor);
243 self().cursorMoved();
244 }
245
246 void doWordRight()
247 {
248 int const last = text.size() - 1;
249
250 acceptCompletion();
251
252 // If inside a word, jump to its end.
253 while (cursor <= last && text[de::min(last, cursor)].isLetterOrNumber())
254 {
255 cursor++;
256 }
257
258 // Jump over any non-word chars.
259 while (cursor <= last && !text[de::min(last, cursor)].isLetterOrNumber())
260 {
261 cursor++;
262 }
263
264 self().cursorMoved();
265 }
266
267 void doHome()
268 {
269 acceptCompletion();
270
271 cursor = lineSpan(lineCursorPos().y).range.start;
272 self().cursorMoved();
273 }
274
275 void doEnd()
276 {
277 acceptCompletion();
278
279 WrappedLine const span = lineSpan(lineCursorPos().y);
280 cursor = span.range.end - (span.isFinal? 0 : 1);
281 self().cursorMoved();
282 }
283
284 void killEndOfLine()
285 {
286 text.remove(cursor, lineSpan(lineCursorPos().y).range.end - cursor);
287 rewrapNow();
288 }
289
290 bool suggestingCompletion() const
291 {
292 return suggesting;
293 //return completion.size > 0;
294 }
295
296 String wordBehindPos(int pos) const
297 {
298 String word;
299 int i = pos - 1;
300 while (i >= 0 && lexicon.isWordChar(text[i])) word.prepend(text[i--]);
301 return word;
302 }
303
304 String wordBehindCursor() const
305 {
306 return wordBehindPos(cursor);
307 }
308
309 QStringList completionsForBase(String base, String &commonPrefix) const
310 {
311 String::CaseSensitivity const sensitivity =
312 lexicon.isCaseSensitive()? String::CaseSensitive : String::CaseInsensitive;
313
314 bool first = true;
315 QStringList sugs;
316
317 foreach (String term, lexicon.terms())
318 {
319 if (term.beginsWith(base, sensitivity) && term.size() > base.size())
320 {
321 sugs << term;
322
323 // Determine if all the suggestions have a common prefix.
324 if (first)
325 {
326 commonPrefix = term;
327 first = false;
328 }
329 else if (!commonPrefix.isEmpty())
330 {
331 int len = commonPrefix.commonPrefixLength(term, sensitivity);
332 commonPrefix = commonPrefix.left(len);
333 }
334 }
335 }
336 qSort(sugs);
337 return sugs;
338 }
339
340 bool doCompletion(bool forwardCycle)
341 {
342 if (!suggestingCompletion())
343 {
344 completionNotified = false;
345 String const base = wordBehindCursor();
346 if (!base.isEmpty())
347 {
348 // Find all the possible completions and apply the first one.
349 String commonPrefix;
350 suggestions = completionsForBase(base, commonPrefix);
351 if (!commonPrefix.isEmpty() && commonPrefix != base)
352 {
353 // Insert the common prefix.
354 completion.ordinal = -1;
355 commonPrefix.remove(0, base.size());
356 completion.pos = cursor;
357 completion.size = commonPrefix.size();
358 text.insert(cursor, commonPrefix);
359 cursor += completion.size;
360 rewrapNow();
361 suggesting = true;
362 return true;
363 }
364 if (!suggestions.isEmpty())
365 {
366 completion.ordinal = -1; //(forwardCycle? 0 : suggestions.size() - 1);
367 /*String comp = suggestions[completion.ordinal];
368 comp.remove(0, base.size());*/
369 completion.pos = cursor;
370 completion.size = 0; //comp.size();
371 //text.insert(cursor, comp);
372 //cursor += completion.size;
373 //rewrapNow();
374 suggesting = true;
375 // Notify immediately.
376 self().autoCompletionBegan(base);
377 completionNotified = true;
378 return true;
379 }
380 }
381 }
382 else
383 {
384 if (!completionNotified)
385 {
386 // Time to notify now.
387 self().autoCompletionBegan(wordBehindPos(completion.pos));
388 completionNotified = true;
389 return true;
390 }
391
392 // Replace the current completion with another suggestion.
393 cursor = completion.pos;
394 String const base = wordBehindCursor();
395
396 if (completion.ordinal < 0)
397 {
398 // This occurs after a common prefix is inserted rather than
399 // a full suggestion.
400 completion.ordinal = (forwardCycle? 0 : suggestions.size() - 1);
401
402 if (base + text.mid(completion.pos, completion.size) == suggestions[completion.ordinal])
403 {
404 // We already had this one, skip it.
405 cycleCompletion(forwardCycle);
406 }
407 }
408 else
409 {
410 cycleCompletion(forwardCycle);
411 }
412
413 String comp = suggestions[completion.ordinal];
414 comp.remove(0, base.size());
415
416 text.remove(completion.pos, completion.size);
417 text.insert(completion.pos, comp);
418 completion.size = comp.size();
419 cursor = completion.pos + completion.size;
420 rewrapNow();
421
422 return true;
423 }
424 return false;
425 }
426
427 void cycleCompletion(bool forwardCycle)
428 {
429 completion.ordinal = de::wrap(completion.ordinal + (forwardCycle? 1 : -1),
430 0, suggestions.size());
431 }
432
433 void resetCompletion()
434 {
435 completion.reset();
436 suggestions.clear();
437 suggesting = false;
438 completionNotified = false;
439 }
440
441 void acceptCompletion()
442 {
443 if (!suggestingCompletion()) return;
444
445 resetCompletion();
446
447 self().autoCompletionEnded(true);
448 }
449
450 bool rejectCompletion()
451 {
452 if (!suggestingCompletion()) return false;
453
454 int oldCursor = cursor;
455
456 text.remove(completion.pos, completion.size);
457 cursor = completion.pos;
458 resetCompletion();
459 rewrapNow();
460
461 self().autoCompletionEnded(false);
462
463 return cursor != oldCursor; // cursor was moved as part of the rejection
464 }
465 };
466
AbstractLineEditor(ILineWrapping * lineWraps)467 AbstractLineEditor::AbstractLineEditor(ILineWrapping *lineWraps) : d(new Impl(this, lineWraps))
468 {}
469
lineWraps() const470 ILineWrapping const &AbstractLineEditor::lineWraps() const
471 {
472 return *d->wraps;
473 }
474
setPrompt(String const & promptText)475 void AbstractLineEditor::setPrompt(String const &promptText)
476 {
477 d->prompt = promptText;
478 d->rewrapLater();
479 }
480
prompt() const481 String AbstractLineEditor::prompt() const
482 {
483 return d->prompt;
484 }
485
setText(String const & contents)486 void AbstractLineEditor::setText(String const &contents)
487 {
488 d->completion.reset();
489 d->text = contents;
490 d->cursor = contents.size();
491 d->rewrapLater();
492 }
493
text() const494 String AbstractLineEditor::text() const
495 {
496 return d->text;
497 }
498
setCursor(int index)499 void AbstractLineEditor::setCursor(int index)
500 {
501 d->completion.reset();
502 d->cursor = index;
503 cursorMoved();
504 }
505
cursor() const506 int AbstractLineEditor::cursor() const
507 {
508 return d->cursor;
509 }
510
linePos(int index) const511 Vector2i AbstractLineEditor::linePos(int index) const
512 {
513 return d->linePos(index);
514 }
515
isSuggestingCompletion() const516 bool AbstractLineEditor::isSuggestingCompletion() const
517 {
518 return d->suggestingCompletion();
519 }
520
completionRange() const521 Rangei AbstractLineEditor::completionRange() const
522 {
523 return d->completion.range();
524 }
525
suggestedCompletions() const526 QStringList AbstractLineEditor::suggestedCompletions() const
527 {
528 if (!isSuggestingCompletion()) return QStringList();
529
530 return d->suggestions;
531 }
532
acceptCompletion()533 void shell::AbstractLineEditor::acceptCompletion()
534 {
535 d->acceptCompletion();
536 }
537
setLexicon(Lexicon const & lexicon)538 void AbstractLineEditor::setLexicon(Lexicon const &lexicon)
539 {
540 d->lexicon = lexicon;
541 }
542
lexicon() const543 Lexicon const &AbstractLineEditor::lexicon() const
544 {
545 return d->lexicon;
546 }
547
setEchoMode(EchoMode mode)548 void AbstractLineEditor::setEchoMode(EchoMode mode)
549 {
550 d->echoMode = mode;
551 }
552
echoMode() const553 AbstractLineEditor::EchoMode AbstractLineEditor::echoMode() const
554 {
555 return d->echoMode;
556 }
557
handleControlKey(int qtKey,KeyModifiers const & mods)558 bool AbstractLineEditor::handleControlKey(int qtKey, KeyModifiers const &mods)
559 {
560 #ifdef MACOSX
561 # define WORD_JUMP_MODIFIER Alt
562 #else
563 # define WORD_JUMP_MODIFIER Control
564 #endif
565
566 switch (qtKey)
567 {
568 case Qt::Key_Backspace:
569 if (mods.testFlag(WORD_JUMP_MODIFIER))
570 {
571 d->doWordBackspace();
572 }
573 else
574 {
575 d->doBackspace();
576 }
577 return true;
578
579 case Qt::Key_Delete:
580 d->doDelete();
581 return true;
582
583 case Qt::Key_Left:
584 #ifdef MACOSX
585 if (mods.testFlag(Control))
586 {
587 d->doHome();
588 return true;
589 }
590 #endif
591 if (mods.testFlag(WORD_JUMP_MODIFIER))
592 {
593 d->doWordLeft();
594 }
595 else
596 {
597 return d->doLeft();
598 }
599 return true;
600
601 case Qt::Key_Right:
602 #ifdef MACOSX
603 if (mods.testFlag(Control))
604 {
605 d->doEnd();
606 return true;
607 }
608 #endif
609 if (mods.testFlag(WORD_JUMP_MODIFIER))
610 {
611 d->doWordRight();
612 }
613 else
614 {
615 return d->doRight();
616 }
617 return true;
618
619 case Qt::Key_Home:
620 d->doHome();
621 return true;
622
623 case Qt::Key_End:
624 d->doEnd();
625 return true;
626
627 case Qt::Key_Tab:
628 case Qt::Key_Backtab:
629 if (d->doCompletion(qtKey == Qt::Key_Tab))
630 {
631 return true;
632 }
633 break;
634
635 case Qt::Key_K:
636 if (mods.testFlag(Control))
637 {
638 d->killEndOfLine();
639 return true;
640 }
641 break;
642
643 case Qt::Key_Up:
644 // First try moving within the current command.
645 if (!d->moveCursorByLine(-1)) return false; // not eaten
646 return true;
647
648 case Qt::Key_Down:
649 // First try moving within the current command.
650 if (!d->moveCursorByLine(+1)) return false; // not eaten
651 return true;
652
653 case Qt::Key_Enter:
654 case Qt::Key_Return:
655 d->acceptCompletion();
656 return true;
657
658 default:
659 break;
660 }
661
662 return false;
663 }
664
insert(String const & text)665 void AbstractLineEditor::insert(String const &text)
666 {
667 return d->insert(text);
668 }
669
lineWraps()670 ILineWrapping &AbstractLineEditor::lineWraps()
671 {
672 return *d->wraps;
673 }
674
autoCompletionBegan(String const &)675 void AbstractLineEditor::autoCompletionBegan(String const &)
676 {}
677
autoCompletionEnded(bool)678 void AbstractLineEditor::autoCompletionEnded(bool /*accepted*/)
679 {}
680
updateLineWraps(LineWrapUpdateBehavior behavior)681 void AbstractLineEditor::updateLineWraps(LineWrapUpdateBehavior behavior)
682 {
683 if (behavior == WrapUnlessWrappedAlready && !d->wraps->isEmpty())
684 return; // Already wrapped.
685
686 d->updateWraps();
687 }
688
689 }} // namespace de::shell
690