MauiKit TextEditor

TextEditor.qml
1import QtQuick
2import QtQml
3import QtQuick.Controls
4import QtQuick.Layouts
5
6import org.mauikit.controls as Maui
7import org.mauikit.texteditor as TE
8
9import org.kde.sonnet as Sonnet
10
11/**
12 * @since org.mauikit.texteditor 1.0
13 * @brief Integrated text editor component
14 *
15 * A text area for editing text with convinient functions.
16 * The Editor is controlled by the DocumentHandler which controls the files I/O, the syntax highlighting styles, and many more text editing properties.
17 *
18 * @section features Features
19 *
20 * The TextEditor control comes with a set of built-in features, such as find & replace, syntax highlighting support, line number sidebar, I/O capabilities, file document alerts, and syntax corrector.
21 *
22 * @subsection io I/O
23 * Opening a local text file is handle by the DocumentHandler via the `fileUrl` property. The document contents will be loaded by the FileLoader and made available to the TextEditor for drawing.
24 *
25 * @see DocumentHandler::fileUrl
26 *
27 * @warning Opening large contents will cause the app to freeze, since it is not optimized to dynamically allocate the contents by chunks and instead all of the content will be rendered at once. A solution with a different backend is being implemented.
28 *
29 * Once an existing document is opened or created, it will also be watched for any external changes, such as modifications to its contents or its removal, those changes will be notified via the alert bars, exposing the avaliable options.
30 * @see DocumentHandler::autoReload
31 *
32 * @image html alert_bars.png
33 *
34 * To save any changes made to an existing document or to save a new one manually use the exposed method DocumentHandler::saveAs, which will take as parameter the location where to save the file at, if you mean to save the changes to an already existing file, simply pass the DocumentHandler::fileUrl value.
35 * The changes made could be automatically saved every few seconds if the DocumentHandler::autoSave property is enabled.
36 *
37 * @code
38 * ToolButton
39 * {
40 * icon.name: "folder-open"
41 * onClicked: _editor.fileUrl = "file:///home/camiloh/nota/CMakeLists.txt"
42 * }
43 *
44 * ...
45 *
46 * TE.TextEditor
47 * {
48 * id: _editor
49 * anchors.fill: parent
50 * body.wrapMode: Text.NoWrap
51 * document.enableSyntaxHighlighting: true
52 * }
53 * @endcode
54 *
55 * @subsection syntax_highlighting Syntax Highlighting
56 *
57 * To enable the syntax highlighting enable the DocumentHandler::enableSyntaxHighlighting property.
58 *
59 * @note If the language is not detected automatically or if you desire to change it, use the `showSyntaxHighlightingLanguages` property to toggle the selection combobox to allow the user to select a custom language, and bind it to the DocumentHandler::formatName property.
60 *
61 * There are different color schemes available, those can be set using the DocumentHandler::theme property. You can also use the ColorSchemesPage control which lists all the available options.
62 *
63 * @subsection other Others
64 *
65 * The find & replace bars can be toggled using the `showFindBar` property.
66 * @see DocumentHandler::findWholeWords
67 * @see DocumentHandler::findCaseSensitively
68 *
69 * To enable the line number sidebar use the `showLineNumbers` property.
70 *
71 * Spell checking can be anbled using the `spellcheckEnabled` property. For this Sonnet must be available.
72 *
73 * @subsection fonts Fonts & Colors
74 *
75 * For tweaking the font properties and colors use the DocumentHandler::textColor and DocumentHandler::backgroundColor, etc.
76 *
77 * For more details and properties check the own DocumentHandler properties.
78 */
79Page
80{
81 id: control
82
83 padding: 0
84 focus: false
85 clip: false
86 title: document.fileName + (document.modified ? "*" : "")
87
88 /**
89 * @brief
90 */
91 property bool showFindBar: false
92
93 onShowFindBarChanged:
94 {
95 if(showFindBar)
96 {
97 _findField.forceActiveFocus()
98 }else
99 {
100 body.forceActiveFocus()
101 }
103
104 onWidthChanged: body.update()
105
106 onHeightChanged: body.update()
107
108 /**
109 * @brief Access to the editor text area.
110 * @property TextArea TextEditor::body
111 */
112 readonly property alias body : body
113
114 /**
115 * @brief Alias to access the DocumentHandler
116 * @property DocumentHandler TextEditor::document
117 */
118 readonly property alias document : document
120 /**
121 * @brief Alias to the ScrollView
122 * @property ScrollView TextEditor::scrollView
123 */
124 readonly property alias scrollView: _scrollView
125
126 /**
127 * @brief Alias to the contextual menu. This menu is loaded asynchronous.
128 * @property Menu TextEditor::documentMenu
129 */
130 readonly property alias documentMenu : _documentMenuLoader.item
131
132 /**
133 * @brief Alias to the text area text content
134 * @property string TextEditor::text
135 */
136 property alias text: body.text
137
138 /**
139 * @see DocumentHandler::uppercase
140 * @property bool TextEditor::uppercase
141 */
142 property alias uppercase: document.uppercase
143
144 /**
145 * @see DocumentHandler::underline
146 * @property bool TextEditor::underline
147 */
148 property alias underline: document.underline
149
150 /**
151 * @see DocumentHandler::italic
152 * @property bool TextEditor::italic
153 */
154 property alias italic: document.italic
156 /**
157 * @see DocumentHandler::bold
158 * @property bool TextEditor::bold
159 */
160 property alias bold: document.bold
161
162 /**
163 * @brief Whether there are modifications to the document that can be redo. Alias to the TextArea::canRedo
164 * @property bool TextEditor::canRedo
165 */
166 property alias canRedo: body.canRedo
167
168 /**
169 * @brief If a file url is provided the DocumentHandler will try to open its contents and display it
170 * @see DocumentHandler::fileUrl
171 * @property url TextEditor::fileUrl
172 */
173 property alias fileUrl : document.fileUrl
174
175 /**
176 * @brief If a sidebar listing each line number should be visible.
177 * By default this is set to `false`
178 */
179 property bool showLineNumbers : false
181 /**
182 * @brief Whether to enable the spell checker.
183 * By default this is set to `false`
184 */
185 property bool spellcheckEnabled: false
186
187 FontMetrics
188 {
189 id: fontMetrics
190 font: body.font
191 }
192
193 TE.DocumentHandler
194 {
195 id: document
196 document: body.textDocument
197 cursorPosition: body.cursorPosition
198 selectionStart: body.selectionStart
199 selectionEnd: body.selectionEnd
200 backgroundColor: control.Maui.Theme.backgroundColor
201 enableSyntaxHighlighting: false
202 findCaseSensitively: _findCaseSensitively.checked
203 findWholeWords: _findWholeWords.checked
204
205 onSearchFound: (start, end) =>
206 {
207 body.select(start, end)
208 }
209 }
210
211 Loader
212 {
213 id: spellcheckhighlighterLoader
214 property bool activable: control.spellcheckEnabled
215 property Sonnet.Settings settings: Sonnet.Settings {}
216 active: activable && settings.checkerEnabledByDefault
217 onActiveChanged:
218 {
219 if (active)
220 {
221 item.active = true;
222 }
223 }
224
225 sourceComponent: Sonnet.SpellcheckHighlighter
226 {
227 id: spellcheckhighlighter
228 document: body.textDocument
229 cursorPosition: body.cursorPosition
230 selectionStart: body.selectionStart
231 selectionEnd: body.selectionEnd
232 misspelledColor: Maui.Theme.negativeTextColor
233 active: spellcheckhighlighterLoader.activable && settings.checkerEnabledByDefault
234
235 onChangeCursorPosition: (start, end) =>
236 {
237 body.cursorPosition = start;
238 body.moveCursorSelection(end, TextEdit.SelectCharacters);
239 }
240 }
241 }
242
243 Loader
244 {
245 id: _documentMenuLoader
246
247 asynchronous: true
248 sourceComponent: Maui.ContextualMenu
249 {
250 id: _menu
251 property var spellcheckhighlighter: null
252 property var spellcheckhighlighterLoader: null
253 property int restoredCursorPosition: 0
254 property int restoredSelectionStart
255 property int restoredSelectionEnd
256 property var suggestions: []
257 property bool deselectWhenMenuClosed: true
258 property var runOnMenuClose: () => {}
259 property bool persistentSelectionSetting
260
261 Component.onCompleted:
262 {
263 persistentSelectionSetting = body.persistentSelection
264 }
265
266 Maui.MenuItemActionRow
267 {
268 Action
269 {
270 icon.name: "edit-undo-symbolic"
271 text: i18n("Undo")
272 shortcut: StandardKey.Undo
273
274 onTriggered:
275 {
276 documentMenu.deselectWhenMenuClosed = false;
277 documentMenu.runOnMenuClose = () => body.undo();
278 }
279 }
280
281 Action
282 {
283 icon.name: "edit-redo-symbolic"
284 text: i18n("Redo")
285 shortcut: StandardKey.Redo
286
287 onTriggered:
288 {
289 documentMenu.deselectWhenMenuClosed = false;
290 documentMenu.runOnMenuClose = () => body.redo();
291 }
292 }
293 }
294
295 MenuItem
296 {
297 action: Action
298 {
299 icon.name: "edit-copy-symbolic"
300 text: i18n("Copy")
301 shortcut: StandardKey.Copy
302 }
303
304 onTriggered:
305 {
306 documentMenu.deselectWhenMenuClosed = false;
307 documentMenu.runOnMenuClose = () => control.body.copy();
308 }
309
310 enabled: body.selectedText.length
311 visible: enabled
312 height: visible ? implicitHeight : -_menu.spacing
313 }
314
315 MenuItem
316 {
317 action: Action {
318 icon.name: "edit-cut-symbolic"
319 text: i18n("Cut")
320 shortcut: StandardKey.Cut
321 }
322 onTriggered:
323 {
324 documentMenu.deselectWhenMenuClosed = false;
325 documentMenu.runOnMenuClose = () => control.body.cut();
326 }
327 enabled: !body.readOnly && body.selectedText.length
328 visible: enabled
329 height: visible ? implicitHeight : -_menu.spacing
330 }
331
332 MenuItem
333 {
334 action: Action
335 {
336 icon.name: "edit-paste-symbolic"
337 text: i18n("Paste")
338 shortcut: StandardKey.Paste
339 }
340
341 onTriggered:
342 {
343 documentMenu.deselectWhenMenuClosed = false;
344 documentMenu.runOnMenuClose = () => control.body.paste();
345 }
346
347 enabled: !body.readOnly
348 }
349
350 MenuItem
351 {
352 action: Action
353 {
354 icon.name: "edit-select-all-symbolic"
355 text: i18n("Select All")
356 shortcut: StandardKey.SelectAll
357 }
358
359 onTriggered:
360 {
361 documentMenu.deselectWhenMenuClosed = false
362 documentMenu.runOnMenuClose = () => control.body.selectAll();
363 }
364 }
365
366 MenuSeparator {}
367
368 MenuItem
369 {
370 text: i18nd("mauikittexteditor","Search Selected Text on Google...")
371 onTriggered: Qt.openUrlExternally("https://www.google.com/search?q="+body.selectedText)
372 enabled: body.selectedText.length
373 visible: enabled
374 height: visible ? implicitHeight : -_menu.spacing
375 }
376
377 MenuItem
378 {
379 enabled: control.body.selectedText.length > 0 && Maui.Handy.isEmail(control.body.selectedText)
380 visible: enabled
381 height: visible ? implicitHeight : -_menu.spacing
382 text: i18n("Email")
383 icon.name: "mail-sent"
384 onTriggered: Qt.openUrlExternally("mailto:"+control.body.selectedText)
385 }
386
387 MenuItem
388 {
389 enabled: control.body.selectedText.length > 0 && Maui.Handy.isPhoneNumber(control.body.selectedText)
390 visible: enabled
391 height: visible ? implicitHeight : -_menu.spacing
392 text: i18n("Save as Contact")
393 icon.name: "contact-new-symbolic"
394 onTriggered: Qt.openUrlExternally("tel:"+control.body.selectedText)
395 }
396
397 MenuItem
398 {
399 enabled: control.body.selectedText.length > 0 && Maui.Handy.isWebLink(control.body.selectedText)
400 visible: enabled
401 height: visible ? implicitHeight : -_menu.spacing
402 text: i18n("Open Link")
403 icon.name: "website-symbolic"
404 onTriggered: Qt.openUrlExternally(control.body.selectedText)
405 }
406
407 MenuSeparator {}
408
409 MenuItem
410 {
411 enabled: !control.body.readOnly && control.body.selectedText
412 visible: enabled
413 height: visible ? implicitHeight : -_menu.spacing
414 action: Action
415 {
416 icon.name: "edit-delete-symbolic"
417 text: i18n("Delete")
418 shortcut: StandardKey.Delete
419 }
420
421 onTriggered:
422 {
423 documentMenu.deselectWhenMenuClosed = false;
424 documentMenu.runOnMenuClose = () => control.body.remove(control.body.selectionStart, control.body.selectionEnd);
425 }
426 }
427
428 Menu
429 {
430 id: _spellingMenu
431 title: i18nd("mauikittexteditor","Spelling")
432 enabled: control.spellcheckEnabled
433 visible: enabled
434 height: visible ? implicitHeight : -_menu.spacing
435
436 Instantiator
437 {
438 id: _suggestions
439 active: !control.body.readOnly && documentMenu.spellcheckhighlighter !== null && documentMenu.spellcheckhighlighter.active && documentMenu.spellcheckhighlighter.wordIsMisspelled
440 model: documentMenu.suggestions
441 delegate: MenuItem
442 {
443 text: modelData
444 onClicked:
445 {
446 documentMenu.deselectWhenMenuClosed = false;
447 documentMenu.runOnMenuClose = () => documentMenu.spellcheckhighlighter.replaceWord(modelData);
448 }
449 }
450
451 onObjectAdded: (index, object) =>
452 {
453 _spellingMenu.insertItem(0, object)
454 }
455
456 onObjectRemoved: (index, object) =>
457 {
458 _spellingMenu.removeItem(_spellingMenu.itemAt(0))
459 }
460 }
461
462 MenuSeparator
463 {
464 enabled: !control.body.readOnly && ((documentMenu.spellcheckhighlighter !== null && documentMenu.spellcheckhighlighter.active && documentMenu.spellcheckhighlighter.wordIsMisspelled) || (documentMenu.spellcheckhighlighterLoader && documentMenu.spellcheckhighlighterLoader.activable))
465 }
466
467 MenuItem
468 {
469 enabled: !control.body.readOnly && documentMenu.spellcheckhighlighter !== null && documentMenu.spellcheckhighlighter.active && documentMenu.spellcheckhighlighter.wordIsMisspelled && documentMenu.suggestions.length === 0
470 action: Action
471 {
472 text: documentMenu.spellcheckhighlighter ? i18n("No suggestions for \"%1\"").arg(documentMenu.spellcheckhighlighter.wordUnderMouse) : ''
473 enabled: false
474 }
475 }
476
477 MenuItem
478 {
479 enabled: !control.body.readOnly && documentMenu.spellcheckhighlighter !== null && documentMenu.spellcheckhighlighter.active && documentMenu.spellcheckhighlighter.wordIsMisspelled
480 action: Action
481 {
482 text: documentMenu.spellcheckhighlighter ? i18n("Add \"%1\" to dictionary").arg(documentMenu.spellcheckhighlighter.wordUnderMouse) : ''
483 onTriggered:
484 {
485 documentMenu.deselectWhenMenuClosed = false;
486 documentMenu.runOnMenuClose = () => spellcheckhighlighter.addWordToDictionary(documentMenu.spellcheckhighlighter.wordUnderMouse);
487 }
488 }
489 }
490
491 MenuItem
492 {
493 enabled: !control.body.readOnly && documentMenu.spellcheckhighlighter !== null && documentMenu.spellcheckhighlighter.active && documentMenu.spellcheckhighlighter.wordIsMisspelled
494 action: Action
495 {
496 text: i18n("Ignore")
497 onTriggered:
498 {
499 documentMenu.deselectWhenMenuClosed = false;
500 documentMenu.runOnMenuClose = () => documentMenu.spellcheckhighlighter.ignoreWord(documentMenu.spellcheckhighlighter.wordUnderMouse);
501 }
502 }
503 }
504
505 MenuItem
506 {
507 enabled: !control.body.readOnly && documentMenu.spellcheckhighlighterLoader && documentMenu.spellcheckhighlighterLoader.activable
508 checkable: true
509 checked: documentMenu.spellcheckhighlighter ? documentMenu.spellcheckhighlighter.active : false
510 text: i18n("Enable Spellchecker")
511 onCheckedChanged:
512 {
513 spellcheckhighlighterLoader.active = checked;
514 documentMenu.spellcheckhighlighter = documentMenu.spellcheckhighlighterLoader.item;
515 }
516 }
517 }
518
519 function targetClick(spellcheckhighlighter, mousePosition)
520 {
521 control.body.persistentSelection = true; // persist selection when menu is opened
522 documentMenu.spellcheckhighlighterLoader = spellcheckhighlighter;
523 if (spellcheckhighlighter && spellcheckhighlighter.active) {
524 documentMenu.spellcheckhighlighter = spellcheckhighlighter.item;
525 documentMenu.suggestions = mousePosition ? spellcheckhighlighter.item.suggestions(mousePosition) : [];
526 } else {
527 documentMenu.spellcheckhighlighter = null;
528 documentMenu.suggestions = [];
529 }
530
531 storeCursorAndSelection();
532 documentMenu.show()
533 }
534
535 function storeCursorAndSelection()
536 {
537 documentMenu.restoredCursorPosition = control.body.cursorPosition;
538 documentMenu.restoredSelectionStart = control.body.selectionStart;
539 documentMenu.restoredSelectionEnd = control.body.selectionEnd;
540 }
541
542 onOpened:
543 {
544 runOnMenuClose = () => {};
545 }
546
547 onClosed:
548 {
549 // restore text field's original persistent selection setting
550 body.persistentSelection = documentMenu.persistentSelectionSetting
551 // deselect text field text if menu is closed not because of a right click on the text field
552 if (documentMenu.deselectWhenMenuClosed)
553 {
554 body.deselect();
555 }
556 documentMenu.deselectWhenMenuClosed = true;
557
558 // restore cursor position
559 body.forceActiveFocus();
560 body.cursorPosition = documentMenu.restoredCursorPosition;
561 body.select(documentMenu.restoredSelectionStart, documentMenu.restoredSelectionEnd);
562
563 // run action, and free memory
564 runOnMenuClose();
565 runOnMenuClose = () => {};
566 }
567 }
568 }
569
570 footer: Column
571 {
572 width: parent.width
573
574 Maui.ToolBar
575 {
576 id: _findToolBar
577 visible: showFindBar
578 width: parent.width
579 position: ToolBar.Footer
580
581 rightContent: ToolButton
582 {
583 id: _replaceButton
584 icon.name: "edit-find-replace"
585 checkable: true
586 checked: false
587 }
588
589 leftContent: Maui.ToolButtonMenu
590 {
591 icon.name: "overflow-menu"
592
593 MenuItem
594 {
595 id: _findCaseSensitively
596 checkable: true
597 text: i18nd("mauikittexteditor","Case Sensitive")
598 }
599
600 MenuItem
601 {
602 id: _findWholeWords
603 checkable: true
604 text: i18nd("mauikittexteditor","Whole Words Only")
605 }
606 }
607
608 middleContent: Maui.SearchField
609 {
610 id: _findField
611 Layout.fillWidth: true
612 Layout.maximumWidth: 500
613 Layout.alignment: Qt.AlignCenter
614 placeholderText: i18nd("mauikittexteditor","Find")
615
616 onAccepted:
617 {
618 document.find(text)
619 }
620
621 actions:[
622
623 Action
624 {
625 enabled: _findField.text.length
626 icon.name: "arrow-up"
627 onTriggered: document.find(_findField.text, false)
628 }
629 ]
630 }
631 }
632
633 Maui.ToolBar
634 {
635 id: _replaceToolBar
636 position: ToolBar.Footer
637 visible: _replaceButton.checked && _findToolBar.visible
638 width: parent.width
639 enabled: !body.readOnly
640 forceCenterMiddleContent: false
641
642 middleContent: Maui.SearchField
643 {
644 id: _replaceField
645 placeholderText: i18nd("mauikittexteditor","Replace")
646 Layout.fillWidth: true
647 Layout.maximumWidth: 500
648 Layout.alignment: Qt.AlignCenter
649 icon.source: "edit-find-replace"
650 actions: Action
651 {
652 text: i18nd("mauikittexteditor","Replace")
653 enabled: _replaceField.text.length
654 icon.name: "checkmark"
655 onTriggered: document.replace(_findField.text, _replaceField.text)
656 }
657 }
658
659 rightContent: Button
660 {
661 enabled: _replaceField.text.length
662 text: i18nd("mauikittexteditor","Replace All")
663 onClicked: document.replaceAll(_findField.text, _replaceField.text)
664 }
665 }
666 }
667
668 header: Column
669 {
670 width: parent.width
671
672 Repeater
673 {
674 model: document.alerts
675
676 Maui.ToolBar
677 {
678 id: _alertBar
679 property var alert : model.alert
680 readonly property int index_ : index
681 width: parent.width
682
683 Maui.Theme.backgroundColor:
684 {
685 switch(alert.level)
686 {
687 case 0: return Maui.Theme.positiveBackgroundColor
688 case 1: return Maui.Theme.neutralBackgroundColor
689 case 2: return Maui.Theme.negativeBackgroundColor
690 }
691 }
692
693 Maui.Theme.textColor:
694 {
695 switch(alert.level)
696 {
697 case 0: return Maui.Theme.positiveTextColor
698 case 1: return Maui.Theme.neutralTextColor
699 case 2: return Maui.Theme.negativeTextColor
700 }
701 }
702
703 forceCenterMiddleContent: false
704 middleContent: Maui.ListItemTemplate
705 {
706 Maui.Theme.inherit: true
707 Layout.fillWidth: true
708 Layout.fillHeight: true
709 label1.text: alert.title
710 label2.text: alert.body
711 }
712
713 rightContent: Repeater
714 {
715 model: alert.actionLabels
716
717 Button
718 {
719 id: _alertAction
720 property int index_ : index
721 text: modelData
722 onClicked: alert.triggerAction(_alertAction.index_, _alertBar.index_)
723
724 Maui.Theme.backgroundColor: Qt.lighter(_alertBar.Maui.Theme.backgroundColor, 1.2)
725 Maui.Theme.hoverColor: Qt.lighter(_alertBar.Maui.Theme.backgroundColor, 1)
726 Maui.Theme.textColor: Qt.darker(Maui.Theme.backgroundColor)
727 }
728 }
729 }
730 }
731 }
732
733 Component
734 {
735 id: _linesCounterComponent
736
737 Rectangle
738 {
739 anchors.fill: parent
740 anchors.topMargin: body.topPadding + body.textMargin
741
742 color: Qt.darker(Maui.Theme.backgroundColor, 1.2)
743
744 ListView
745 {
746 id: _linesCounterList
747 anchors.fill: parent
748 interactive: false
749 enabled: false
750
751 Binding on contentY
752 {
753 value: _flickable.contentY
754 restoreMode: Binding.RestoreBindingOrValue
755 }
756
757 model: TE.LineNumberModel
758 {
759 lineCount: body.text !== "" ? document.lineCount : 0
760 }
761
762 delegate: RowLayout
763 {
764 id: _delegate
765
766 readonly property int line : index
767 // property bool foldable : control.document.isFoldable(line)
768
769 width: ListView.view.width
770 height: Math.max(Math.ceil(fontMetrics.lineSpacing), document.lineHeight(line))
771
772 readonly property bool isCurrentItem : document.currentLineIndex === index
773
774 Connections
775 {
776 target: control.body
777
778 function onContentHeightChanged()
779 {
780 if(body.wrapMode !== Text.NoWrap)
781 {
782 _delegate.height = control.document.lineHeight(_delegate.line)
783 }
784
785 if(_delegate.isCurrentItem)
786 {
787 console.log("Updating line height")
788 // _delegate.foldable = control.document.isFoldable(_delegate.line)
789 }
790
791 _linesCounterList.contentY = _flickable.contentY
792 }
793
794 function onWrapModeChanged()
795 {
796 _delegate.height = control.document.lineHeight(_delegate.line)
797 }
798 }
799
800 Label
801 {
802 Layout.fillWidth: true
803 Layout.fillHeight: true
804
805 opacity: isCurrentItem ? 1 : 0.7
806 color: isCurrentItem ? control.Maui.Theme.highlightedTextColor : control.body.color
807 font.pointSize: Math.min(Maui.Style.fontSizes.medium, body.font.pointSize)
808 horizontalAlignment: Text.AlignHCenter
809 verticalAlignment: Text.AlignTop
810 // renderType: Text.NativeRendering
811 font.family: "Monospace"
812 text: index+1
813
814 background: Rectangle
815 {
816 visible: isCurrentItem
817 color: Maui.Theme.highlightColor
818 }
819 }
820
821 // AbstractButton
822 // {
823 // visible: foldable
824 // Layout.alignment: Qt.AlignVCenter
825 // implicitHeight: 8
826 // implicitWidth: 8
827 // //onClicked:
828 // //{
829 // //control.goToLine(_delegate.line)
830 // //control.document.toggleFold(_delegate.line)
831 // //}
832 // contentItem: Maui.Icon
833 // {
834 // source: "go-down"
835 // color: isCurrentItem ? control.Maui.Theme.highlightedTextColor : control.body.color
836 // }
837 // }
838 }
839
840 }
841 }
842
843 }
844
845 contentItem: Item
846 {
847 RowLayout
848 {
849 anchors.fill: parent
850 clip: false
851
852 Loader
853 {
854 id: _linesCounter
855 asynchronous: true
856 active: control.showLineNumbers && !document.isRich && body.lineCount > 1 && body.wrapMode === Text.NoWrap
857
858 Layout.fillHeight: true
859 Layout.preferredWidth: active ? fontMetrics.averageCharacterWidth
860 * (Math.floor(Math.log10(body.lineCount)) + 1) + 10 : 0
861
862
863 sourceComponent: _linesCounterComponent
864 }
865
867 {
868 id: _scrollView
869
870 Layout.fillHeight: true
871 Layout.fillWidth: true
872
873 clip: false
874
875 ScrollBar.horizontal.policy: ScrollBar.AsNeeded
876
877 Keys.enabled: true
878 Keys.forwardTo: body
879 Keys.onPressed: (event) =>
880 {
881 if((event.key === Qt.Key_F) && (event.modifiers & Qt.ControlModifier))
882 {
883 control.showFindBar = true
884
885 if(control.body.selectedText.length)
886 {
887 _findField.text = control.body.selectedText
888 }else
889 {
890 _findField.selectAll()
891 }
892
893 _findField.forceActiveFocus()
894 event.accepted = true
895 }
896
897 if((event.key === Qt.Key_R) && (event.modifiers & Qt.ControlModifier))
898 {
899 control.showFindBar = true
900 _replaceButton.checked = true
901 _findField.text = control.body.selectedText
902 _replaceField.forceActiveFocus()
903 event.accepted = true
904 }
905 }
906
907 Flickable
908 {
909 id: _flickable
910 clip: false
911
912 interactive: true
913 boundsBehavior : Flickable.StopAtBounds
914 boundsMovement : Flickable.StopAtBounds
915
916 TextArea.flickable: TextArea
917 {
918 id: body
919 Maui.Theme.inherit: true
920 text: document.text
921
922 placeholderText: i18nd("mauikittexteditor","Body")
923
924 textFormat: TextEdit.PlainText
925
926 tabStopDistance: fontMetrics.averageCharacterWidth * 4
927 renderType: Text.QtRendering
928 antialiasing: true
929 activeFocusOnPress: true
930 focusPolicy: Qt.StrongFocus
931
932 Keys.onReturnPressed: (event) =>
933 {
934 body.insert(body.cursorPosition, "\n")
935 if(Maui.Handy.isAndroid)//workaround for Android, since pressing return/enter will close the keyboard after inserting the break
936 /*The fix to this workaround has been introduced into Qt 6.8
937 see: https://doc.qt.io/qt-6/qml-qtquick-virtualkeyboard-settings-virtualkeyboardsettings.html#closeOnReturn-prop
938 */
939 {
940 Qt.inputMethod.show();
941 event.accepted = true
942 }
943 }
944
945 Keys.onPressed: (event) =>
946 {
947 if(event.key === Qt.Key_PageUp)
948 {
949 _flickable.flick(0, 60*Math.sqrt(_flickable.height))
950 event.accepted = true
951 }
952
953 if(event.key === Qt.Key_PageDown)
954 {
955 _flickable.flick(0, -60*Math.sqrt(_flickable.height))
956 event.accepted = true
957 } // TODO: Move cursor
958 }
959
960 onPressAndHold: (event) =>
961 {
962 if(Maui.Handy.isAndroid)
963 {
964 return
965 }
966
967 if(Maui.Handy.isMobile || Maui.Handy.isTouch)
968 {
969 documentMenu.targetClick(spellcheckhighlighterLoader, body.positionAt(event.x, event.y))
970 event.accepted = true
971 return
972 }
973 event.accepted = false
974 }
975
976 onPressed: (event) =>
977 {
978 if(Maui.Handy.isMobile)
979 {
980 return
981 }
982
983 if(event.button === Qt.RightButton)
984 {
985 documentMenu.targetClick(spellcheckhighlighterLoader, body.positionAt(event.x, event.y))
986 event.accepted = true
987 }
988 }
989 }
990 }
991 }
992 }
993
994 Loader
995 {
996 active: Maui.Handy.isTouch
997 asynchronous: true
998
999 anchors.bottom: parent.bottom
1000 anchors.right: parent.right
1001 anchors.margins: Maui.Style.space.big
1002
1003 sourceComponent: Maui.FloatingButton
1004 {
1005 icon.name: "edit-menu"
1006 onClicked: documentMenu.targetClick(spellcheckhighlighterLoader, body.cursorPosition)
1007 }
1008 }
1009 }
1010
1011 /**
1012 * @brief Force to focus the text area for input
1013 */
1014 function forceActiveFocus()
1015 {
1016 body.forceActiveFocus()
1017 }
1018
1019 /**
1020 * @brief Position the view and cursor at the given line number
1021 * @param line the line number
1022 */
1023 function goToLine(line)
1024 {
1025 if(line>0 && line <= body.lineCount)
1026 {
1027 body.cursorPosition = document.goToLine(line-1)
1028 body.forceActiveFocus()
1029 }
1030 }
1031}
alias text
Alias to the text area text content.
alias document
Alias to access the DocumentHandler.
alias documentMenu
Alias to the contextual menu.
alias underline
alias fileUrl
If a file url is provided the DocumentHandler will try to open its contents and display it.
bool spellcheckEnabled
Whether to enable the spell checker.
alias uppercase
alias scrollView
Alias to the ScrollView.
bool showLineNumbers
If a sidebar listing each line number should be visible.
alias canRedo
Whether there are modifications to the document that can be redo.
alias body
Access to the editor text area.
Q_SCRIPTABLE QString start(QString train="")
QString i18nd(const char *domain, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
const QList< QKeySequence > & end()
void forceActiveFocus()
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri May 2 2025 12:05:33 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.