1/*
2 Nested list helper
3 SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
4
5 SPDX-License-Identifier: LGPL-2.1-or-later
6*/
7
8#include "nestedlisthelper_p.h"
9
10#include <QKeyEvent>
11#include <QTextBlock>
12#include <QTextCursor>
13#include <QTextList>
14
15#include "ktextedit.h"
16
17NestedListHelper::NestedListHelper(QTextEdit *te)
18 : textEdit(te)
19{
20}
21
22NestedListHelper::~NestedListHelper()
23{
24}
25
26bool NestedListHelper::handleKeyPressEvent(QKeyEvent *event)
27{
28 QTextCursor cursor = textEdit->textCursor();
29 if (!cursor.currentList()) {
30 return false;
31 }
32
33 if (event->key() == Qt::Key_Backspace && !cursor.hasSelection() && cursor.atBlockStart() && canDedent()) {
34 changeIndent(delta: -1);
35 return true;
36 }
37
38 if (event->key() == Qt::Key_Return && !cursor.hasSelection() && cursor.block().text().isEmpty() && canDedent()) {
39 changeIndent(delta: -1);
40 return true;
41 }
42
43 if (event->key() == Qt::Key_Tab && (cursor.atBlockStart() || cursor.hasSelection()) && canIndent()) {
44 changeIndent(delta: +1);
45 return true;
46 }
47
48 return false;
49}
50
51bool NestedListHelper::canIndent() const
52{
53 const QTextCursor cursor = topOfSelection();
54 const QTextBlock block = cursor.block();
55 if (!block.isValid()) {
56 return false;
57 }
58 if (!block.textList()) {
59 return true;
60 }
61 const QTextBlock prevBlock = block.previous();
62 if (!prevBlock.textList()) {
63 return false;
64 }
65 return block.textList()->format().indent() <= prevBlock.textList()->format().indent();
66}
67
68bool NestedListHelper::canDedent() const
69{
70 const QTextCursor cursor = bottomOfSelection();
71 const QTextBlock block = cursor.block();
72 if (!block.isValid()) {
73 return false;
74 }
75 if (!block.textList() || block.textList()->format().indent() <= 0) {
76 return false;
77 }
78 const QTextBlock nextBlock = block.next();
79 if (!nextBlock.textList()) {
80 return true;
81 }
82 return block.textList()->format().indent() >= nextBlock.textList()->format().indent();
83}
84
85bool NestedListHelper::handleAfterDropEvent(QDropEvent *dropEvent)
86{
87 Q_UNUSED(dropEvent);
88 QTextCursor cursor = topOfSelection();
89
90 QTextBlock droppedBlock = cursor.block();
91 int firstDroppedItemIndent = droppedBlock.textList()->format().indent();
92
93 int minimumIndent = droppedBlock.previous().textList()->format().indent();
94
95 if (firstDroppedItemIndent < minimumIndent) {
96 cursor = QTextCursor(droppedBlock);
97 QTextListFormat fmt = droppedBlock.textList()->format();
98 fmt.setIndent(minimumIndent);
99 QTextList *list = cursor.createList(format: fmt);
100
101 int endOfDrop = bottomOfSelection().position();
102 while (droppedBlock.next().position() < endOfDrop) {
103 droppedBlock = droppedBlock.next();
104 if (droppedBlock.textList()->format().indent() != firstDroppedItemIndent) {
105 // new list?
106 }
107 list->add(block: droppedBlock);
108 }
109 // list.add( droppedBlock );
110 }
111
112 return true;
113}
114
115void NestedListHelper::processList(QTextList *list)
116{
117 QTextBlock block = list->item(i: 0);
118 int thisListIndent = list->format().indent();
119
120 QTextCursor cursor = QTextCursor(block);
121 list = cursor.createList(format: list->format());
122 bool processingSubList = false;
123 while (block.next().textList() != nullptr) {
124 block = block.next();
125
126 QTextList *nextList = block.textList();
127 int nextItemIndent = nextList->format().indent();
128 if (nextItemIndent < thisListIndent) {
129 return;
130 } else if (nextItemIndent > thisListIndent) {
131 if (processingSubList) {
132 continue;
133 }
134 processingSubList = true;
135 processList(list: nextList);
136 } else {
137 processingSubList = false;
138 list->add(block);
139 }
140 }
141 // delete nextList;
142 // nextList = 0;
143}
144
145void NestedListHelper::reformatList(QTextBlock block)
146{
147 if (block.textList()) {
148 int minimumIndent = block.textList()->format().indent();
149
150 // Start at the top of the list
151 while (block.previous().textList() != nullptr) {
152 if (block.previous().textList()->format().indent() < minimumIndent) {
153 break;
154 }
155 block = block.previous();
156 }
157
158 processList(list: block.textList());
159 }
160}
161
162void NestedListHelper::reformatList()
163{
164 QTextCursor cursor = textEdit->textCursor();
165 reformatList(block: cursor.block());
166}
167
168QTextCursor NestedListHelper::topOfSelection() const
169{
170 QTextCursor cursor = textEdit->textCursor();
171
172 if (cursor.hasSelection()) {
173 cursor.setPosition(pos: qMin(a: cursor.position(), b: cursor.anchor()));
174 }
175 return cursor;
176}
177
178QTextCursor NestedListHelper::bottomOfSelection() const
179{
180 QTextCursor cursor = textEdit->textCursor();
181
182 if (cursor.hasSelection()) {
183 cursor.setPosition(pos: qMax(a: cursor.position(), b: cursor.anchor()));
184 }
185 return cursor;
186}
187
188void NestedListHelper::changeIndent(int delta)
189{
190 QTextCursor cursor = textEdit->textCursor();
191 cursor.beginEditBlock();
192
193 const int top = qMin(a: cursor.position(), b: cursor.anchor());
194 const int bottom = qMax(a: cursor.position(), b: cursor.anchor());
195
196 // A reformatList should be called on the block inside selection
197 // with the lowest indentation level
198 int minIndentPosition;
199 int minIndent = -1;
200
201 // Changing indentation of all blocks between top and bottom
202 cursor.setPosition(pos: top);
203 do {
204 QTextList *list = cursor.currentList();
205 // Setting up listFormat
206 QTextListFormat listFmt;
207 if (!list) {
208 if (delta > 0) {
209 // No list, we're increasing indentation -> create a new one
210 listFmt.setStyle(QTextListFormat::ListDisc);
211 listFmt.setIndent(delta);
212 }
213 // else do nothing
214 } else {
215 const int newIndent = list->format().indent() + delta;
216 if (newIndent > 0) {
217 listFmt = list->format();
218 listFmt.setIndent(newIndent);
219 } else {
220 listFmt.setIndent(0);
221 }
222 }
223
224 if (listFmt.indent() > 0) {
225 // This block belongs to a list: here we create a new one
226 // for each block, and then let reformatList() sort it out
227 cursor.createList(format: listFmt);
228 if (minIndent == -1 || minIndent > listFmt.indent()) {
229 minIndent = listFmt.indent();
230 minIndentPosition = cursor.block().position();
231 }
232 } else {
233 // If the block belonged to a list, remove it from there
234 if (list) {
235 list->remove(cursor.block());
236 }
237 // The removal does not change the indentation, we need to do it explicitly
238 QTextBlockFormat blkFmt;
239 blkFmt.setIndent(0);
240 cursor.mergeBlockFormat(modifier: blkFmt);
241 }
242 if (!cursor.block().next().isValid()) {
243 break;
244 }
245 cursor.movePosition(op: QTextCursor::NextBlock);
246 } while (cursor.position() < bottom);
247 // Reformatting the whole list
248 if (minIndent != -1) {
249 cursor.setPosition(pos: minIndentPosition);
250 reformatList(block: cursor.block());
251 }
252 cursor.setPosition(pos: top);
253 reformatList(block: cursor.block());
254 cursor.endEditBlock();
255}
256
257void NestedListHelper::handleOnBulletType(int styleIndex)
258{
259 QTextCursor cursor = textEdit->textCursor();
260 if (styleIndex != 0) {
261 QTextListFormat::Style style = static_cast<QTextListFormat::Style>(styleIndex);
262 QTextList *currentList = cursor.currentList();
263 QTextListFormat listFmt;
264
265 cursor.beginEditBlock();
266
267 if (currentList) {
268 listFmt = currentList->format();
269 listFmt.setStyle(style);
270 currentList->setFormat(listFmt);
271 } else {
272 listFmt.setStyle(style);
273 cursor.createList(format: listFmt);
274 }
275
276 cursor.endEditBlock();
277 } else {
278 QTextBlockFormat bfmt;
279 bfmt.setObjectIndex(-1);
280 cursor.setBlockFormat(bfmt);
281 }
282
283 reformatList();
284}
285

source code of ktextwidgets/src/widgets/nestedlisthelper.cpp