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 
NestedListHelper(QTextEdit * te)17 NestedListHelper::NestedListHelper(QTextEdit *te)
18     : textEdit(te)
19 {
20 }
21 
~NestedListHelper()22 NestedListHelper::~NestedListHelper()
23 {
24 }
25 
handleKeyPressEvent(QKeyEvent * event)26 bool 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(-1);
35         return true;
36     }
37 
38     if (event->key() == Qt::Key_Return && !cursor.hasSelection() && cursor.block().text().isEmpty() && canDedent()) {
39         changeIndent(-1);
40         return true;
41     }
42 
43     if (event->key() == Qt::Key_Tab && (cursor.atBlockStart() || cursor.hasSelection()) && canIndent()) {
44         changeIndent(+1);
45         return true;
46     }
47 
48     return false;
49 }
50 
canIndent() const51 bool 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 
canDedent() const68 bool 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 
handleAfterDropEvent(QDropEvent * dropEvent)85 bool 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(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(droppedBlock);
108         }
109         //         list.add( droppedBlock );
110     }
111 
112     return true;
113 }
114 
processList(QTextList * list)115 void NestedListHelper::processList(QTextList *list)
116 {
117     QTextBlock block = list->item(0);
118     int thisListIndent = list->format().indent();
119 
120     QTextCursor cursor = QTextCursor(block);
121     list = cursor.createList(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(nextList);
136         } else {
137             processingSubList = false;
138             list->add(block);
139         }
140     }
141     //     delete nextList;
142     //     nextList = 0;
143 }
144 
reformatList(QTextBlock block)145 void 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(block.textList());
159     }
160 }
161 
reformatList()162 void NestedListHelper::reformatList()
163 {
164     QTextCursor cursor = textEdit->textCursor();
165     reformatList(cursor.block());
166 }
167 
topOfSelection() const168 QTextCursor NestedListHelper::topOfSelection() const
169 {
170     QTextCursor cursor = textEdit->textCursor();
171 
172     if (cursor.hasSelection()) {
173         cursor.setPosition(qMin(cursor.position(), cursor.anchor()));
174     }
175     return cursor;
176 }
177 
bottomOfSelection() const178 QTextCursor NestedListHelper::bottomOfSelection() const
179 {
180     QTextCursor cursor = textEdit->textCursor();
181 
182     if (cursor.hasSelection()) {
183         cursor.setPosition(qMax(cursor.position(), cursor.anchor()));
184     }
185     return cursor;
186 }
187 
changeIndent(int delta)188 void NestedListHelper::changeIndent(int delta)
189 {
190     QTextCursor cursor = textEdit->textCursor();
191     cursor.beginEditBlock();
192 
193     const int top = qMin(cursor.position(), cursor.anchor());
194     const int bottom = qMax(cursor.position(), 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(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(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(blkFmt);
241         }
242         if (!cursor.block().next().isValid()) {
243             break;
244         }
245         cursor.movePosition(QTextCursor::NextBlock);
246     } while (cursor.position() < bottom);
247     // Reformatting the whole list
248     if (minIndent != -1) {
249         cursor.setPosition(minIndentPosition);
250         reformatList(cursor.block());
251     }
252     cursor.setPosition(top);
253     reformatList(cursor.block());
254     cursor.endEditBlock();
255 }
256 
handleOnBulletType(int styleIndex)257 void 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(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