├── texteditlib.pri ├── tests ├── textdocument │ ├── unicode.txt │ ├── textdocument.pro │ └── tst_textdocument.cpp ├── tests.pro ├── run.sh ├── syntaxhighlighter │ ├── syntaxhighlighter.pro │ └── tst_syntaxhighlighter.cpp ├── textedit │ ├── textedit.pro │ ├── spectre.scs │ └── tst_textedit.cpp └── textcursor │ ├── textcursor.pro │ └── tst_textcursor.cpp ├── .gitignore ├── weakpointer.h ├── textedit.pro ├── README ├── textedit.pri ├── textsection_p.h ├── textsection.cpp ├── textsection.h ├── syntaxhighlighter.h ├── syntaxhighlighter.cpp ├── textcursor.h ├── textlayout_p.h ├── textedit_p.h ├── textedit.h ├── textdocument.h ├── textcursor_p.h ├── LICENSE ├── textdocument_p.h ├── textlayout_p.cpp ├── main.cpp └── textcursor.cpp /texteditlib.pri: -------------------------------------------------------------------------------- 1 | TEMPLATE = staticlib 2 | LIBS += -ltextedit -L$$(PWD) 3 | 4 | include($$PWD/textedit.pri) 5 | -------------------------------------------------------------------------------- /tests/textdocument/unicode.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andersbakken/LazyTextEdit/HEAD/tests/textdocument/unicode.txt -------------------------------------------------------------------------------- /tests/tests.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = subdirs 2 | SUBDIRS += textcursor 3 | SUBDIRS += textedit 4 | SUBDIRS += textdocument 5 | SUBDIRS += syntaxhighlighter 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | moc_* 3 | *.moc 4 | *.log 5 | Makefile 6 | tests/textedit/textedit 7 | tests/textcursor/textcursor 8 | tests/textdocument/textdocument 9 | tests/syntaxhighlighter/syntaxhighlighter 10 | /textedit 11 | -------------------------------------------------------------------------------- /tests/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dir="`dirname $0`" 4 | grep SUBDIRS "$dir/tests.pro" | awk '{print $NF}' | while read i; do 5 | pushd $dir/$i 6 | qmake 7 | make 8 | echo $PWD 9 | ./$i 10 | popd 11 | done 12 | -------------------------------------------------------------------------------- /weakpointer.h: -------------------------------------------------------------------------------- 1 | #ifndef WEAKPOINTER_H 2 | #define WEAKPOINTER_H 3 | 4 | #include 5 | 6 | #if (QT_VERSION < QT_VERSION_CHECK(4, 6, 0)) 7 | // while QWeakPointer existed in Qt 4.5 it lacked certain APIs (like 8 | // data so I'll stick with QPointer until 4.6) 9 | #include 10 | #define WeakPointer QPointer 11 | #else 12 | #include 13 | #define WeakPointer QWeakPointer 14 | #endif 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /textedit.pro: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | # Automatically generated by qmake (2.01a) Thu Apr 17 13:41:18 2008 3 | ###################################################################### 4 | 5 | TEMPLATE = app 6 | 7 | QT += core gui 8 | greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 9 | 10 | SOURCES += main.cpp 11 | include($$PWD/textedit.pri) 12 | CONFIG -= app_bundle 13 | DEFINES += TEXTDOCUMENT_LINENUMBER_CACHE 14 | release { 15 | QMAKE_CXXFLAGS += -g 16 | } 17 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | LazyTextEdit is a Qt based text editor that lazily loads data from disk when 2 | necessary. 3 | 4 | It tries to keep memory usage as low as possible and only stores chunks of data 5 | that have been modified. Its APIs resembles that of 6 | QText(Edit|Document|Cursor). It supports basic formatting options using 7 | TextSections and SyntaxHighlighter. 8 | 9 | LazyTextEdit is licensed under the Apache 2.0 license (See LICENSE) 10 | 11 | If you have bug reports/feature requests etc please send an email to Anders 12 | Bakken 13 | -------------------------------------------------------------------------------- /tests/syntaxhighlighter/syntaxhighlighter.pro: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | # Automatically generated by qmake (2.01a) Fri Jul 18 18:56:23 2008 3 | ###################################################################### 4 | 5 | TEMPLATE = app 6 | TARGET = 7 | DEPENDPATH += . 8 | INCLUDEPATH += . 9 | 10 | # Input 11 | SOURCES += tst_syntaxhighlighter.cpp 12 | CONFIG += debug 13 | CONFIG -= app_bundle 14 | unix { 15 | MOC_DIR=.moc 16 | UI_DIR=.ui 17 | OBJECTS_DIR=.obj 18 | } else { 19 | MOC_DIR=tmp/moc 20 | UI_DIR=tmp/ui 21 | OBJECTS_DIR=tmp/obj 22 | } 23 | load(qtestlib.prf) 24 | include(../../textedit.pri) 25 | -------------------------------------------------------------------------------- /tests/textedit/textedit.pro: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | # Automatically generated by qmake (2.01a) Fri Jul 18 18:56:23 2008 3 | ###################################################################### 4 | 5 | TEMPLATE = app 6 | TARGET = 7 | DEPENDPATH += . 8 | INCLUDEPATH += . 9 | 10 | # Input 11 | SOURCES += tst_textedit.cpp 12 | CONFIG += debug 13 | CONFIG -= app_bundle 14 | DEFINES += LAZYTEXTEDIT_AUTOTEST 15 | unix { 16 | MOC_DIR=.moc 17 | UI_DIR=.ui 18 | OBJECTS_DIR=.obj 19 | } else { 20 | MOC_DIR=tmp/moc 21 | UI_DIR=tmp/ui 22 | OBJECTS_DIR=tmp/obj 23 | } 24 | load(qtestlib.prf) 25 | include(../../textedit.pri) 26 | -------------------------------------------------------------------------------- /tests/textcursor/textcursor.pro: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | # Automatically generated by qmake (2.01a) Fri Jul 18 18:56:23 2008 3 | ###################################################################### 4 | 5 | TEMPLATE = app 6 | TARGET = 7 | DEPENDPATH += . 8 | INCLUDEPATH += . 9 | DEFINES += LAZYTEXTEDIT_AUTOTEST 10 | 11 | # Input 12 | SOURCES += tst_textcursor.cpp 13 | CONFIG += debug 14 | CONFIG -= app_bundle 15 | unix { 16 | MOC_DIR=.moc 17 | UI_DIR=.ui 18 | OBJECTS_DIR=.obj 19 | } else { 20 | MOC_DIR=tmp/moc 21 | UI_DIR=tmp/ui 22 | OBJECTS_DIR=tmp/obj 23 | } 24 | load(qtestlib.prf) 25 | include(../../textedit.pri) 26 | -------------------------------------------------------------------------------- /textedit.pri: -------------------------------------------------------------------------------- 1 | INCLUDEPATH += $$PWD 2 | 3 | DEFINES += FATAL_ASSUMES TEXTDOCUMENT_LINENUMBER_CACHE 4 | #DEFINES += TEXTDOCUMENT_FIND_INTERVAL_PERCENTAGE=100 5 | # Input 6 | SOURCES += $$PWD/textedit.cpp $$PWD/textdocument.cpp $$PWD/syntaxhighlighter.cpp $$PWD/textcursor.cpp $$PWD/textlayout_p.cpp $$PWD/textsection.cpp 7 | HEADERS += $$PWD/textedit.h $$PWD/textdocument.h $$PWD/textdocument_p.h $$PWD/syntaxhighlighter.h $$PWD/textcursor.h $$PWD/textlayout_p.h $$PWD/textedit_p.h $$PWD/textcursor_p.h $$PWD/textsection.h $$PWD/weakpointer.h 8 | unix { 9 | MOC_DIR=.moc 10 | UI_DIR=.ui 11 | OBJECTS_DIR=.obj 12 | } else { 13 | MOC_DIR=tmp/moc 14 | UI_DIR=tmp/ui 15 | OBJECTS_DIR=tmp/obj 16 | } 17 | -------------------------------------------------------------------------------- /tests/textdocument/textdocument.pro: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | # Automatically generated by qmake (2.01a) Fri Jul 18 18:56:23 2008 3 | ###################################################################### 4 | 5 | TEMPLATE = app 6 | TARGET = 7 | DEPENDPATH += . 8 | INCLUDEPATH += . 9 | 10 | # Input 11 | SOURCES += tst_textdocument.cpp 12 | CONFIG += debug 13 | CONFIG -= app_bundle 14 | DEFINES += TEXTDOCUMENT_FIND_SLEEP LAZYTEXTEDIT_AUTOTEST 15 | 16 | unix { 17 | MOC_DIR=.moc 18 | UI_DIR=.ui 19 | OBJECTS_DIR=.obj 20 | } else { 21 | MOC_DIR=tmp/moc 22 | UI_DIR=tmp/ui 23 | OBJECTS_DIR=tmp/obj 24 | } 25 | load(qtestlib.prf) 26 | include(../../textedit.pri) 27 | 28 | -------------------------------------------------------------------------------- /tests/textedit/spectre.scs: -------------------------------------------------------------------------------- 1 | // "spectre" description for "worklib", "TB1_vco", "Spectre" 2 | 3 | 4 | simulator lang=spectre 5 | 6 | subckt TB1_vco out1 out2 out3 7 | // X1 (ibias out0 out1 out2 out3 out4 out5 out6 out7 vcom vcop gnd! vdd! vdd! gnd!) vco 8 | X1 (ibias out0 out1 out2 out3 out4 out5 out6 out7 vcom vcop) vco 9 | I5 (setIC0!) ICforce amp=500m 10 | I14 (sch_setIC0!) ICforce amp=0 11 | V1 (net030 vcop) vsource type=pwl wave=[ 0 0 40n 0 80n 200m 160n -200m ] 12 | V2 (vcom net030) vsource type=pwl wave=[ 0 0 40n 0 80n 200m 160n -200m ] 13 | R0 (out0 gnd!) res r=1G 14 | I2 (Freq out0) F2Vconv 15 | VCM (net030 gnd!) vsource dc=vdd/2 type=dc 16 | V5 (vdd! gnd!) vsource dc=vdd type=dc 17 | I25 (gnd! ibias) isource dc=20u type=dc 18 | ends TB1_vco 19 | 20 | I1 (o1 o2 o3) TB1_vco -------------------------------------------------------------------------------- /textsection_p.h: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #ifndef TEXTSECTION_P_H 16 | #define TEXTSECTION_P_H 17 | 18 | #include 19 | #include 20 | class TextSection; 21 | class TextSectionManager : public QObject 22 | { 23 | Q_OBJECT 24 | public: 25 | static TextSectionManager *instance() { static TextSectionManager *inst = new TextSectionManager; return inst; } 26 | signals: 27 | void sectionFormatChanged(TextSection *section); 28 | void sectionCursorChanged(TextSection *section); 29 | private: 30 | TextSectionManager() : QObject(QCoreApplication::instance()) {} 31 | friend class TextSection; 32 | }; 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /textsection.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include "textsection.h" 16 | #include "textdocument.h" 17 | #include "textdocument_p.h" 18 | 19 | TextSection::~TextSection() 20 | { 21 | if (d.document) 22 | d.document->takeTextSection(this); 23 | } 24 | 25 | QString TextSection::text() const 26 | { 27 | Q_ASSERT(d.document); 28 | return d.document->read(d.position, d.size); 29 | } 30 | 31 | void TextSection::setFormat(const QTextCharFormat &format) 32 | { 33 | Q_ASSERT(d.document); 34 | d.format = format; 35 | emit d.document->d->sectionFormatChanged(this); 36 | } 37 | 38 | QCursor TextSection::cursor() const 39 | { 40 | return d.cursor; 41 | } 42 | 43 | void TextSection::setCursor(const QCursor &cursor) 44 | { 45 | d.cursor = cursor; 46 | d.hasCursor = true; 47 | emit d.document->d->sectionCursorChanged(this); 48 | } 49 | 50 | void TextSection::resetCursor() 51 | { 52 | d.hasCursor = false; 53 | d.cursor = QCursor(); 54 | emit d.document->d->sectionCursorChanged(this); 55 | } 56 | 57 | bool TextSection::hasCursor() const 58 | { 59 | return d.hasCursor; 60 | } 61 | 62 | void TextSection::setPriority(int priority) 63 | { 64 | d.priority = priority; 65 | emit d.document->d->sectionFormatChanged(this); // ### it hasn't really but I to need it dirtied 66 | } 67 | -------------------------------------------------------------------------------- /textsection.h: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #ifndef TEXTSECTION_H 16 | #define TEXTSECTION_H 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | class TextDocument; 24 | class TextEdit; 25 | class TextSection 26 | { 27 | public: 28 | enum TextSectionOption { 29 | IncludePartial = 0x01 30 | }; 31 | Q_DECLARE_FLAGS(TextSectionOptions, TextSectionOption); 32 | 33 | ~TextSection(); 34 | QString text() const; 35 | int position() const { return d.position; } 36 | int size() const { return d.size; } 37 | QTextCharFormat format() const { return d.format; } 38 | void setFormat(const QTextCharFormat &format); 39 | QVariant data() const { return d.data; } 40 | void setData(const QVariant &data) { d.data = data; } 41 | TextDocument *document() const { return d.document; } 42 | TextEdit *textEdit() const { return d.textEdit; } 43 | QCursor cursor() const; 44 | void setCursor(const QCursor &cursor); 45 | void resetCursor(); 46 | bool hasCursor() const; 47 | int priority() const { return d.priority; } 48 | void setPriority(int priority); 49 | private: 50 | struct Data { 51 | Data(int p, int s, TextDocument *doc, const QTextCharFormat &f, const QVariant &d) 52 | : position(p), size(s), priority(0), document(doc), textEdit(0), format(f), data(d), hasCursor(false) 53 | {} 54 | int position, size, priority; 55 | TextDocument *document; 56 | TextEdit *textEdit; 57 | QTextCharFormat format; 58 | QVariant data; 59 | QCursor cursor; 60 | bool hasCursor; 61 | } d; 62 | 63 | TextSection(int pos, int size, TextDocument *doc, const QTextCharFormat &format, const QVariant &data) 64 | : d(pos, size, doc, format, data) 65 | {} 66 | 67 | friend class TextDocument; 68 | friend class TextDocumentPrivate; 69 | friend class TextEdit; 70 | }; 71 | 72 | Q_DECLARE_OPERATORS_FOR_FLAGS(TextSection::TextSectionOptions); 73 | 74 | 75 | #endif 76 | -------------------------------------------------------------------------------- /syntaxhighlighter.h: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #ifndef SYNTAXHIGHLIGHTER_H 16 | #define SYNTAXHIGHLIGHTER_H 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | class TextEdit; 27 | class TextLayout; 28 | class TextDocument; 29 | class SyntaxHighlighter : public QObject 30 | { 31 | Q_OBJECT 32 | public: 33 | SyntaxHighlighter(QObject *parent = 0); 34 | SyntaxHighlighter(TextEdit *parent); 35 | ~SyntaxHighlighter(); 36 | void setTextEdit(TextEdit *doc); 37 | TextEdit *textEdit() const; 38 | TextDocument *document() const; 39 | virtual void highlightBlock(const QString &text) = 0; 40 | QString currentBlock() const { return d->currentBlock; } 41 | void setFormat(int start, int count, const QTextCharFormat &format); 42 | void setFormat(int start, int count, const QColor &color); 43 | inline void setColor(int start, int count, const QColor &color) 44 | { setFormat(start, count, color); } 45 | inline void setBackground(int start, int count, const QBrush &brush) 46 | { QTextCharFormat format; format.setBackground(brush); setFormat(start, count, format); } 47 | inline void setBackgroundColor(int start, int count, const QColor &color) 48 | { setBackground(start, count, color); } 49 | void setFormat(int start, int count, const QFont &font); 50 | inline void setFont(int start, int count, const QFont &font) 51 | { setFormat(start, count, font); } 52 | QTextBlockFormat blockFormat() const; 53 | void setBlockFormat(const QTextBlockFormat &format); 54 | QTextCharFormat format(int pos) const; 55 | int previousBlockState() const; 56 | int currentBlockState() const; 57 | void setCurrentBlockState(int s); 58 | int currentBlockPosition() const; 59 | public Q_SLOTS: 60 | void rehighlight(); 61 | private: 62 | struct Private { 63 | Private() : textEdit(0), textLayout(0), previousBlockState(0), currentBlockState(0), 64 | currentBlockPosition(-1) {} 65 | TextEdit *textEdit; 66 | TextLayout *textLayout; 67 | int previousBlockState, currentBlockState, currentBlockPosition; 68 | QList formatRanges; 69 | QTextBlockFormat blockFormat; 70 | QString currentBlock; 71 | } *d; 72 | 73 | friend class TextEdit; 74 | friend class TextLayout; 75 | }; 76 | 77 | #endif 78 | -------------------------------------------------------------------------------- /syntaxhighlighter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | #include "syntaxhighlighter.h" 15 | #include "textedit.h" 16 | #include "textdocument.h" 17 | #include "textlayout_p.h" 18 | #include "textdocument_p.h" 19 | 20 | SyntaxHighlighter::SyntaxHighlighter(QObject *parent) 21 | : QObject(parent), d(new Private) 22 | { 23 | } 24 | 25 | SyntaxHighlighter::SyntaxHighlighter(TextEdit *parent) 26 | : QObject(parent), d(new Private) 27 | { 28 | if (parent) { 29 | parent->addSyntaxHighlighter(this); 30 | } 31 | } 32 | 33 | SyntaxHighlighter::~SyntaxHighlighter() 34 | { 35 | delete d; 36 | } 37 | 38 | void SyntaxHighlighter::setTextEdit(TextEdit *doc) 39 | { 40 | Q_ASSERT(doc); 41 | doc->addSyntaxHighlighter(this); 42 | } 43 | TextEdit *SyntaxHighlighter::textEdit() const 44 | { 45 | return d->textEdit; 46 | } 47 | 48 | 49 | TextDocument * SyntaxHighlighter::document() const 50 | { 51 | return d->textEdit ? d->textEdit->document() : 0; 52 | } 53 | 54 | void SyntaxHighlighter::rehighlight() 55 | { 56 | if (d->textEdit) { 57 | Q_ASSERT(d->textLayout); 58 | d->textLayout->layoutDirty = true; 59 | d->textEdit->viewport()->update(); 60 | } 61 | } 62 | 63 | void SyntaxHighlighter::setFormat(int start, int count, const QTextCharFormat &format) 64 | { 65 | ASSUME(d->textEdit); 66 | Q_ASSERT(start >= 0); 67 | Q_ASSERT(start + count <= d->currentBlock.size()); 68 | d->formatRanges.append(QTextLayout::FormatRange()); 69 | QTextLayout::FormatRange &range = d->formatRanges.last(); 70 | range.start = start; 71 | range.length = count; 72 | range.format = format; 73 | } 74 | 75 | void SyntaxHighlighter::setFormat(int start, int count, const QColor &color) 76 | { 77 | QTextCharFormat format; 78 | format.setForeground(color); 79 | setFormat(start, count, format); 80 | } 81 | 82 | void SyntaxHighlighter::setFormat(int start, int count, const QFont &font) 83 | { 84 | QTextCharFormat format; 85 | format.setFont(font); 86 | setFormat(start, count, format); 87 | } 88 | 89 | QTextCharFormat SyntaxHighlighter::format(int pos) const 90 | { 91 | QTextCharFormat ret; 92 | foreach(const QTextLayout::FormatRange &range, d->formatRanges) { 93 | if (range.start <= pos && range.start + range.length > pos) { 94 | ret.merge(range.format); 95 | } else if (range.start > pos) { 96 | break; 97 | } 98 | } 99 | return ret; 100 | } 101 | 102 | 103 | int SyntaxHighlighter::previousBlockState() const 104 | { 105 | return d->previousBlockState; 106 | } 107 | 108 | int SyntaxHighlighter::currentBlockState() const 109 | { 110 | return d->currentBlockState; 111 | } 112 | 113 | void SyntaxHighlighter::setCurrentBlockState(int s) 114 | { 115 | d->previousBlockState = d->currentBlockState; 116 | d->currentBlockState = s; // ### These don't entirely follow QSyntaxHighlighter's behavior 117 | } 118 | 119 | int SyntaxHighlighter::currentBlockPosition() const 120 | { 121 | return d->currentBlockPosition; 122 | } 123 | 124 | QTextBlockFormat SyntaxHighlighter::blockFormat() const 125 | { 126 | return d->blockFormat; 127 | } 128 | 129 | void SyntaxHighlighter::setBlockFormat(const QTextBlockFormat &format) 130 | { 131 | d->blockFormat = format; 132 | } 133 | -------------------------------------------------------------------------------- /tests/syntaxhighlighter/tst_syntaxhighlighter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | 17 | #include 18 | #include 19 | QT_FORWARD_DECLARE_CLASS(SyntaxHighlighter) 20 | 21 | //TESTED_CLASS= 22 | //TESTED_FILES= 23 | 24 | class tst_SyntaxHighlighter : public QObject 25 | { 26 | Q_OBJECT 27 | 28 | public: 29 | tst_SyntaxHighlighter(); 30 | virtual ~tst_SyntaxHighlighter(); 31 | 32 | private slots: 33 | void sanityCheck(); 34 | private: 35 | }; 36 | 37 | tst_SyntaxHighlighter::tst_SyntaxHighlighter() 38 | { 39 | } 40 | 41 | tst_SyntaxHighlighter::~tst_SyntaxHighlighter() 42 | { 43 | } 44 | 45 | class Highlighter : public SyntaxHighlighter 46 | { 47 | public: 48 | Highlighter(TextEdit *parent = 0) 49 | : SyntaxHighlighter(parent) 50 | {} 51 | void highlightBlock(const QString &string) 52 | { 53 | highlighted = string; 54 | } 55 | QString highlighted; 56 | }; 57 | 58 | void tst_SyntaxHighlighter::sanityCheck() 59 | { 60 | delete new Highlighter; // make sure null parent doesn't crash 61 | { 62 | TextEdit edit; 63 | SyntaxHighlighter *s = new Highlighter(&edit); 64 | QVERIFY(s->textEdit() == &edit); 65 | QCOMPARE(edit.syntaxHighlighters().size(), 1); 66 | QVERIFY(edit.syntaxHighlighters().first() == s); 67 | QVERIFY(edit.syntaxHighlighter() == s); 68 | delete s; 69 | QCOMPARE(edit.syntaxHighlighters().size(), 0); 70 | } 71 | { 72 | QPointer ptr; 73 | TextEdit *textEdit = new TextEdit; 74 | ptr = new Highlighter(textEdit); 75 | delete textEdit; 76 | QVERIFY(!ptr); 77 | } 78 | { 79 | QPointer ptr; 80 | TextEdit *textEdit = new TextEdit; 81 | ptr = new Highlighter; 82 | textEdit->addSyntaxHighlighter(ptr); 83 | delete textEdit; 84 | QVERIFY(ptr); 85 | delete ptr; 86 | } 87 | { 88 | TextEdit edit; 89 | QPointer s = new Highlighter; 90 | edit.setSyntaxHighlighter(s); 91 | QCOMPARE(edit.syntaxHighlighters().size(), 1); 92 | QVERIFY(edit.syntaxHighlighters().first() == s); 93 | QVERIFY(edit.syntaxHighlighter() == s); 94 | edit.setSyntaxHighlighter(s); 95 | QCOMPARE(edit.syntaxHighlighters().size(), 1); 96 | QVERIFY(edit.syntaxHighlighters().first() == s); 97 | QVERIFY(edit.syntaxHighlighter() == s); 98 | SyntaxHighlighter *s2 = new Highlighter; 99 | edit.setSyntaxHighlighter(s2); 100 | QCOMPARE(edit.syntaxHighlighters().size(), 1); 101 | QVERIFY(edit.syntaxHighlighters().first() == s2); 102 | QVERIFY(edit.syntaxHighlighter() == s2); 103 | QVERIFY(!s->textEdit()); 104 | } 105 | 106 | { 107 | TextEdit edit; 108 | QList list; 109 | for (int i=0; i<1024; ++i) { 110 | list.append(new Highlighter(&edit)); 111 | } 112 | QCOMPARE(list, edit.syntaxHighlighters()); 113 | } 114 | { 115 | TextEdit edit; 116 | Highlighter hl; 117 | hl.setTextEdit(&edit); 118 | } 119 | } 120 | 121 | QTEST_MAIN(tst_SyntaxHighlighter) 122 | #include "tst_syntaxhighlighter.moc" 123 | -------------------------------------------------------------------------------- /textcursor.h: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #ifndef TEXTCURSOR_H 16 | #define TEXTCURSOR_H 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | class TextEdit; 24 | class TextLayout; 25 | class TextDocument; 26 | class TextCursorSharedPrivate; 27 | class TextCursor 28 | { 29 | public: 30 | TextCursor(); 31 | explicit TextCursor(const TextDocument *document, int pos = 0, int anchor = -1); 32 | explicit TextCursor(const TextEdit *document, int pos = 0, int anchor = -1); 33 | TextCursor(const TextCursor &cursor); 34 | TextCursor &operator=(const TextCursor &other); 35 | ~TextCursor(); 36 | 37 | TextDocument *document() const; 38 | bool isNull() const; 39 | inline bool isValid() const { return !isNull(); } 40 | 41 | enum MoveMode { 42 | MoveAnchor, 43 | KeepAnchor 44 | }; 45 | 46 | void setPosition(int pos, MoveMode mode = MoveAnchor); 47 | int position() const; 48 | 49 | void setSelection(int pos, int length); // can be negative 50 | 51 | int viewportWidth() const; 52 | void setViewportWidth(int width); 53 | 54 | int anchor() const; 55 | 56 | void insertText(const QString &text); 57 | 58 | QChar cursorCharacter() const; 59 | QString cursorLine() const; 60 | int lineHeight() const; 61 | 62 | QString wordUnderCursor() const; 63 | QString paragraphUnderCursor() const; 64 | 65 | enum MoveOperation { 66 | NoMove, 67 | Start, 68 | Up, 69 | StartOfLine, 70 | StartOfBlock, 71 | StartOfWord, 72 | PreviousBlock, 73 | PreviousCharacter, 74 | PreviousWord, 75 | Left, 76 | WordLeft, 77 | End, 78 | Down, 79 | EndOfLine, 80 | EndOfWord, 81 | EndOfBlock, 82 | NextBlock, 83 | NextCharacter, 84 | NextWord, 85 | Right, 86 | WordRight 87 | }; 88 | 89 | bool movePosition(MoveOperation op, MoveMode = MoveAnchor, int n = 1); 90 | 91 | void deleteChar(); 92 | void deletePreviousChar(); 93 | 94 | enum SelectionType { 95 | WordUnderCursor, 96 | LineUnderCursor, 97 | BlockUnderCursor 98 | }; 99 | 100 | void select(SelectionType selection); 101 | 102 | bool hasSelection() const; 103 | void removeSelectedText(); 104 | void clearSelection(); 105 | int selectionStart() const; 106 | int selectionEnd() const; 107 | int selectionSize() const; 108 | inline int selectionLength() const { return selectionSize(); } 109 | 110 | QString selectedText() const; 111 | 112 | bool atBlockStart() const; 113 | bool atBlockEnd() const; 114 | bool atStart() const; 115 | bool atEnd() const; 116 | 117 | bool operator!=(const TextCursor &rhs) const; 118 | bool operator<(const TextCursor &rhs) const; 119 | bool operator<=(const TextCursor &rhs) const; 120 | bool operator==(const TextCursor &rhs) const; 121 | bool operator>=(const TextCursor &rhs) const; 122 | bool operator>(const TextCursor &rhs) const; 123 | 124 | bool isCopyOf(const TextCursor &other) const; 125 | 126 | int columnNumber() const; 127 | int lineNumber() const; 128 | private: 129 | bool cursorMoveKeyEvent(QKeyEvent *e); 130 | void cursorChanged(bool ensureCursorVisible); 131 | void detach(); 132 | bool ref(); 133 | bool deref(); 134 | 135 | TextCursorSharedPrivate *d; 136 | TextEdit *textEdit; 137 | friend class TextEdit; 138 | friend class TextLayoutCacheManager; 139 | friend class TextDocument; 140 | friend class TextDocumentPrivate; 141 | }; 142 | 143 | QDebug operator<<(QDebug dbg, const TextCursor &cursor); 144 | 145 | #endif 146 | -------------------------------------------------------------------------------- /textlayout_p.h: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #ifndef TEXTLAYOUT_P_H 16 | #define TEXTLAYOUT_P_H 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #ifndef QT_NO_DEBUG_STREAM 26 | #include 27 | #endif 28 | #include "textdocument.h" 29 | #include "textedit.h" 30 | #include "syntaxhighlighter.h" 31 | #include "weakpointer.h" 32 | 33 | #ifndef QT_NO_DEBUG_STREAM 34 | QDebug &operator<<(QDebug &str, const QTextLine &line); 35 | #endif 36 | 37 | class TextDocumentBuffer 38 | { 39 | public: 40 | TextDocumentBuffer(TextDocument *doc) : document(doc), bufferPosition(0) {} 41 | virtual ~TextDocumentBuffer() {} 42 | 43 | inline QString bufferRead(int from, int size) const 44 | { 45 | Q_ASSERT(document); 46 | if (from < bufferPosition || from + size > bufferPosition + buffer.size()) { 47 | return document->read(from, size); 48 | } 49 | return buffer.mid(from - bufferPosition, size); 50 | } 51 | inline QChar bufferReadCharacter(int index) const // document coordinates 52 | { 53 | Q_ASSERT(document); 54 | if (index >= bufferPosition && index < bufferPosition + buffer.size()) { 55 | return buffer.at(index - bufferPosition); 56 | } else { 57 | Q_ASSERT(index >= 0 && index < document->documentSize()); // what if index == documentSize? 58 | return document->readCharacter(index); 59 | } 60 | } 61 | 62 | TextDocument *document; 63 | int bufferPosition; 64 | QString buffer; 65 | }; 66 | 67 | class TextEdit; 68 | class TextLayout : public TextDocumentBuffer 69 | { 70 | public: 71 | enum { MinimumBufferSize = 5000, LeftMargin = 3 }; 72 | TextLayout(TextDocument *doc = 0) 73 | : TextDocumentBuffer(doc), textEdit(0), 74 | viewportPosition(0), layoutEnd(-1), viewport(-1), 75 | visibleLines(-1), lastVisibleCharacter(-1), lastBottomMargin(0), 76 | widest(-1), maxViewportPosition(0), layoutDirty(true), sectionsDirty(true), 77 | lineBreaking(true), suppressTextEditUpdates(false) 78 | { 79 | } 80 | 81 | virtual ~TextLayout() 82 | { 83 | qDeleteAll(textLayouts); 84 | qDeleteAll(unusedTextLayouts); 85 | } 86 | 87 | TextEdit *textEdit; 88 | QList syntaxHighlighters; 89 | int viewportPosition, layoutEnd, viewport, visibleLines, 90 | lastVisibleCharacter, lastBottomMargin, widest, maxViewportPosition; 91 | bool layoutDirty, sectionsDirty, lineBreaking, suppressTextEditUpdates; 92 | QList textLayouts, unusedTextLayouts; 93 | QHash blockFormats; 94 | QList extraSelections; 95 | QList > lines; // int is start position of line in document coordinates 96 | QRect contentRect; // contentRect means the laid out area, not just the area currently visible 97 | QList sections; // these are all the sections in the buffer. Some might be before the current viewport 98 | QFont font; 99 | 100 | QList relayoutCommon(); // should maybe be smarter about MinimumScreenSize. Detect it based on font and viewport size 101 | void relayoutByPosition(int size); 102 | void relayoutByGeometry(int height); 103 | virtual void relayout(); 104 | 105 | int viewportWidth() const; 106 | 107 | int doLayout(int index, QList *sections); 108 | 109 | QTextLine lineForPosition(int pos, int *offsetInLine = 0, 110 | int *lineIndex = 0, bool *lastLine = 0) const; 111 | QTextLayout *layoutForPosition(int pos, int *offset = 0, int *index = 0) const; 112 | 113 | int textPositionAt(const QPoint &pos) const; 114 | inline int bufferOffset() const { return viewportPosition - bufferPosition; } 115 | 116 | QString dump() const; 117 | 118 | enum Direction { 119 | Forward = 0, 120 | Backward = TextDocument::FindBackward 121 | }; 122 | void updateViewportPosition(int pos, Direction direction); 123 | }; 124 | 125 | #endif 126 | -------------------------------------------------------------------------------- /textedit_p.h: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #ifndef TEXTEDIT_P_H 16 | #define TEXTEDIT_P_H 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include "textlayout_p.h" 24 | #include "textdocument_p.h" 25 | #include "textcursor.h" 26 | #include "textedit.h" 27 | 28 | struct DocumentCommand; 29 | struct CursorData { 30 | int position, anchor; 31 | }; 32 | 33 | class TextEditPrivate : public QObject, public TextLayout 34 | { 35 | Q_OBJECT 36 | public: 37 | TextEditPrivate(TextEdit *qptr) 38 | : requestedScrollBarPosition(-1), lastRequestedScrollBarPosition(-1), cursorWidth(1), 39 | sectionCount(0), maximumSizeCopy(50000), pendingTimeOut(-1), autoScrollLines(0), 40 | readOnly(false), cursorVisible(false), blockScrollBarUpdate(false), 41 | updateScrollBarPageStepPending(true), inMouseEvent(false), sectionPressed(0), 42 | pendingScrollBarUpdate(false), sectionCursor(0) 43 | { 44 | textEdit = qptr; 45 | } 46 | 47 | bool canInsertFromMimeData(const QMimeData *data) const; 48 | void updateHorizontalPosition(); 49 | void updateScrollBarPosition(); 50 | void updateScrollBarPageStep(); 51 | void scrollLines(int lines); 52 | void timerEvent(QTimerEvent *e); 53 | void updateCursorPosition(const QPoint &pos); 54 | int findLastPageSize() const; 55 | bool atBeginning() const { return viewportPosition == 0; } 56 | bool atEnd() const { return textEdit->verticalScrollBar()->value() == textEdit->verticalScrollBar()->maximum(); } 57 | bool dirtyForSection(TextSection *section); 58 | void updateCopyAndCutEnabled(); 59 | bool isSectionOnScreen(const TextSection *section) const; 60 | void cursorMoveKeyEventReadOnly(QKeyEvent *e); 61 | virtual void relayout(); // from TextLayout 62 | 63 | int requestedScrollBarPosition, lastRequestedScrollBarPosition, cursorWidth, sectionCount, 64 | maximumSizeCopy, pendingTimeOut, autoScrollLines; 65 | bool readOnly, cursorVisible, blockScrollBarUpdate, updateScrollBarPageStepPending, inMouseEvent; 66 | QBasicTimer autoScrollTimer, cursorBlinkTimer; 67 | QAction *actions[TextEdit::SelectAllAction]; 68 | TextSection *sectionPressed; 69 | TextCursor textCursor, dragOverrideCursor; 70 | QBasicTimer tripleClickTimer; 71 | bool pendingScrollBarUpdate; 72 | QCursor *sectionCursor; 73 | QPoint lastHoverPos, lastMouseMove; 74 | QHash > undoRedoCommands; 75 | public slots: 76 | void onSyntaxHighlighterDestroyed(QObject *o); 77 | void onSelectionChanged(); 78 | void onTextSectionAdded(TextSection *section); 79 | void onTextSectionRemoved(TextSection *section); 80 | void onTextSectionFormatChanged(TextSection *section); 81 | void onTextSectionCursorChanged(TextSection *section); 82 | void updateScrollBar(); 83 | void onDocumentDestroyed(); 84 | void onDocumentSizeChanged(int size); 85 | void onDocumentCommandInserted(DocumentCommand *cmd); 86 | void onDocumentCommandFinished(DocumentCommand *cmd); 87 | void onDocumentCommandRemoved(DocumentCommand *cmd); 88 | void onDocumentCommandTriggered(DocumentCommand *cmd, bool undo); 89 | void onScrollBarValueChanged(int value); 90 | void onScrollBarActionTriggered(int action); 91 | void onCharactersAddedOrRemoved(int index, int count); 92 | }; 93 | 94 | class DebugWindow : public QWidget 95 | { 96 | public: 97 | DebugWindow(TextEditPrivate *p) 98 | : priv(p) 99 | { 100 | } 101 | 102 | void paintEvent(QPaintEvent *) 103 | { 104 | if (priv->lines.isEmpty()) 105 | return; 106 | 107 | QPainter p(this); 108 | p.fillRect(QRect(0, pixels(priv->viewportPosition), width(), 109 | pixels(priv->lines.last().first + 110 | priv->lines.last().second.textLength())), 111 | Qt::black); 112 | p.fillRect(QRect(0, pixels(priv->viewportPosition), width(), pixels(priv->layoutEnd)), Qt::red); 113 | } 114 | 115 | int pixels(int pos) const 116 | { 117 | double fraction = double(pos) / double(priv->document->documentSize()); 118 | return int(double(height()) * fraction); 119 | } 120 | private: 121 | TextEditPrivate *priv; 122 | }; 123 | 124 | #endif 125 | -------------------------------------------------------------------------------- /textedit.h: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #ifndef TEXTEDIT_H 16 | #define TEXTEDIT_H 17 | 18 | #include 19 | #include 20 | #include "textdocument.h" 21 | #include "textcursor.h" 22 | #include "textsection.h" 23 | #include "syntaxhighlighter.h" 24 | 25 | class TextEditPrivate; 26 | class TextEdit : public QAbstractScrollArea 27 | { 28 | Q_OBJECT 29 | Q_PROPERTY(int cursorWidth READ cursorWidth WRITE setCursorWidth) 30 | Q_PROPERTY(bool readOnly READ readOnly WRITE setReadOnly) 31 | Q_PROPERTY(bool cursorVisible READ cursorVisible WRITE setCursorVisible) 32 | Q_PROPERTY(QString selectedText READ selectedText) 33 | Q_PROPERTY(bool undoAvailable READ isUndoAvailable NOTIFY undoAvailableChanged) 34 | Q_PROPERTY(bool redoAvailable READ isRedoAvailable NOTIFY redoAvailableChanged) 35 | Q_PROPERTY(int maximumSizeCopy READ maximumSizeCopy WRITE setMaximumSizeCopy) 36 | Q_PROPERTY(bool lineBreaking READ lineBreaking WRITE setLineBreaking) 37 | 38 | public: 39 | TextEdit(QWidget *parent = 0); 40 | ~TextEdit(); 41 | 42 | TextDocument *document() const; 43 | void setDocument(TextDocument *doc); 44 | 45 | int cursorWidth() const; 46 | void setCursorWidth(int cc); 47 | 48 | struct ExtraSelection 49 | { 50 | TextCursor cursor; 51 | QTextCharFormat format; 52 | }; 53 | 54 | void setExtraSelections(const QList &selections); 55 | QList extraSelections() const; 56 | 57 | void setSyntaxHighlighter(SyntaxHighlighter *h); 58 | inline SyntaxHighlighter *syntaxHighlighter() const { return syntaxHighlighters().value(0); } 59 | 60 | QList syntaxHighlighters() const; 61 | void addSyntaxHighlighter(SyntaxHighlighter *highlighter); 62 | void takeSyntaxHighlighter(SyntaxHighlighter *highlighter); 63 | void removeSyntaxHighlighter(SyntaxHighlighter *highlighter); 64 | void clearSyntaxHighlighters(); 65 | 66 | bool load(QIODevice *device, TextDocument::DeviceMode mode = TextDocument::Sparse, QTextCodec *codec = 0); 67 | bool load(const QString &fileName, TextDocument::DeviceMode mode = TextDocument::Sparse, QTextCodec *codec = 0); 68 | void paintEvent(QPaintEvent *e); 69 | void scrollContentsBy(int dx, int dy); 70 | 71 | bool moveCursorPosition(TextCursor::MoveOperation op, TextCursor::MoveMode = TextCursor::MoveAnchor, int n = 1); 72 | void setCursorPosition(int pos, TextCursor::MoveMode mode = TextCursor::MoveAnchor); 73 | 74 | int viewportPosition() const; 75 | int cursorPosition() const; 76 | 77 | int textPositionAt(const QPoint &pos) const; 78 | 79 | bool readOnly() const; 80 | void setReadOnly(bool rr); 81 | 82 | bool lineBreaking() const; 83 | void setLineBreaking(bool lb); 84 | 85 | int maximumSizeCopy() const; 86 | void setMaximumSizeCopy(int max); 87 | 88 | QRect cursorBlockRect(const TextCursor &cursor) const; 89 | QRect cursorRect(const TextCursor &cursor) const; 90 | 91 | int lineNumber(int position) const; 92 | int columnNumber(int position) const; 93 | int lineNumber(const TextCursor &cursor) const; 94 | int columnNumber(const TextCursor &cursor) const; 95 | 96 | bool cursorVisible() const; 97 | void setCursorVisible(bool cc); 98 | 99 | QString selectedText() const; 100 | bool hasSelection() const; 101 | 102 | bool save(QIODevice *device); 103 | bool save(); 104 | bool save(const QString &file); 105 | 106 | void setText(const QString &text); 107 | QString read(int pos, int size) const; 108 | QChar readCharacter(int index) const; 109 | 110 | void insert(int pos, const QString &text); 111 | void remove(int from, int size); 112 | 113 | TextCursor &textCursor(); 114 | const TextCursor &textCursor() const; 115 | void setTextCursor(const TextCursor &textCursor); 116 | 117 | TextCursor cursorForPosition(const QPoint &pos) const; 118 | 119 | TextSection *sectionAt(const QPoint &pos) const; 120 | 121 | QList sections(int from = 0, int size = -1, TextSection::TextSectionOptions opt = 0) const; 122 | inline TextSection *sectionAt(int pos) const { return sections(pos, 1, TextSection::IncludePartial).value(0); } 123 | TextSection *insertTextSection(int pos, int size, const QTextCharFormat &format = QTextCharFormat(), 124 | const QVariant &data = QVariant()); 125 | 126 | void ensureCursorVisible(const TextCursor &cursor, int linesMargin = 0); 127 | bool isUndoAvailable() const; 128 | bool isRedoAvailable() const; 129 | 130 | enum ActionType { 131 | CopyAction, 132 | PasteAction, 133 | CutAction, 134 | UndoAction, 135 | RedoAction, 136 | SelectAllAction 137 | }; 138 | QAction *action(ActionType type) const; 139 | public slots: 140 | void ensureCursorVisible(); 141 | void append(const QString &text); 142 | void removeSelectedText(); 143 | void copy(QClipboard::Mode mode = QClipboard::Clipboard); 144 | void paste(QClipboard::Mode mode = QClipboard::Clipboard); 145 | void cut(); 146 | void undo(); 147 | void redo(); 148 | void selectAll(); 149 | void clearSelection(); 150 | signals: 151 | void copyAvailable(bool on); 152 | void textChanged(); 153 | void selectionChanged(); 154 | void cursorPositionChanged(int pos); 155 | void sectionClicked(TextSection *section, const QPoint &pos); 156 | void undoAvailableChanged(bool on); 157 | void redoAvailableChanged(bool on); 158 | protected: 159 | virtual void paste(int position, QClipboard::Mode mode); 160 | virtual void changeEvent(QEvent *e); 161 | virtual void keyPressEvent(QKeyEvent *e); 162 | virtual void keyReleaseEvent(QKeyEvent *e); 163 | virtual void wheelEvent(QWheelEvent *e); 164 | virtual void mousePressEvent(QMouseEvent *e); 165 | virtual void mouseDoubleClickEvent(QMouseEvent *); 166 | virtual void mouseMoveEvent(QMouseEvent *e); 167 | virtual void mouseReleaseEvent(QMouseEvent *e); 168 | virtual void resizeEvent(QResizeEvent *e); 169 | #if 0 // ### not done yet 170 | virtual void dragEnterEvent(QDragEnterEvent *e); 171 | virtual void dragMoveEvent(QDragMoveEvent *e); 172 | virtual void dropEvent(QDropEvent *e); 173 | #endif 174 | private: 175 | TextEditPrivate *d; 176 | friend class TextLayoutCacheManager; 177 | friend class TextEditPrivate; 178 | friend class TextCursor; 179 | }; 180 | 181 | #endif 182 | -------------------------------------------------------------------------------- /textdocument.h: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #ifndef TEXTDOCUMENT_H 16 | #define TEXTDOCUMENT_H 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include "textcursor.h" 30 | #include "textsection.h" 31 | 32 | class Chunk; 33 | class TextDocumentPrivate; 34 | class TextDocument : public QObject 35 | { 36 | Q_OBJECT 37 | Q_PROPERTY(int documentSize READ documentSize) 38 | Q_PROPERTY(int chunkCount READ chunkCount) 39 | Q_PROPERTY(int instantiatedChunkCount READ instantiatedChunkCount) 40 | Q_PROPERTY(int swappedChunkCount READ swappedChunkCount) 41 | Q_PROPERTY(int chunkSize READ chunkSize WRITE setChunkSize) 42 | Q_PROPERTY(bool undoRedoEnabled READ isUndoRedoEnabled WRITE setUndoRedoEnabled) 43 | Q_PROPERTY(bool modified READ isModified WRITE setModified DESIGNABLE false) 44 | Q_PROPERTY(bool undoAvailable READ isUndoAvailable NOTIFY undoAvailableChanged) 45 | Q_PROPERTY(bool redoAvailable READ isRedoAvailable NOTIFY redoAvailableChanged) 46 | Q_PROPERTY(bool collapseInsertUndo READ collapseInsertUndo WRITE setCollapseInsertUndo) 47 | Q_ENUMS(DeviceMode) 48 | Q_FLAGS(Options) 49 | Q_FLAGS(FindMode) 50 | 51 | public: 52 | TextDocument(QObject *parent = 0); 53 | ~TextDocument(); 54 | 55 | enum DeviceMode { 56 | Sparse, 57 | LoadAll 58 | }; 59 | 60 | enum Option { 61 | NoOptions = 0x0000, 62 | SwapChunks = 0x0001, 63 | KeepTemporaryFiles = 0x0002, 64 | ConvertCarriageReturns = 0x0004, // incompatible with Sparse and must be set before loading 65 | AutoDetectCarriageReturns = 0x0010, 66 | NoImplicitLoadAll = 0x0020, 67 | Locking = 0x0040, 68 | DefaultOptions = AutoDetectCarriageReturns 69 | }; 70 | Q_DECLARE_FLAGS(Options, Option); 71 | 72 | Options options() const; 73 | void setOptions(Options opt); 74 | inline void setOption(Option opt, bool on = true) { setOptions(on ? (options() | opt) : (options() &= ~opt)); } 75 | 76 | inline bool load(QIODevice *device, DeviceMode mode, const QByteArray &codecName) 77 | { return load(device, mode, QTextCodec::codecForName(codecName)); } 78 | inline bool load(const QString &fileName, DeviceMode mode, const QByteArray &codecName) 79 | { return load(fileName, mode, QTextCodec::codecForName(codecName)); } 80 | bool load(QIODevice *device, DeviceMode mode = Sparse, QTextCodec *codec = 0); 81 | bool load(const QString &fileName, DeviceMode mode = Sparse, QTextCodec *codec = 0); 82 | 83 | void clear(); 84 | DeviceMode deviceMode() const; 85 | 86 | QTextCodec *textCodec() const; 87 | 88 | void setText(const QString &text); 89 | QString read(int pos, int size) const; 90 | QStringRef readRef(int pos, int size) const; 91 | QChar readCharacter(int index) const; 92 | bool save(const QString &file); 93 | bool save(QIODevice *device); 94 | bool save(); 95 | int documentSize() const; 96 | int chunkCount() const; 97 | int instantiatedChunkCount() const; 98 | int swappedChunkCount() const; 99 | 100 | void lockForRead(); 101 | void lockForWrite(); 102 | bool tryLockForRead(); 103 | bool tryLockForWrite(); 104 | void unlock(); 105 | 106 | enum FindModeFlag { 107 | FindNone = 0x00000, 108 | FindBackward = 0x00001, 109 | FindCaseSensitively = 0x00002, 110 | FindWholeWords = 0x00004, 111 | FindAllowInterrupt = 0x00008, 112 | FindWrap = 0x00010, 113 | FindAll = 0x00020 114 | }; 115 | Q_DECLARE_FLAGS(FindMode, FindModeFlag); 116 | 117 | int chunkSize() const; 118 | void setChunkSize(int pos); 119 | 120 | bool isUndoRedoEnabled() const; 121 | void setUndoRedoEnabled(bool enable); 122 | 123 | QIODevice *device() const; 124 | 125 | TextCursor find(const QRegExp &rx, const TextCursor &cursor, FindMode flags = 0) const; 126 | TextCursor find(const QString &ba, const TextCursor &cursor, FindMode flags = 0) const; 127 | TextCursor find(const QChar &ch, const TextCursor &cursor, FindMode flags = 0) const; 128 | 129 | inline TextCursor find(const QRegExp &rx, int pos = 0, FindMode flags = 0) const 130 | { return find(rx, TextCursor(this, pos), flags); } 131 | inline TextCursor find(const QString &ba, int pos = 0, FindMode flags = 0) const 132 | { return find(ba, TextCursor(this, pos), flags); } 133 | inline TextCursor find(const QChar &ch, int pos = 0, FindMode flags = 0) const 134 | { return find(ch, TextCursor(this, pos), flags); } 135 | 136 | 137 | bool insert(int pos, const QString &ba); 138 | inline bool insert(int pos, const QChar &ba) { return insert(pos, QString(ba)); } 139 | void remove(int pos, int size); 140 | 141 | QList sections(int from = 0, int size = -1, TextSection::TextSectionOptions opt = 0) const; 142 | inline TextSection *sectionAt(int pos) const { return sections(pos, 1, TextSection::IncludePartial).value(0); } 143 | TextSection *insertTextSection(int pos, int size, const QTextCharFormat &format = QTextCharFormat(), 144 | const QVariant &data = QVariant()); 145 | void insertTextSection(TextSection *section); 146 | void takeTextSection(TextSection *section); 147 | int currentMemoryUsage() const; 148 | 149 | bool isUndoAvailable() const; 150 | bool isRedoAvailable() const; 151 | 152 | bool collapseInsertUndo() const; 153 | void setCollapseInsertUndo(bool collapse); 154 | 155 | bool isModified() const; 156 | 157 | int lineNumber(int position) const; 158 | int columnNumber(int position) const; 159 | int lineNumber(const TextCursor &cursor) const; 160 | int columnNumber(const TextCursor &cursor) const; 161 | virtual bool isWordCharacter(const QChar &ch, int index) const; 162 | public slots: 163 | inline bool append(const QString &ba) { return insert(documentSize(), ba); } 164 | inline bool append(const QChar &ba) { return append(QString(ba)); } 165 | void setModified(bool modified); 166 | void undo(); 167 | void redo(); 168 | bool abortSave(); 169 | bool abortFind() const; 170 | signals: 171 | void entryFound(const TextCursor &cursor) const; 172 | void textChanged(); 173 | void sectionAdded(TextSection *section); 174 | void sectionRemoved(TextSection *removed); 175 | void charactersAdded(int from, int count); 176 | void charactersRemoved(int from, int count); 177 | void saveProgress(qreal progress); 178 | void findProgress(qreal progress, int position) const; 179 | void documentSizeChanged(int size); 180 | void undoAvailableChanged(bool on); 181 | void redoAvailableChanged(bool on); 182 | void modificationChanged(bool modified); 183 | protected: 184 | virtual QString swapFileName(Chunk *chunk); 185 | private: 186 | TextDocumentPrivate *d; 187 | friend class TextEdit; 188 | friend class TextCursor; 189 | friend class TextDocumentPrivate; 190 | friend class TextLayout; 191 | friend class TextSection; 192 | }; 193 | 194 | Q_DECLARE_OPERATORS_FOR_FLAGS(TextDocument::FindMode); 195 | Q_DECLARE_OPERATORS_FOR_FLAGS(TextDocument::Options); 196 | 197 | #endif 198 | -------------------------------------------------------------------------------- /textcursor_p.h: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #ifndef TEXTCURSOR_P_H 16 | #define TEXTCURSOR_P_H 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include "textlayout_p.h" 26 | #include "textedit.h" 27 | #include "textedit_p.h" 28 | 29 | static inline bool match(const TextCursor &cursor, const TextLayout *layout, int lines) 30 | { 31 | Q_ASSERT(cursor.document() == layout->document); 32 | if (cursor.viewportWidth() != -1 && cursor.viewportWidth() != layout->viewport) { 33 | return false; 34 | } 35 | if (layout->viewportPosition > cursor.position() 36 | || layout->layoutEnd < cursor.position()) { 37 | return false; 38 | } 39 | int index = -1; 40 | if (!layout->layoutForPosition(cursor.position(), 0, &index)) { 41 | // ### need an interface for this if I am going to have a mode 42 | // ### that doesn't break lines 43 | return false; 44 | } 45 | 46 | if (index < lines && layout->viewportPosition > 0) { 47 | return false; // need more margin before the cursor 48 | } else if (layout->textLayouts.size() - index - 1 < lines && layout->layoutEnd < layout->document->documentSize()) { 49 | return false; // need more margin after cursor 50 | } 51 | return true; 52 | } 53 | 54 | class TextLayoutCacheManager : public QObject 55 | { 56 | Q_OBJECT 57 | public: 58 | static TextLayoutCacheManager *instance() 59 | { 60 | static TextLayoutCacheManager *inst = new TextLayoutCacheManager(QCoreApplication::instance()); 61 | return inst; 62 | } 63 | // ### this class doesn't react to TextSections added or removed. I 64 | // ### don't think it needs to since it's only being used for 65 | // ### cursor movement which shouldn't be impacted by these things 66 | 67 | static TextLayout *requestLayout(const TextCursor &cursor, int margin) 68 | { 69 | Q_ASSERT(cursor.document()); 70 | if (cursor.textEdit && match(cursor, cursor.textEdit->d, margin)) { 71 | return cursor.textEdit->d; 72 | } 73 | 74 | TextDocument *doc = cursor.document(); 75 | Q_ASSERT(doc); 76 | QList &layouts = instance()->cache[doc]; 77 | Q_ASSERT(cursor.document()); 78 | foreach(TextLayout *l, layouts) { 79 | if (match(cursor, l, margin)) { 80 | return l; 81 | } 82 | } 83 | if (layouts.size() < instance()->maxLayouts) { 84 | if (layouts.isEmpty()) { 85 | connect(cursor.document(), SIGNAL(charactersAdded(int, int)), 86 | instance(), SLOT(onCharactersAddedOrRemoved(int))); 87 | connect(cursor.document(), SIGNAL(charactersRemoved(int, int)), 88 | instance(), SLOT(onCharactersAddedOrRemoved(int))); 89 | connect(cursor.document(), SIGNAL(destroyed(QObject*)), 90 | instance(), SLOT(onDocumentDestroyed(QObject*))); 91 | } 92 | layouts.append(new TextLayout(doc)); 93 | } 94 | TextLayout *l = layouts.last(); 95 | l->viewport = cursor.viewportWidth(); 96 | if (l->viewport == -1) 97 | l->viewport = INT_MAX - 1024; // prevent overflow in comparisons. 98 | if (cursor.textEdit) { 99 | l->textEdit = cursor.textEdit; 100 | l->suppressTextEditUpdates = true; 101 | l->lineBreaking = cursor.textEdit->lineBreaking(); 102 | l->sections = cursor.textEdit->d->sections; 103 | l->font = cursor.textEdit->font(); 104 | l->syntaxHighlighters = cursor.textEdit->syntaxHighlighters(); 105 | // l->extraSelections = cursor.textEdit->extraSelections(); 106 | // ### can the extra selections impact layout? If so they 107 | // ### need to be in the actual textLayout shouldn't need 108 | // ### to care about the actual selection 109 | } 110 | int startPos = (cursor.position() == 0 111 | ? 0 112 | : qMax(0, doc->find(QLatin1Char('\n'), cursor.position() - 1, TextDocument::FindBackward).anchor())); 113 | // We start at the beginning of the current line 114 | int linesAbove = margin; 115 | if (startPos > 0) { 116 | while (linesAbove > 0) { 117 | const TextCursor c = doc->find(QLatin1Char('\n'), startPos - 1, TextDocument::FindBackward); 118 | if (c.isNull()) { 119 | startPos = 0; 120 | break; 121 | } 122 | startPos = c.anchor(); 123 | ASSUME(c.anchor() == 0 || c.cursorCharacter() == QLatin1Char('\n')); 124 | ASSUME(c.anchor() == 0 || doc->readCharacter(c.anchor()) == QLatin1Char('\n')); 125 | ASSUME(c.cursorCharacter() == doc->readCharacter(c.anchor())); 126 | 127 | --linesAbove; 128 | } 129 | } 130 | 131 | int linesBelow = margin; 132 | int endPos = cursor.position(); 133 | if (endPos < doc->documentSize()) { 134 | while (linesBelow > 0) { 135 | const TextCursor c = doc->find(QLatin1Char('\n'), endPos + 1); 136 | if (c.isNull()) { 137 | endPos = doc->documentSize(); 138 | break; 139 | } 140 | endPos = c.anchor(); 141 | --linesBelow; 142 | } 143 | } 144 | if (startPos > 0) 145 | ++startPos; // in this case startPos points to the newline before it 146 | l->viewportPosition = startPos; 147 | l->layoutDirty = true; 148 | ASSUME(l->viewportPosition == 0 || doc->readCharacter(l->viewportPosition - 1) == QLatin1Char('\n')); 149 | l->relayoutByPosition(endPos - startPos + 100); // ### fudged a couple of lines likely 150 | ASSUME(l->viewportPosition < l->layoutEnd 151 | || (l->viewportPosition == l->layoutEnd && l->viewportPosition == doc->documentSize())); 152 | ASSUME(l->textLayouts.size() > margin * 2 || l->viewportPosition == 0 || l->layoutEnd == doc->documentSize()); 153 | return l; 154 | } 155 | private slots: 156 | void onDocumentDestroyed(QObject *o) 157 | { 158 | qDeleteAll(cache.take(static_cast(o))); 159 | } 160 | 161 | void onCharactersAddedOrRemoved(int pos) 162 | { 163 | QList &layouts = cache[qobject_cast(sender())]; 164 | ASSUME(!layouts.isEmpty()); 165 | for (int i=layouts.size() - 1; i>=0; --i) { 166 | TextLayout *l = layouts.at(i); 167 | if (pos <= l->layoutEnd) { 168 | delete l; 169 | layouts.removeAt(i); 170 | } 171 | } 172 | if (layouts.isEmpty()) { 173 | disconnect(sender(), 0, this, 0); 174 | cache.remove(qobject_cast(sender())); 175 | } 176 | } 177 | private: 178 | TextLayoutCacheManager(QObject *parent) 179 | : QObject(parent) 180 | { 181 | maxLayouts = qMax(1, qgetenv("TEXTCURSOR_MAX_CACHED_TEXTLAYOUTS").toInt()); 182 | } 183 | 184 | int maxLayouts; 185 | QHash > cache; 186 | }; 187 | 188 | class TextLayout; 189 | struct TextCursorSharedPrivate 190 | { 191 | public: 192 | TextCursorSharedPrivate() : ref(1), position(-1), anchor(-1), 193 | overrideColumn(-1), viewportWidth(-1), 194 | document(0) 195 | {} 196 | 197 | ~TextCursorSharedPrivate() 198 | { 199 | ASSUME(ref == 0); 200 | } 201 | 202 | void invalidate() 203 | { 204 | 205 | } 206 | 207 | mutable QAtomicInt ref; 208 | int position, anchor, overrideColumn, viewportWidth; 209 | 210 | TextDocument *document; 211 | }; 212 | 213 | 214 | #endif 215 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /tests/textcursor/tst_textcursor.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | 17 | #include 18 | #include 19 | QT_FORWARD_DECLARE_CLASS(TextCursor) 20 | 21 | //TESTED_CLASS= 22 | //TESTED_FILES= 23 | 24 | class tst_TextCursor : public QObject 25 | { 26 | Q_OBJECT 27 | 28 | public: 29 | tst_TextCursor(); 30 | virtual ~tst_TextCursor(); 31 | 32 | private slots: 33 | void operatorEquals_data(); 34 | void operatorEquals(); 35 | void setPosition(); 36 | void setPosition_data(); 37 | void movePosition_data(); 38 | void movePosition(); 39 | void invalidPosition_data(); 40 | void invalidPosition(); 41 | private: 42 | TextDocument document; 43 | }; 44 | 45 | Q_DECLARE_METATYPE(TextCursor *); 46 | Q_DECLARE_METATYPE(TextCursor::MoveOperation); 47 | Q_DECLARE_METATYPE(TextCursor::MoveMode); 48 | 49 | tst_TextCursor::tst_TextCursor() 50 | { 51 | document.append("This is some text"); 52 | } 53 | 54 | tst_TextCursor::~tst_TextCursor() 55 | { 56 | } 57 | 58 | void tst_TextCursor::setPosition_data() 59 | { 60 | QTest::addColumn("one"); 61 | QTest::addColumn("two"); 62 | QTest::addColumn("moveMode"); 63 | QTest::addColumn("expectedPos"); 64 | QTest::addColumn("expectedAnchor"); 65 | QTest::addColumn("selectedText"); 66 | 67 | QTest::newRow("1") << 0 << 1 << TextCursor::MoveAnchor << 1 << 1 << QString(); 68 | QTest::newRow("2") << 0 << 2 << TextCursor::KeepAnchor << 2 << 0 << QString("Fi"); 69 | QTest::newRow("3") << 5 << 0 << TextCursor::KeepAnchor << 0 << 5 << QString("First"); 70 | } 71 | 72 | void tst_TextCursor::setPosition() 73 | { 74 | static QString text("First1\n" 75 | "Second2\n" 76 | "Third333\n" 77 | "Fourth4444\n" 78 | "Fifth55555\n"); 79 | TextDocument document; 80 | document.setText(text); 81 | 82 | TextCursor cursor(&document); 83 | QFETCH(int, one); 84 | QFETCH(int, two); 85 | QFETCH(TextCursor::MoveMode, moveMode); 86 | QFETCH(int, expectedPos); 87 | QFETCH(int, expectedAnchor); 88 | QFETCH(QString, selectedText); 89 | 90 | cursor.setPosition(one); 91 | cursor.setPosition(two, moveMode); 92 | QCOMPARE(expectedPos, cursor.position()); 93 | QCOMPARE(expectedAnchor, cursor.anchor()); 94 | QCOMPARE(selectedText, cursor.selectedText()); 95 | } 96 | 97 | static const char *trueToLife = "I can only sing it loud\n" // 0 - 23 98 | "always try to sing it clear what the hell are we all doing here\n" // 24 - 87 99 | "making too much of nothing\n" // 88 - 114 100 | "or creating one unholy mess an unfair study in survival, I guess\n" // 115 - 179 101 | "\nSomething\n"; // 180 - 190 102 | 103 | 104 | void tst_TextCursor::movePosition_data() 105 | { 106 | QTest::addColumn("initialPosition"); 107 | QTest::addColumn("initialAnchor"); 108 | QTest::addColumn("moveOperation"); 109 | QTest::addColumn("moveMode"); 110 | QTest::addColumn("count"); 111 | QTest::addColumn("expectedPosition"); 112 | QTest::addColumn("expectedAnchor"); 113 | QTest::addColumn("selectedText"); 114 | 115 | QTest::newRow("Start") << 20 << 20 << TextCursor::Start << TextCursor::MoveAnchor << 1 116 | << 0 << 0 << QString(); 117 | QTest::newRow("Up") << 25 << 25 << TextCursor::Up 118 | << TextCursor::MoveAnchor << 1 << 1 << 1 << QString(); 119 | QTest::newRow("StartOfLine") << 24 << 25 << TextCursor::StartOfLine 120 | << TextCursor::MoveAnchor << 1 << 24 << 24 << QString(); 121 | QTest::newRow("StartOfLine2") << 35 << 35 << TextCursor::StartOfLine 122 | << TextCursor::MoveAnchor << 1 << 24 << 24 << QString(); 123 | QTest::newRow("StartOfBlock") << 85 << 85 << TextCursor::StartOfBlock 124 | << TextCursor::MoveAnchor << 1 << 24 << 24 << QString(); 125 | QTest::newRow("StartOfWord") << 28 << 28 << TextCursor::StartOfWord 126 | << TextCursor::MoveAnchor << 1 << 24 << 24 << QString(); 127 | QTest::newRow("PreviousBlock") << 89 << 89 << TextCursor::PreviousBlock 128 | << TextCursor::MoveAnchor << 1 << 24 << 24 << QString(); 129 | QTest::newRow("PreviousCharacter") << 0 << 0 << TextCursor::PreviousCharacter 130 | << TextCursor::MoveAnchor << 1 << 0 << 0 << QString(); 131 | QTest::newRow("PreviousWord") << 0 << 0 << TextCursor::PreviousWord 132 | << TextCursor::MoveAnchor << 1 << 0 << 0 << QString(); 133 | QTest::newRow("Left") << 0 << 0 << TextCursor::Left 134 | << TextCursor::MoveAnchor << 1 << 0 << 0 << QString(); 135 | QTest::newRow("WordLeft") << 0 << 0 << TextCursor::WordLeft 136 | << TextCursor::MoveAnchor << 1 << 0 << 0 << QString(); 137 | QTest::newRow("FirstCharacter") << 0 << 0 << TextCursor::NextCharacter 138 | << TextCursor::KeepAnchor << 1 << 1 << 0 << "I"; 139 | 140 | QTest::newRow("FirstCharacterReverse") << 1 << 1 << TextCursor::PreviousCharacter 141 | << TextCursor::KeepAnchor << 1 << 0 << 1 << "I"; 142 | QTest::newRow("LastCharacter") << 179 << 179 << TextCursor::NextCharacter 143 | << TextCursor::KeepAnchor << 1 << 180 << 179 << "\n"; 144 | QTest::newRow("LastCharacterReverse") << 180 << 180 << TextCursor::PreviousCharacter 145 | << TextCursor::KeepAnchor << 1 << 179 << 180 << "\n"; 146 | 147 | QTest::newRow("EmptyBlock") << 180 << 180 << TextCursor::NextBlock 148 | << TextCursor::KeepAnchor << 1 << 181 << 180 << "\n"; 149 | 150 | QTest::newRow("EmptyBlockReverse") << 181 << 181 << TextCursor::PreviousBlock 151 | << TextCursor::KeepAnchor << 1 << 180 << 181 << "\n"; 152 | QTest::newRow("100Down") << 0 << 0 << TextCursor::Down << TextCursor::MoveAnchor << 100 153 | << 191 << 191 << QString(); 154 | 155 | // QTest::newRow("End") << 0 << 0 << TextCursor::End 156 | // << TextCursor::MoveAnchor << 0 << 0 << QString(); 157 | // QTest::newRow("Down") << 0 << 0 << TextCursor::Down 158 | // << TextCursor::MoveAnchor << 0 << 0 << QString(); 159 | // QTest::newRow("EndOfLine") << 0 << 0 << TextCursor::EndOfLine 160 | // << TextCursor::MoveAnchor << 0 << 0 << QString(); 161 | // QTest::newRow("EndOfWord") << 0 << 0 << TextCursor::EndOfWord << TextCursor::MoveAnchor << 0 << 0; 162 | // QTest::newRow("EndOfBlock") << 0 << 0 << TextCursor::EndOfBlock << TextCursor::MoveAnchor << 0 << 0; 163 | // QTest::newRow("NextBlock") << 0 << 0 << TextCursor::NextBlock << TextCursor::MoveAnchor << 0 << 0; 164 | // QTest::newRow("NextCharacter") << 0 << 0 << TextCursor::NextCharacter << TextCursor::MoveAnchor << 0 << 0; 165 | // QTest::newRow("NextWord") << 0 << 0 << TextCursor::NextWord << TextCursor::MoveAnchor << 0 << 0; 166 | // QTest::newRow("Right") << 0 << 0 << TextCursor::Right << TextCursor::MoveAnchor << 0 << 0; 167 | // QTest::newRow("WordRight") << 0 << 0 << TextCursor::WordRight << TextCursor::MoveAnchor << 0 << 0; 168 | } 169 | 170 | void tst_TextCursor::movePosition() 171 | { 172 | TextDocument document; 173 | document.setText(QString::fromLatin1(trueToLife)); 174 | 175 | TextCursor cursor(&document); 176 | QFETCH(int, initialPosition); 177 | QFETCH(int, initialAnchor); 178 | QFETCH(TextCursor::MoveOperation, moveOperation); 179 | QFETCH(TextCursor::MoveMode, moveMode); 180 | QFETCH(int, count); 181 | QFETCH(int, expectedPosition); 182 | QFETCH(int, expectedAnchor); 183 | QFETCH(QString, selectedText); 184 | 185 | cursor.setPosition(initialAnchor); 186 | if (initialPosition != initialAnchor) { 187 | cursor.setPosition(initialPosition, TextCursor::KeepAnchor); 188 | } 189 | Q_ASSERT(cursor.position() == initialPosition); 190 | Q_ASSERT(cursor.anchor() == initialAnchor); 191 | cursor.movePosition(moveOperation, moveMode, count); 192 | QCOMPARE(cursor.position(), expectedPosition); 193 | QCOMPARE(cursor.anchor(), expectedAnchor); 194 | QCOMPARE(cursor.selectedText(), selectedText); 195 | } 196 | 197 | void tst_TextCursor::invalidPosition_data() 198 | { 199 | QTest::addColumn("position"); 200 | QTest::addColumn("anchor"); 201 | QTest::addColumn("valid"); 202 | QTest::newRow("1") << 0 << -1 << true; 203 | QTest::newRow("2") << -1 << -1 << false; 204 | QTest::newRow("3") << -1 << -2 << false; 205 | QTest::newRow("4") << 0 << -2 << false; 206 | QTest::newRow("5") << 7 << 7 << true; 207 | QTest::newRow("6") << 7 << -1 << true; 208 | QTest::newRow("7") << 8 << 7 << false; 209 | QTest::newRow("8") << 8 << -1 << true; 210 | } 211 | 212 | void tst_TextCursor::invalidPosition() 213 | { 214 | TextDocument doc; 215 | doc.setText("foo bar"); 216 | QCOMPARE(doc.documentSize(), 7); 217 | TextCursor cursor(&doc, -1); 218 | 219 | } 220 | 221 | 222 | // void tst_TextCursor::assertCase1() 223 | // { 224 | // TextDocument document; 225 | // document.setText(QString::fromLatin1(trueToLife)); 226 | 227 | // TextCursor cursor(&document); 228 | // cursor.setPosition(0); 229 | // cursor.movePosition(TextCursor::Down, TextCursor::MoveAnchor, 100); 230 | // QCOMPARE(cursor.selectionSize(), document.documentSize()); 231 | // } 232 | 233 | void tst_TextCursor::operatorEquals_data() 234 | { 235 | QTest::addColumn("left"); 236 | QTest::addColumn("right"); 237 | QTest::addColumn("expected"); 238 | 239 | QTest::newRow("null vs null") << new TextCursor << new TextCursor << true; 240 | QTest::newRow("non null vs null") << new TextCursor(&document) << new TextCursor << false; 241 | 242 | QTest::newRow("normal vs normal equal") << new TextCursor(&document) << new TextCursor(&document) << true; 243 | TextCursor *cursor = new TextCursor(&document); 244 | cursor->setPosition(1); 245 | QTest::newRow("normal vs normal different") << new TextCursor(&document) << cursor << false; 246 | 247 | cursor = new TextCursor(&document); 248 | cursor->setPosition(1, TextCursor::KeepAnchor); 249 | 250 | TextCursor *cursor2 = new TextCursor(&document); 251 | cursor2->setPosition(1, TextCursor::KeepAnchor); 252 | QTest::newRow("selection vs selection") << cursor << cursor2 << true; 253 | 254 | cursor = new TextCursor(&document); 255 | cursor->setPosition(1, TextCursor::KeepAnchor); 256 | 257 | cursor2 = new TextCursor(&document); 258 | cursor2->setPosition(1, TextCursor::MoveAnchor); 259 | QTest::newRow("selection vs selection different") << cursor << cursor2 << false; 260 | 261 | 262 | } 263 | 264 | void tst_TextCursor::operatorEquals() 265 | { 266 | QFETCH(TextCursor*, left); 267 | QFETCH(TextCursor*, right); 268 | QFETCH(bool, expected); 269 | 270 | const bool equal = (*left == *right); 271 | const bool notEqual = (*left != *right); 272 | delete left; 273 | delete right; 274 | QVERIFY(equal != notEqual); 275 | QCOMPARE(equal, expected); 276 | } 277 | 278 | 279 | 280 | QTEST_MAIN(tst_TextCursor) 281 | #include "tst_textcursor.moc" 282 | -------------------------------------------------------------------------------- /tests/textedit/tst_textedit.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | 17 | #include 18 | #include 19 | QT_FORWARD_DECLARE_CLASS(TextEdit) 20 | 21 | //TESTED_CLASS= 22 | //TESTED_FILES= 23 | 24 | // Need Qt 4.6.0> for qWaitForWindowShown 25 | #if (QT_VERSION < QT_VERSION_CHECK(4, 6, 0)) 26 | 27 | #ifdef Q_WS_X11 28 | extern void qt_x11_wait_for_window_manager(QWidget *w); 29 | #endif 30 | 31 | namespace QTest 32 | { 33 | inline static bool qWaitForWindowShown(QWidget *window) 34 | { 35 | #if defined(Q_WS_X11) 36 | qt_x11_wait_for_window_manager(window); 37 | QCoreApplication::processEvents(); 38 | #elif defined(Q_WS_QWS) 39 | Q_UNUSED(window); 40 | qWait(100); 41 | #else 42 | Q_UNUSED(window); 43 | qWait(50); 44 | #endif 45 | return true; 46 | } 47 | 48 | } 49 | 50 | QT_END_NAMESPACE 51 | 52 | #endif // Qt < 4.6.0 53 | 54 | class tst_TextEdit : public QObject 55 | { 56 | Q_OBJECT 57 | 58 | public: 59 | tst_TextEdit(); 60 | virtual ~tst_TextEdit(); 61 | 62 | private slots: 63 | void clickInReadOnlyEdit(); 64 | void scrollReadOnly(); 65 | void scrollLines(); 66 | void cursorForPosition(); 67 | void columnNumberIssue(); 68 | void selectionTest(); 69 | void sectionTest(); 70 | void deleteTest(); 71 | void emptyDocumentTest(); 72 | void changeDocumentTest(); 73 | void clickInBlankAreaTest(); 74 | }; 75 | 76 | tst_TextEdit::tst_TextEdit() 77 | { 78 | } 79 | 80 | tst_TextEdit::~tst_TextEdit() 81 | { 82 | } 83 | 84 | void tst_TextEdit::scrollReadOnly() 85 | { 86 | TextEdit edit; 87 | QString line = "12434567890\n"; 88 | QString text; 89 | for (int i=0; i<1000; ++i) { 90 | text.append(line); 91 | } 92 | edit.document()->setText(text); 93 | edit.setReadOnly(true); 94 | edit.resize(400, 400); 95 | edit.show(); 96 | QTest::qWaitForWindowShown(&edit); 97 | // edit.setCursorPosition(0); 98 | // edit.verticalScrollBar()->setValue(0); 99 | // qDebug() << edit.cursorPosition() << edit.verticalScrollBar()->value(); 100 | // QEventLoop loop; 101 | // loop.exec(); 102 | QTest::keyClick(&edit, Qt::Key_Down); 103 | QVERIFY(edit.verticalScrollBar()->value() != 0); 104 | } 105 | 106 | void tst_TextEdit::scrollLines() 107 | { 108 | TextEdit edit; 109 | QString text = "Line 1\nLine2\nLine 3\n\nLine 5\n\nLine 7\nLine 8\n\nLine 10\n"; 110 | int numLines = text.count('\n'); 111 | edit.document()->setText(text); 112 | 113 | QList lineSteps; 114 | lineSteps << 1 << 3; // Single-click, mouse wheel 115 | 116 | QList directions; 117 | directions << 1 << -1; // Forwards, backwards 118 | 119 | QScrollBar *vsb = edit.verticalScrollBar(); 120 | 121 | foreach(int lineStep, lineSteps) { 122 | int lineNo = 0; 123 | //printf("%d-line step\n", lineStep); 124 | foreach(int direction, directions) { 125 | //printf(" %s\n", direction>0?"Forwards":"Backwards"); 126 | if (direction>0) { 127 | // Forwards 128 | vsb->setSliderPosition( 0 ); 129 | } 130 | 131 | // Forwards 132 | while (lineNo <= numLines && lineNo >= 0) { 133 | int actualLineNo = edit.lineNumber(edit.viewportPosition()); 134 | //printf(" Line %d\n", actualLineNo); 135 | if (lineNo != actualLineNo) { 136 | printf(" %s: expected to be at line %d, actually at line %d\n", 137 | (direction > 0 ? "Forwards" : "Backwards"), lineNo, actualLineNo); 138 | } 139 | QCOMPARE(lineNo, actualLineNo); 140 | for (int i=0; itriggerAction(direction > 0 142 | ? QAbstractSlider::SliderSingleStepAdd 143 | : QAbstractSlider::SliderSingleStepSub); 144 | } 145 | } 146 | lineNo = numLines; 147 | } 148 | } 149 | } 150 | 151 | void tst_TextEdit::clickInReadOnlyEdit() 152 | { 153 | TextEdit edit; 154 | QString line = "1234567890\n"; 155 | QString text; 156 | for (int i=0; i<1000; ++i) { 157 | text.append(line); 158 | } 159 | edit.document()->setText(text); 160 | edit.setReadOnly(true); 161 | edit.resize(400, 400); 162 | edit.show(); 163 | QTest::qWaitForWindowShown(&edit); 164 | // edit.setCursorPosition(0); 165 | // edit.verticalScrollBar()->setValue(0); 166 | // qDebug() << edit.cursorPosition() << edit.verticalScrollBar()->value(); 167 | // QEventLoop loop; 168 | // loop.exec(); 169 | QTest::mouseClick(edit.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(20, 20)); 170 | QVERIFY(edit.verticalScrollBar()->value() == 0); 171 | 172 | } 173 | 174 | void tst_TextEdit::cursorForPosition() 175 | { 176 | TextEdit edit; 177 | const QString string = "123456"; 178 | QSet all; 179 | for (int i=0; irect(); 188 | QPoint pos(0, 0); 189 | for (int x=r.left(); xmaximum() != 0); 230 | while (edit.cursorPosition() < edit.document()->documentSize()) { 231 | QCOMPARE(0, edit.textCursor().columnNumber()); 232 | QTest::keyClick(&edit, Qt::Key_Down); 233 | } 234 | // QVERIFY(edit.verticalScrollBar()->value() != 0); 235 | while (edit.cursorPosition() > 0) { 236 | // qDebug() << edit.cursorPosition(); 237 | QTest::keyClick(&edit, Qt::Key_Up); 238 | QCOMPARE(0, edit.textCursor().columnNumber()); 239 | } 240 | // QEventLoop loop; 241 | // loop.exec(); 242 | } 243 | 244 | void tst_TextEdit::selectionTest() 245 | { 246 | TextEdit edit; 247 | QString line = "1234567890\n"; 248 | edit.document()->setText(line); 249 | // edit.setReadOnly(true); 250 | edit.resize(400, 400); 251 | edit.show(); 252 | QTest::qWaitForWindowShown(&edit); 253 | 254 | QCOMPARE(edit.selectedText(), QString()); 255 | QVERIFY(!edit.textCursor().hasSelection()); 256 | 257 | QTest::keyClick(&edit, Qt::Key_Right, Qt::ShiftModifier); 258 | QVERIFY(edit.textCursor().hasSelection()); 259 | QCOMPARE(edit.selectedText(), QString("1")); 260 | QCOMPARE(edit.textCursor().anchor(), 0); 261 | QCOMPARE(edit.textCursor().position(), 1); 262 | 263 | QTest::keyClick(&edit, Qt::Key_Right, Qt::ShiftModifier); 264 | QVERIFY(edit.textCursor().hasSelection()); 265 | QCOMPARE(edit.selectedText(), QString("12")); 266 | QCOMPARE(edit.textCursor().anchor(), 0); 267 | QCOMPARE(edit.textCursor().position(), 2); 268 | 269 | QTest::keyClick(&edit, Qt::Key_Right, Qt::ShiftModifier); 270 | QVERIFY(edit.textCursor().hasSelection()); 271 | QCOMPARE(edit.textCursor().selectedText(), QString("123")); 272 | QCOMPARE(edit.textCursor().anchor(), 0); 273 | QCOMPARE(edit.textCursor().position(), 3); 274 | 275 | QTest::keyClick(&edit, Qt::Key_Left, Qt::ShiftModifier); 276 | QVERIFY(edit.textCursor().hasSelection()); 277 | QCOMPARE(edit.textCursor().selectedText(), QString("12")); 278 | QCOMPARE(edit.textCursor().anchor(), 0); 279 | QCOMPARE(edit.textCursor().position(), 2); 280 | 281 | QTest::keyClick(&edit, Qt::Key_Left, Qt::ShiftModifier); 282 | QVERIFY(edit.textCursor().hasSelection()); 283 | QCOMPARE(edit.textCursor().selectedText(), QString("1")); 284 | QCOMPARE(edit.textCursor().anchor(), 0); 285 | QCOMPARE(edit.textCursor().position(), 1); 286 | 287 | QTest::keyClick(&edit, Qt::Key_Left, Qt::ShiftModifier); 288 | QVERIFY(!edit.textCursor().hasSelection()); 289 | QCOMPARE(edit.textCursor().selectedText(), QString()); 290 | QCOMPARE(edit.textCursor().anchor(), 0); 291 | QCOMPARE(edit.textCursor().position(), 0); 292 | } 293 | 294 | void tst_TextEdit::sectionTest() 295 | { 296 | TextEdit edit; 297 | QString text = "This is some\nrandom document string\nthing\n"; 298 | edit.document()->setText(text); 299 | TextSection *ts = edit.insertTextSection(5, 10); 300 | edit.document()->takeTextSection(ts); 301 | QVERIFY( !edit.sections().contains(ts) ); 302 | } 303 | 304 | void tst_TextEdit::deleteTest() 305 | { 306 | TextDocument *doc = new TextDocument(); 307 | doc->setText("Test to make sure that deleting a text edit with sections doesn't result in the sections being deleted again in the document destructor\n"); 308 | TextEdit *edit = new TextEdit(); 309 | edit->setDocument(doc); 310 | 311 | TextSection *s = edit->insertTextSection(4, 4); 312 | Q_UNUSED(s); 313 | delete edit; 314 | delete doc; 315 | } 316 | 317 | void tst_TextEdit::emptyDocumentTest() 318 | { 319 | TextEdit edit; 320 | for(int i=0; i < 100 ; i++) { 321 | edit.append(QString("This is text on line %1\n").arg(i)); 322 | } 323 | edit.show(); 324 | QTest::qWaitForWindowShown(&edit); 325 | edit.setDocument(new TextDocument()); /// This causes the problem 326 | QTest::qWaitForWindowShown(&edit); 327 | } 328 | 329 | void tst_TextEdit::changeDocumentTest() 330 | { 331 | TextDocument doc1; 332 | doc1.setText("abcdefg"); 333 | 334 | TextDocument doc2; 335 | doc2.setText("hijklmn"); 336 | 337 | TextEdit editor1; 338 | editor1.setDocument(&doc1); 339 | editor1.setDocument(&doc2); 340 | 341 | TextEdit editor2; 342 | editor2.setDocument(&doc1); 343 | 344 | TextSection *s = editor2.insertTextSection(0, 1); 345 | s->setCursor(Qt::PointingHandCursor); 346 | } 347 | 348 | void tst_TextEdit::clickInBlankAreaTest() 349 | { 350 | TextEdit edit; 351 | edit.load("spectre.scs"); 352 | // Make the window big enough to leave blank space below 353 | edit.setMinimumSize(QSize(1000, 1200)); 354 | edit.show(); 355 | QTest::qWaitForWindowShown(&edit); 356 | // Click below the visible text 357 | QTest::mouseClick(edit.viewport(), Qt::LeftButton, Qt::NoModifier, 358 | QPoint(50, 1000)); 359 | } 360 | 361 | 362 | 363 | QTEST_MAIN(tst_TextEdit) 364 | #include "tst_textedit.moc" 365 | -------------------------------------------------------------------------------- /textdocument_p.h: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #ifndef TEXTDOCUMENT_P_H 16 | #define TEXTDOCUMENT_P_H 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #ifndef ASSUME 32 | #ifdef FATAL_ASSUMES 33 | #define ASSUME(cond) Q_ASSERT(cond) 34 | #elif defined Q_OS_SOLARIS 35 | #define ASSUME(cond) if (!(cond)) qWarning("Failed assumption %s:%d %s", __FILE__, __LINE__, #cond); 36 | #else 37 | #define ASSUME(cond) if (!(cond)) qWarning("Failed assumption %s:%d (%s) %s", __FILE__, __LINE__, __FUNCTION__, #cond); 38 | #endif 39 | #endif 40 | 41 | #define Q_COMPARE_ASSERT(left, right) if (left != right) { qWarning() << left << right; Q_ASSERT(left == right); } 42 | 43 | #include "textdocument.h" 44 | #ifdef NO_TEXTDOCUMENT_CACHE 45 | #define NO_TEXTDOCUMENT_CHUNK_CACHE 46 | #define NO_TEXTDOCUMENT_READ_CACHE 47 | #endif 48 | 49 | #if defined TEXTDOCUMENT_LINENUMBER_CACHE && !defined TEXTDOCUMENT_LINENUMBER_CACHE_INTERVAL 50 | #define TEXTDOCUMENT_LINENUMBER_CACHE_INTERVAL 100 51 | #endif 52 | 53 | static inline bool matchSection(const TextSection *section, const TextEdit *textEdit) 54 | { 55 | if (!textEdit) { 56 | return true; 57 | } else if (!section->textEdit()) { 58 | return true; 59 | } else { 60 | return textEdit == section->textEdit(); 61 | } 62 | } 63 | 64 | 65 | struct Chunk { 66 | Chunk() : previous(0), next(0), from(-1), length(0), firstLineIndex(-1) 67 | #ifndef TEXTDOCUMENT_LINENUMBER_CACHE 68 | , lines(-1) 69 | #endif 70 | {} 71 | 72 | mutable QString data; 73 | Chunk *previous, *next; 74 | int size() const { return data.isEmpty() ? length : data.size(); } 75 | #ifdef QT_DEBUG 76 | int pos() const { int p = 0; Chunk *c = previous; while (c) { p += c->size(); c = c->previous; }; return p; } 77 | #endif 78 | mutable int from, length; // Not used when all is loaded 79 | mutable int firstLineIndex; 80 | #ifdef TEXTDOCUMENT_LINENUMBER_CACHE 81 | mutable QVector lineNumbers; 82 | // format is how many endlines in the area from (n * 83 | // TEXTDOCUMENT_LINENUMBER_CACHE_INTERVAL) to 84 | // ((n + 1) * TEXTDOCUMENT_LINENUMBER_CACHE_INTERVAL) 85 | #else 86 | mutable int lines; 87 | #endif 88 | QString swap; 89 | }; 90 | 91 | 92 | // should really use this stuff for all of this stuff 93 | 94 | static inline QPair intersection(int index1, int size1, int index2, int size2) 95 | { 96 | QPair ret; 97 | ret.first = qMax(index1, index2); 98 | const int right = qMin(index1 + size1, index2 + size2); 99 | ret.second = right - ret.first; 100 | if (ret.second <= 0) 101 | return qMakePair(-1, 0); 102 | return ret; 103 | } 104 | 105 | static inline bool compareTextSection(const TextSection *left, const TextSection *right) 106 | { 107 | // don't make this compare document. Look at ::sections() 108 | return left->position() < right->position(); 109 | } 110 | 111 | class TextDocumentIterator; 112 | struct DocumentCommand { 113 | enum Type { 114 | None, 115 | Inserted, 116 | Removed 117 | }; 118 | 119 | DocumentCommand(Type t, int pos = -1, const QString &string = QString()) 120 | : type(t), position(pos), text(string), joinStatus(NoJoin) 121 | {} 122 | 123 | const Type type; 124 | int position; 125 | QString text; 126 | 127 | enum JoinStatus { 128 | NoJoin, 129 | Forward, 130 | Backward 131 | } joinStatus; 132 | }; 133 | 134 | struct TextSection; 135 | struct TextCursorSharedPrivate; 136 | struct TextDocumentPrivate : public QObject 137 | { 138 | Q_OBJECT 139 | public: 140 | TextDocumentPrivate(TextDocument *doc) 141 | : q(doc), first(0), last(0), 142 | #ifndef NO_TEXTDOCUMENT_CHUNK_CACHE 143 | cachedChunk(0), cachedChunkPos(-1), 144 | #endif 145 | #ifndef NO_TEXTDOCUMENT_READ_CACHE 146 | cachePos(-1), 147 | #endif 148 | documentSize(0), 149 | saveState(NotSaving), findState(NotFinding), ownDevice(false), modified(false), 150 | deviceMode(TextDocument::Sparse), chunkSize(16384), 151 | undoRedoStackCurrent(0), modifiedIndex(-1), undoRedoEnabled(true), ignoreUndoRedo(false), 152 | collapseInsertUndo(false), hasChunksWithLineNumbers(false), textCodec(0), options(TextDocument::DefaultOptions), 153 | readWriteLock(0), cursorCommand(false) 154 | { 155 | first = last = new Chunk; 156 | } 157 | 158 | TextDocument *q; 159 | QSet textCursors; 160 | mutable Chunk *first, *last; 161 | 162 | #ifndef NO_TEXTDOCUMENT_CHUNK_CACHE 163 | mutable Chunk *cachedChunk; 164 | mutable int cachedChunkPos; 165 | mutable QString cachedChunkData; // last uninstantiated chunk's read from file 166 | #endif 167 | #ifndef NO_TEXTDOCUMENT_READ_CACHE 168 | mutable int cachePos; 169 | mutable QString cache; // results of last read(). Could span chunks 170 | #endif 171 | 172 | int documentSize; 173 | enum SaveState { NotSaving, Saving, AbortSave } saveState; 174 | enum FindState { NotFinding, Finding, AbortFind } mutable findState; 175 | QList sections; 176 | QPointer device; 177 | bool ownDevice, modified; 178 | TextDocument::DeviceMode deviceMode; 179 | int chunkSize; 180 | 181 | QList undoRedoStack; 182 | int undoRedoStackCurrent, modifiedIndex; 183 | bool undoRedoEnabled, ignoreUndoRedo, collapseInsertUndo; 184 | 185 | bool hasChunksWithLineNumbers; 186 | QTextCodec *textCodec; 187 | TextDocument::Options options; 188 | QReadWriteLock *readWriteLock; 189 | bool cursorCommand; 190 | 191 | #ifdef QT_DEBUG 192 | mutable QSet iterators; 193 | #endif 194 | void joinLastTwoCommands(); 195 | 196 | void removeChunk(Chunk *c); 197 | QString chunkData(const Chunk *chunk, int pos) const; 198 | int chunkIndex(const Chunk *c) const; 199 | 200 | // evil API. pos < 0 means don't cache 201 | 202 | void updateChunkLineNumbers(Chunk *c, int pos) const; 203 | int countNewLines(Chunk *c, int chunkPos, int index) const; 204 | 205 | void instantiateChunk(Chunk *chunk); 206 | Chunk *chunkAt(int pos, int *offset) const; 207 | void clearRedo(); 208 | void undoRedo(bool undo); 209 | 210 | QString wordAt(int position, int *start = 0) const; 211 | QString paragraphAt(int position, int *start = 0) const; 212 | 213 | uint wordBoundariesAt(int pos) const; 214 | 215 | friend class TextDocument; 216 | void swapOutChunk(Chunk *c); 217 | QList getSections(int from, int size, TextSection::TextSectionOptions opt, const TextEdit *filter) const; 218 | inline TextSection *sectionAt(int pos, const TextEdit *filter) const { return getSections(pos, 1, TextSection::IncludePartial, filter).value(0); } 219 | void textEditDestroyed(TextEdit *edit); 220 | signals: 221 | void sectionFormatChanged(TextSection *section); 222 | void sectionCursorChanged(TextSection *section); 223 | void undoRedoCommandInserted(DocumentCommand *cmd); 224 | void undoRedoCommandRemoved(DocumentCommand *cmd); 225 | void undoRedoCommandTriggered(DocumentCommand *cmd, bool undo); 226 | void undoRedoCommandFinished(DocumentCommand *cmd); 227 | private: 228 | friend class TextSection; 229 | }; 230 | 231 | // should not be kept as a member. Invalidated on inserts/removes 232 | class TextDocumentIterator 233 | { 234 | public: 235 | TextDocumentIterator(const TextDocumentPrivate *d, int p) 236 | : doc(d), pos(p), min(0), max(-1), convert(false) 237 | { 238 | Q_ASSERT(doc); 239 | #ifndef NO_TEXTDOCUMENTITERATOR_CACHE 240 | chunk = doc->chunkAt(p, &offset); 241 | Q_ASSERT(chunk); 242 | const int chunkPos = p - offset; 243 | chunkData = doc->chunkData(chunk, chunkPos); 244 | Q_COMPARE_ASSERT(chunkData.size(), chunk->size()); 245 | #ifdef QT_DEBUG 246 | if (p != d->documentSize) { 247 | if (doc->q->readCharacter(p) != chunkData.at(offset)) { 248 | qDebug() << "got" << chunkData.at(offset) << "at" << offset << "in" << chunk << "of size" << chunkData.size() 249 | << "expected" << doc->q->readCharacter(p) << "at document pos" << pos; 250 | } 251 | Q_ASSERT(chunkData.at(offset) == doc->q->readCharacter(p)); 252 | } else { 253 | Q_ASSERT(chunkData.size() == offset); 254 | } 255 | #endif 256 | #endif 257 | 258 | #ifdef QT_DEBUG 259 | doc->iterators.insert(this); 260 | #endif 261 | } 262 | #ifdef QT_DEBUG 263 | ~TextDocumentIterator() 264 | { 265 | Q_ASSERT(doc->iterators.remove(this)); 266 | } 267 | #endif 268 | 269 | int end() const 270 | { 271 | return max != -1 ? max : doc->documentSize; 272 | } 273 | 274 | void setMinBoundary(int bound) 275 | { 276 | min = bound; 277 | Q_ASSERT(pos >= min); 278 | } 279 | 280 | void setMaxBoundary(int bound) 281 | { 282 | max = bound; 283 | Q_ASSERT(pos <= max); 284 | } 285 | 286 | 287 | inline bool hasNext() const 288 | { 289 | return pos < end(); 290 | } 291 | 292 | inline bool hasPrevious() const 293 | { 294 | return pos > min; 295 | } 296 | 297 | inline int position() const 298 | { 299 | return pos; 300 | } 301 | 302 | inline QChar current() const 303 | { 304 | Q_ASSERT(doc); 305 | #ifndef NO_TEXTDOCUMENTITERATOR_CACHE 306 | Q_ASSERT(chunk); 307 | Q_COMPARE_ASSERT(chunkData.size(), chunk->size()); 308 | if (pos == end()) 309 | return QChar(); 310 | #ifdef QT_DEBUG 311 | if (doc->q->readCharacter(pos) != chunkData.at(offset)) { 312 | qDebug() << "got" << chunkData.at(offset) << "at" << offset << "in" << chunk << "of size" << chunkData.size() 313 | << "expected" << doc->q->readCharacter(pos) << "at document pos" << pos; 314 | } 315 | #endif 316 | ASSUME(doc->q->readCharacter(pos) == chunkData.at(offset)); 317 | return convert ? chunkData.at(offset).toLower() : chunkData.at(offset); 318 | #else 319 | return convert ? doc->q->readCharacter(pos).toLower() : doc->q->readCharacter(pos); 320 | #endif 321 | } 322 | 323 | inline QChar next() 324 | { 325 | Q_ASSERT(doc); 326 | ++pos; 327 | #ifndef NO_TEXTDOCUMENTITERATOR_CACHE 328 | Q_ASSERT(chunk); 329 | if (++offset >= chunkData.size() && chunk->next) { // special case for offset == chunkData.size() and !chunk->next 330 | offset = 0; 331 | chunk = chunk->next; 332 | Q_ASSERT(chunk); 333 | chunkData = doc->chunkData(chunk, pos); 334 | Q_COMPARE_ASSERT(chunkData.size(), chunk->size()); 335 | } 336 | #endif 337 | return current(); 338 | } 339 | 340 | inline QChar previous() 341 | { 342 | Q_ASSERT(doc); 343 | Q_ASSERT(hasPrevious()); 344 | --pos; 345 | Q_ASSERT(pos >= min); 346 | Q_ASSERT(pos <= end()); 347 | #ifndef NO_TEXTDOCUMENTITERATOR_CACHE 348 | Q_ASSERT(chunk); 349 | if (--offset < 0) { 350 | chunk = chunk->previous; 351 | Q_ASSERT(chunk); 352 | chunkData = doc->chunkData(chunk, pos - chunk->size() + 1); 353 | Q_COMPARE_ASSERT(chunkData.size(), chunk->size()); 354 | offset = chunkData.size() - 1; 355 | } 356 | #endif 357 | return current(); 358 | } 359 | 360 | enum Direction { None = 0x0, Left = 0x1, Right = 0x2, Both = Left|Right }; 361 | inline QChar nextPrev(Direction dir, bool &ok) 362 | { 363 | if (dir == Left) { 364 | if (!hasPrevious()) { 365 | ok = false; 366 | return QChar(); 367 | } 368 | ok = true; 369 | return previous(); 370 | } else { 371 | if (!hasNext()) { 372 | ok = false; 373 | return QChar(); 374 | } 375 | ok = true; 376 | return next(); 377 | } 378 | } 379 | 380 | inline bool convertToLowerCase() const 381 | { 382 | return convert; 383 | } 384 | 385 | inline void setConvertToLowerCase(bool on) 386 | { 387 | convert = on; 388 | } 389 | 390 | private: 391 | const TextDocumentPrivate *doc; 392 | int pos; 393 | int min, max; 394 | #ifndef NO_TEXTDOCUMENTITERATOR_CACHE 395 | int offset; 396 | QString chunkData; 397 | Chunk *chunk; 398 | #endif 399 | bool convert; 400 | }; 401 | 402 | 403 | #endif 404 | -------------------------------------------------------------------------------- /textlayout_p.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include "textlayout_p.h" 16 | #include "textedit_p.h" 17 | #include "textdocument.h" 18 | #include "textedit.h" 19 | 20 | int TextLayout::viewportWidth() const 21 | { 22 | if (!lineBreaking) 23 | return INT_MAX - 1024; 24 | return textEdit ? textEdit->viewport()->width() : viewport; 25 | } 26 | 27 | int TextLayout::doLayout(int index, QList *sections) // index is in document coordinates 28 | { 29 | QTextLayout *textLayout = 0; 30 | if (!unusedTextLayouts.isEmpty()) { 31 | textLayout = unusedTextLayouts.takeLast(); 32 | textLayout->clearAdditionalFormats(); 33 | } else { 34 | textLayout = new QTextLayout; 35 | textLayout->setCacheEnabled(true); 36 | textLayout->setFont(font); 37 | QTextOption option; 38 | option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); 39 | textLayout->setTextOption(option); 40 | } 41 | textLayouts.append(textLayout); 42 | if (index != 0 && bufferReadCharacter(index - 1) != '\n') { 43 | qWarning() << index << viewportPosition << document->read(index - 1, 20) 44 | << bufferReadCharacter(index - 1); 45 | } 46 | Q_ASSERT(index == 0 || bufferReadCharacter(index - 1) == '\n'); 47 | const int max = bufferPosition + buffer.size(); 48 | const int lineStart = index; 49 | while (index < max && bufferReadCharacter(index) != '\n') 50 | ++index; 51 | 52 | const QString string = buffer.mid(lineStart - bufferPosition, index - lineStart); 53 | Q_ASSERT(string.size() == index - lineStart); 54 | Q_ASSERT(!string.contains('\n')); 55 | if (index < max) 56 | ++index; // for the newline 57 | textLayout->setText(string); 58 | 59 | QMultiMap formatMap; 60 | if (sections) { 61 | do { 62 | Q_ASSERT(!sections->isEmpty()); 63 | TextSection *l = sections->first(); 64 | Q_ASSERT(::matchSection(l, textEdit)); 65 | Q_ASSERT(l->position() + l->size() >= lineStart); 66 | if (l->position() >= index) { 67 | break; 68 | } 69 | // section is in this QTextLayout 70 | QTextLayout::FormatRange range; 71 | range.start = qMax(0, l->position() - lineStart); // offset in QTextLayout 72 | range.length = qMin(l->position() + l->size(), index) - lineStart - range.start; 73 | range.format = l->format(); 74 | formatMap.insertMulti(l->priority(), range); 75 | if (l->position() + l->size() >= index) { // > ### ??? 76 | // means section didn't end here. It continues in the next QTextLayout 77 | break; 78 | } 79 | sections->removeFirst(); 80 | } while (!sections->isEmpty()); 81 | } 82 | QList formats = formatMap.values(); 83 | 84 | int leftMargin = LeftMargin; 85 | int rightMargin = 0; 86 | int topMargin = 0; 87 | int bottomMargin = 0; 88 | foreach(SyntaxHighlighter *syntaxHighlighter, syntaxHighlighters) { 89 | syntaxHighlighter->d->currentBlockPosition = lineStart; 90 | syntaxHighlighter->d->formatRanges.clear(); 91 | syntaxHighlighter->d->currentBlock = string; 92 | syntaxHighlighter->highlightBlock(string); 93 | syntaxHighlighter->d->currentBlock.clear(); 94 | if (syntaxHighlighter->d->blockFormat.isValid()) { 95 | blockFormats[textLayout] = syntaxHighlighter->d->blockFormat; 96 | if (syntaxHighlighter->d->blockFormat.hasProperty(QTextFormat::BlockLeftMargin)) 97 | leftMargin = syntaxHighlighter->d->blockFormat.leftMargin(); 98 | if (syntaxHighlighter->d->blockFormat.hasProperty(QTextFormat::BlockRightMargin)) 99 | rightMargin = syntaxHighlighter->d->blockFormat.rightMargin(); 100 | if (syntaxHighlighter->d->blockFormat.hasProperty(QTextFormat::BlockTopMargin)) 101 | topMargin = syntaxHighlighter->d->blockFormat.topMargin(); 102 | if (syntaxHighlighter->d->blockFormat.hasProperty(QTextFormat::BlockBottomMargin)) 103 | bottomMargin = syntaxHighlighter->d->blockFormat.bottomMargin(); 104 | 105 | } 106 | syntaxHighlighter->d->previousBlockState = syntaxHighlighter->d->currentBlockState; 107 | if (!syntaxHighlighter->d->formatRanges.isEmpty()) 108 | formats += syntaxHighlighter->d->formatRanges; 109 | } 110 | textLayout->setAdditionalFormats(formats); 111 | textLayout->beginLayout(); 112 | const int lineWidth = viewportWidth() - (leftMargin + rightMargin); 113 | 114 | int localWidest = -1; 115 | forever { 116 | QTextLine line = textLayout->createLine(); 117 | if (!line.isValid()) { 118 | break; 119 | } 120 | line.setLineWidth(lineWidth); 121 | if (!lineBreaking) 122 | localWidest = qMax(localWidest, line.naturalTextWidth() + (LeftMargin * 2)); 123 | // ### support blockformat margins etc 124 | int y = topMargin + lastBottomMargin; 125 | if (!lines.isEmpty()) { 126 | y += int(lines.last().second.rect().bottom()); 127 | // QTextLine doesn't seem to get its rect() update until a 128 | // new line has been created (or presumably in endLayout) 129 | } 130 | line.setPosition(QPoint(leftMargin, y)); 131 | lines.append(qMakePair(lineStart + line.textStart(), line)); 132 | } 133 | widest = qMax(widest, localWidest); 134 | lastBottomMargin = bottomMargin; 135 | 136 | textLayout->endLayout(); 137 | #ifndef QT_NO_DEBUG 138 | for (int i=1; iboundingRect().toRect(); 145 | // this will actually take the entire width set in setLineWidth 146 | // and not what it actually uses. 147 | r.setWidth(localWidest); 148 | 149 | contentRect |= r; 150 | Q_ASSERT(!lineBreaking || contentRect.right() <= qint64(viewportWidth()) + LeftMargin 151 | || viewportWidth() == -1); 152 | 153 | foreach(SyntaxHighlighter *syntaxHighlighter, syntaxHighlighters) { 154 | syntaxHighlighter->d->formatRanges.clear(); 155 | syntaxHighlighter->d->blockFormat = QTextBlockFormat(); 156 | syntaxHighlighter->d->currentBlockPosition = -1; 157 | } 158 | 159 | return index; 160 | } 161 | 162 | int TextLayout::textPositionAt(const QPoint &p) const 163 | { 164 | QPoint pos = p; 165 | if (pos.x() >= 0 && pos.x() < LeftMargin) 166 | pos.rx() = LeftMargin; // clicking in the margin area should count as the first characters 167 | 168 | int textLayoutOffset = viewportPosition; 169 | foreach(const QTextLayout *l, textLayouts) { 170 | if (l->boundingRect().toRect().contains(pos)) { 171 | const int lineCount = l->lineCount(); 172 | for (int i=0; ilineAt(i); 174 | if (line.y() <= pos.y() && pos.y() <= line.height() + line.y()) { // ### < ??? 175 | return textLayoutOffset + line.xToCursor(qMax(LeftMargin, pos.x())); 176 | } 177 | } 178 | } 179 | textLayoutOffset += l->text().size() + 1; // + 1 for newlines which aren't in the QTextLayout 180 | } 181 | return -1; 182 | } 183 | 184 | QList TextLayout::relayoutCommon() 185 | { 186 | // widest = -1; // ### should this be relative to current content or remember? What if you remove the line that was the widest? 187 | Q_ASSERT(layoutDirty); 188 | layoutDirty = false; 189 | Q_ASSERT(document); 190 | lines.clear(); 191 | unusedTextLayouts = textLayouts; 192 | textLayouts.clear(); 193 | contentRect = QRect(); 194 | visibleLines = lastVisibleCharacter = -1; 195 | 196 | foreach(SyntaxHighlighter *syntaxHighlighter, syntaxHighlighters) { 197 | syntaxHighlighter->d->previousBlockState = syntaxHighlighter->d->currentBlockState = -1; 198 | } 199 | 200 | if (viewportPosition < bufferPosition 201 | || (bufferPosition + buffer.size() < document->documentSize() 202 | && buffer.size() - bufferOffset() < MinimumBufferSize)) { 203 | bufferPosition = qMax(0, viewportPosition - MinimumBufferSize); 204 | buffer = document->read(bufferPosition, int(MinimumBufferSize * 2.5)); 205 | sections = document->d->getSections(bufferPosition, buffer.size(), TextSection::IncludePartial, textEdit); 206 | } else if (sectionsDirty) { 207 | sections = document->d->getSections(bufferPosition, buffer.size(), TextSection::IncludePartial, textEdit); 208 | } 209 | sectionsDirty = false; 210 | QList l = sections; 211 | while (!l.isEmpty() && l.first()->position() + l.first()->size() < viewportPosition) 212 | l.takeFirst(); // could cache these as well 213 | return l; 214 | } 215 | 216 | void TextLayout::relayoutByGeometry(int height) 217 | { 218 | if (!layoutDirty) 219 | return; 220 | 221 | QList l = relayoutCommon(); 222 | 223 | const int max = viewportPosition + buffer.size() - bufferOffset(); // in document coordinates 224 | ASSUME(viewportPosition == 0 || bufferReadCharacter(viewportPosition - 1) == '\n'); 225 | 226 | static const int extraLines = qMax(2, qgetenv("LAZYTEXTEDIT_EXTRA_LINES").toInt()); 227 | int index = viewportPosition; 228 | while (index < max) { 229 | index = doLayout(index, l.isEmpty() ? 0 : &l); 230 | Q_ASSERT(index == max || document->readCharacter(index - 1) == '\n'); 231 | Q_ASSERT(!textLayouts.isEmpty()); 232 | const int y = int(textLayouts.last()->boundingRect().bottom()); 233 | if (y >= height) { 234 | if (visibleLines == -1) { 235 | visibleLines = lines.size(); 236 | lastVisibleCharacter = index; 237 | } else if (lines.size() >= visibleLines + extraLines) { 238 | break; 239 | } 240 | } 241 | } 242 | if (visibleLines == -1) { 243 | visibleLines = lines.size(); 244 | lastVisibleCharacter = index; 245 | } 246 | 247 | 248 | layoutEnd = qMin(index, max); 249 | qDeleteAll(unusedTextLayouts); 250 | unusedTextLayouts.clear(); 251 | Q_ASSERT(viewportPosition < layoutEnd || 252 | (viewportPosition == layoutEnd && viewportPosition == document->documentSize())); 253 | // qDebug() << "layoutEnd" << layoutEnd << "viewportPosition" << viewportPosition; 254 | } 255 | 256 | void TextLayout::relayoutByPosition(int size) 257 | { 258 | if (!layoutDirty) 259 | return; 260 | 261 | QList l = relayoutCommon(); 262 | 263 | const int max = viewportPosition + qMin(size, buffer.size() - bufferOffset()); 264 | Q_ASSERT(viewportPosition == 0 || bufferReadCharacter(viewportPosition - 1) == '\n'); 265 | int index = viewportPosition; 266 | while (index < max) { 267 | index = doLayout(index, l.isEmpty() ? 0 : &l); 268 | } 269 | layoutEnd = index; 270 | 271 | qDeleteAll(unusedTextLayouts); 272 | unusedTextLayouts.clear(); 273 | Q_ASSERT(viewportPosition < layoutEnd || 274 | (viewportPosition == layoutEnd && viewportPosition == document->documentSize())); 275 | } 276 | 277 | void TextLayout::relayout() 278 | { 279 | relayoutByPosition(2000); // ### totally arbitrary number 280 | } 281 | 282 | QTextLayout *TextLayout::layoutForPosition(int pos, int *offset, int *index) const 283 | { 284 | if (offset) 285 | *offset = -1; 286 | if (index) 287 | *index = -1; 288 | 289 | if (textLayouts.isEmpty() || pos < viewportPosition || pos > layoutEnd) { 290 | return 0; 291 | } 292 | 293 | int textLayoutOffset = viewportPosition; 294 | int i = 0; 295 | 296 | foreach(QTextLayout *l, textLayouts) { 297 | if (pos >= textLayoutOffset && pos <= l->text().size() + textLayoutOffset) { 298 | if (offset) 299 | *offset = pos - textLayoutOffset; 300 | if (index) 301 | *index = i; 302 | return l; 303 | } 304 | ++i; 305 | textLayoutOffset += l->text().size() + 1; 306 | } 307 | return 0; 308 | } 309 | 310 | QTextLine TextLayout::lineForPosition(int pos, int *offsetInLine, int *lineIndex, bool *lastLine) const 311 | { 312 | if (offsetInLine) 313 | *offsetInLine = -1; 314 | if (lineIndex) 315 | *lineIndex = -1; 316 | if (lastLine) 317 | *lastLine = false; 318 | 319 | if (pos < viewportPosition || pos >= layoutEnd || textLayouts.isEmpty() || lines.isEmpty()) { 320 | return QTextLine(); 321 | } 322 | int layoutIndex = 0; 323 | QTextLayout *layout = textLayouts.value(layoutIndex); 324 | Q_ASSERT(layout); 325 | for (int i=0; i &line = lines.at(i); 327 | int lineEnd = line.first + line.second.textLength(); 328 | const bool last = line.second.lineNumber() + 1 == layout->lineCount(); 329 | if (last) { 330 | ++lineEnd; 331 | // 1 is for newline characters 332 | layout = textLayouts.value(++layoutIndex); 333 | // could be 0 334 | } 335 | if (pos < lineEnd) { 336 | if (offsetInLine) { 337 | *offsetInLine = pos - line.first; 338 | Q_ASSERT(*offsetInLine >= 0); 339 | Q_ASSERT(*offsetInLine < lineEnd + pos); 340 | } 341 | if (lineIndex) { 342 | *lineIndex = i; 343 | } 344 | if (lastLine) 345 | *lastLine = last; 346 | return line.second; 347 | } else if (!layout) { 348 | break; 349 | } 350 | } 351 | qWarning() << "Couldn't find a line for" << pos << "viewportPosition" << viewportPosition 352 | << "layoutEnd" << layoutEnd; 353 | Q_ASSERT(0); 354 | return QTextLine(); 355 | } 356 | 357 | // pos is not necessarily on a newline. Finds closest newline in the 358 | // right direction and sets viewportPosition to that. Updates 359 | // scrollbars if this is a TextEditPrivate 360 | 361 | void TextLayout::updateViewportPosition(int pos, Direction direction) 362 | { 363 | pos = qMin(pos, maxViewportPosition); 364 | if (document->documentSize() == 0) { 365 | viewportPosition = 0; 366 | } else { 367 | Q_ASSERT(document->documentSize() > 0); 368 | int index = document->find('\n', qMax(0, pos + (direction == Backward ? -1 : 0)), 369 | TextDocument::FindMode(direction)).anchor(); 370 | if (index == -1) { 371 | if (direction == Backward) { 372 | index = 0; 373 | } else { 374 | index = qMax(0, document->find('\n', document->documentSize() - 1, TextDocument::FindBackward).position()); 375 | // position after last newline in document 376 | // if there is no newline put it at 0 377 | } 378 | } else { 379 | ++index; 380 | } 381 | Q_ASSERT(index != -1); 382 | viewportPosition = index; 383 | 384 | if (viewportPosition != 0 && document->read(viewportPosition - 1, 1) != QString("\n")) 385 | qWarning() << "viewportPosition" << viewportPosition << document->read(viewportPosition - 1, 10) << this; 386 | ASSUME(viewportPosition == 0 || document->read(viewportPosition - 1, 1) == QString("\n")); 387 | } 388 | if (viewportPosition > maxViewportPosition && direction == Forward) { 389 | updateViewportPosition(viewportPosition, Backward); 390 | return; 391 | } 392 | layoutDirty = true; 393 | 394 | if (textEdit && !suppressTextEditUpdates) { 395 | textEdit->viewport()->update(); 396 | TextEditPrivate *p = static_cast(this); 397 | p->pendingScrollBarUpdate = true; 398 | p->updateCursorPosition(p->lastHoverPos); 399 | if (!textEdit->verticalScrollBar()->isSliderDown()) { 400 | p->updateScrollBar(); 401 | } // sliderReleased is connected to updateScrollBar() 402 | } 403 | relayout(); 404 | } 405 | 406 | #ifndef QT_NO_DEBUG_STREAM 407 | QDebug &operator<<(QDebug &str, const QTextLine &line) 408 | { 409 | if (!line.isValid()) { 410 | str << "QTextLine() (invalid)"; 411 | return str; 412 | } 413 | str.space() << "lineNumber" << line.lineNumber() 414 | << "textStart" << line.textStart() 415 | << "textLength" << line.textLength() 416 | << "position" << line.position(); 417 | return str; 418 | } 419 | 420 | QString TextLayout::dump() const 421 | { 422 | QString out; 423 | QTextStream ts(&out, QIODevice::WriteOnly); 424 | ts << "viewportPosition " << viewportPosition 425 | << " layoutEnd " << layoutEnd 426 | << " viewportWidth " << viewportWidth() << '\n'; 427 | for (int i=0; ilineCount(); ++j) { 430 | QTextLine line = layout->lineAt(j); 431 | ts << layout->text().mid(line.textStart(), line.textLength()); 432 | if (j + 1 < layout->lineCount()) { 433 | ts << "\n"; 434 | } else { 435 | ts << "\n"; 436 | } 437 | } 438 | } 439 | return out; 440 | } 441 | 442 | #endif 443 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | // abcde fghij klmno pqrst uvwxy z1234 56789 0!@#$ abcde fghij klmno pqrst uvwxy z1234 56789 0!@#$ 2 | // Copyright 2010 Anders Bakken 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include "textedit.h" 35 | 36 | // ### TODO ### 37 | // ### Should clear selection when something else selects something. 38 | // ### Line break. Could vastly simplify textlayout if not breaking lines. 39 | // ### saving to same file. Need something there. 40 | // ### could refactor chunks so that I only have one and split when I need. Not sure if it's worth it. Would 41 | // ### need chunkData(int from = 0, int size = -1). The current approach makes caching easier since I can now cache an entire chunk. 42 | // ### ensureCursorVisible(TextCursor) is not implemented 43 | // ### add a command line switch to randomly do stuff. Insert, move around etc n times to see if I can reproduce a crash. Allow input of seed 44 | // ### I still have some weird debris when scrolling in large documents 45 | // ### use tests from Qt. E.g. for QTextCursor 46 | // ### need to protect against undo with huge amounts of text in it. E.g. select all and delete. MsgBox asking? 47 | // ### maybe refactor updateViewportPosition so it can handle the margins usecase that textcursor needs. 48 | // ### could keep undo/redo history in the textcursor and have an api on the cursor. 49 | // ### block state in SyntaxHighlighter doesn't remember between 50 | // ### highlights. In regular QText.* it does. Could store this info. 51 | // ### Shouldn't be that much. Maybe base it off of position in 52 | // ### document rather than line number since I don't know which line 53 | // ### we're on 54 | // ### TextCursor(const TextEdit *). Is this a good idea? 55 | // ### caching stuff is getting a little convoluted 56 | // ### what should TextDocument::read(documentSize + 1, 10) do? ASSERT? 57 | // ### should I allow to write in a formatted manner? currentTextFormat and all? Why not really. 58 | // ### consider using QTextBoundaryFinder for something 59 | // ### drag and drop 60 | // ### documentation 61 | // ### consider having extra textLayouts on each side of viewport for optimized scrolling. Could detect that condition 62 | // ### Undo section removal/adding. This is a mess 63 | 64 | class SpellCheck : public SyntaxHighlighter 65 | { 66 | public: 67 | SpellCheck(QObject *parent) : SyntaxHighlighter(parent) {} 68 | virtual void highlightBlock(const QString &string) 69 | { 70 | if (string.contains(QRegExp("[0-9]"))) { 71 | QTextCharFormat format; 72 | format.setUnderlineStyle(QTextCharFormat::WaveUnderline); 73 | // SpellCheckUnderline 74 | setFormat(0, string.size(), format); 75 | } 76 | } 77 | }; 78 | 79 | class FindHighlight : public SyntaxHighlighter 80 | { 81 | Q_OBJECT 82 | public: 83 | FindHighlight(const QString &str, TextEdit *edit) 84 | : SyntaxHighlighter(edit), match(str) 85 | {} 86 | 87 | virtual void highlightBlock(const QString &string) 88 | { 89 | int idx = 0; 90 | while ((idx = string.indexOf(match, idx)) != -1) { 91 | setBackgroundColor(idx++, match.size(), Qt::green); 92 | } 93 | } 94 | public slots: 95 | void setFindString(const QString &text) 96 | { 97 | match = text; 98 | rehighlight(); 99 | } 100 | private: 101 | QString match; 102 | }; 103 | 104 | 105 | 106 | class Highlighter : public SyntaxHighlighter 107 | { 108 | public: 109 | Highlighter(QWidget *parent) 110 | : SyntaxHighlighter(parent) 111 | { 112 | 113 | } 114 | 115 | void helper(int from, int size, bool blackForeground) 116 | { 117 | QTextCharFormat format; 118 | format.setBackground(blackForeground ? Qt::yellow : Qt::black); 119 | format.setForeground(blackForeground ? Qt::black : Qt::yellow); 120 | setFormat(from, size, format); 121 | // qDebug() << "setting format" << from << size << currentBlock().mid(from, size) << blackForeground; 122 | } 123 | 124 | virtual void highlightBlock(const QString &text) 125 | { 126 | enum { Space, LetterOrNumber, Other } state = Space; 127 | int last = 0; 128 | for (int i=0; ipos()); 195 | if (cursor.isValid()) { 196 | emit cursorCharacter(cursor.cursorCharacter()); 197 | } 198 | TextEdit::mouseMoveEvent(e); 199 | } 200 | signals: 201 | void cursorCharacter(const QChar &ch); 202 | }; 203 | 204 | bool add = false; 205 | class MainWindow : public QMainWindow 206 | { 207 | Q_OBJECT 208 | public: 209 | MainWindow(QWidget *parent = 0) 210 | : QMainWindow(parent), doLineNumbers(false) 211 | { 212 | findHighlight = 0; 213 | // changeSelectionTimer.start(1000, this); 214 | QString fileName = "main.cpp"; 215 | bool replay = false; 216 | bool readOnly = false; 217 | int chunkSize = -1; 218 | QTextCodec *codec = 0; 219 | QString fontFamily; 220 | int fontSize = 20; 221 | const QStringList list = QApplication::arguments().mid(1); 222 | QRegExp fontFamilyRegexp("--font=(.+)"); 223 | QRegExp fontSizeRegexp("--font-size=([0-9]+)"); 224 | TextDocument::DeviceMode mode = TextDocument::Sparse; 225 | for (int i=0; i> fileName; // reuse this variable for loading document. First QString in log file 295 | while (!file.atEnd()) { 296 | int type; 297 | ds >> type; 298 | if (type == QEvent::KeyPress || type == QEvent::KeyRelease) { 299 | int key; 300 | int modifiers; 301 | QString text; 302 | bool isAutoRepeat; 303 | int count; 304 | ds >> key; 305 | ds >> modifiers; 306 | ds >> text; 307 | ds >> isAutoRepeat; 308 | ds >> count; 309 | events.append(new QKeyEvent(static_cast(type), key, 310 | static_cast(modifiers), 311 | text, isAutoRepeat, count)); 312 | } else if (type == QEvent::Resize) { 313 | QSize size; 314 | ds >> size; 315 | events.append(new QResizeEvent(size, QSize())); 316 | } else { 317 | Q_ASSERT(type == QEvent::MouseMove || type == QEvent::MouseButtonPress || type == QEvent::MouseButtonRelease); 318 | QPoint pos; 319 | int button; 320 | int buttons; 321 | int modifiers; 322 | ds >> pos; 323 | ds >> button; 324 | ds >> buttons; 325 | ds >> modifiers; 326 | events.append(new QMouseEvent(static_cast(type), pos, 327 | static_cast(button), 328 | static_cast(buttons), 329 | static_cast(modifiers))); 330 | } 331 | } 332 | if (!events.isEmpty()) 333 | QTimer::singleShot(0, this, SLOT(sendNextEvent())); 334 | } 335 | 336 | QWidget *w = new QWidget(this); 337 | QVBoxLayout *l = new QVBoxLayout(w); 338 | setCentralWidget(w); 339 | l->addWidget(textEdit = new Editor(w)); 340 | textEdit->setCursorWidth(10); 341 | textEdit->setObjectName("primary"); 342 | if (chunkSize != -1) { 343 | textEdit->document()->setChunkSize(chunkSize); 344 | } 345 | l->addWidget(otherEdit = new Editor); 346 | connect(textEdit, SIGNAL(cursorCharacter(QChar)), 347 | this, SLOT(onCursorCharacterChanged(QChar))); 348 | connect(otherEdit, SIGNAL(cursorCharacter(QChar)), 349 | this, SLOT(onCursorCharacterChanged(QChar))); 350 | otherEdit->setReadOnly(true); 351 | otherEdit->hide(); 352 | otherEdit->setLineBreaking(false); 353 | otherEdit->setObjectName("otherEdit"); 354 | textEdit->setReadOnly(readOnly); 355 | findEdit = new QLineEdit; 356 | l->addWidget(findEdit); 357 | connect(findEdit, SIGNAL(textChanged(QString)), this, SLOT(onFindEditTextChanged(QString))); 358 | QShortcut *shortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F), this); 359 | connect(shortcut, SIGNAL(activated()), findEdit, SLOT(show())); 360 | connect(shortcut, SIGNAL(activated()), findEdit, SLOT(setFocus())); 361 | 362 | shortcut = new QShortcut(QKeySequence(Qt::Key_Escape), findEdit); 363 | connect(shortcut, SIGNAL(activated()), findEdit, SLOT(clear())); 364 | connect(shortcut, SIGNAL(activated()), findEdit, SLOT(hide())); 365 | findEdit->hide(); 366 | 367 | if (!fontFamily.isEmpty()) { 368 | QFontDatabase fdb; 369 | foreach(const QString &family, fdb.families()) { 370 | if (fdb.isFixedPitch(family)) { 371 | fontFamily = family; 372 | break; 373 | } 374 | } 375 | } 376 | QFont f(fontFamily, fontSize); 377 | textEdit->setFont(f); 378 | textEdit->addSyntaxHighlighter(new Highlighter(textEdit)); 379 | textEdit->addSyntaxHighlighter(new BlockLight(textEdit)); 380 | textEdit->addSyntaxHighlighter(new SpellCheck(textEdit)); 381 | #ifndef QT_NO_DEBUG_STREAM 382 | if (codec) { 383 | qDebug() << "using codec" << codec->name(); 384 | } 385 | #endif 386 | if (appendTimer.isActive()) 387 | textEdit->document()->setOption(TextDocument::SwapChunks, true); 388 | if (!fileName.isEmpty() && !textEdit->load(fileName, mode, codec)) { 389 | #ifndef QT_NO_DEBUG_STREAM 390 | qDebug() << "Can't load" << fileName; 391 | #endif 392 | } 393 | #if 0 394 | textEdit->document()->setText("This is a test"); 395 | // textEdit->setSyntaxHighlighter(new Yellow(textEdit)); 396 | QTextCharFormat format; 397 | format.setBackground(Qt::red); 398 | textEdit->document()->insertTextSection(0, 4, format)->setPriority(10000); 399 | format = QTextCharFormat(); 400 | format.setBackground(Qt::blue); 401 | textEdit->insertTextSection(0, 7, format); 402 | #endif 403 | otherEdit->setDocument(textEdit->document()); 404 | 405 | lbl = new QLabel(w); 406 | connect(textEdit, SIGNAL(cursorPositionChanged(int)), 407 | this, SLOT(onCursorPositionChanged(int))); 408 | l->addWidget(lbl); 409 | 410 | new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_E), textEdit, SLOT(ensureCursorVisible())); 411 | new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_L), this, SLOT(createTextSection())); 412 | new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_M), this, SLOT(markLine())); 413 | new QShortcut(QKeySequence(QKeySequence::Close), this, SLOT(close())); 414 | new QShortcut(QKeySequence(Qt::Key_F2), this, SLOT(changeTextSectionFormat())); 415 | 416 | new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_S), this, SLOT(save())); 417 | new QShortcut(QKeySequence(Qt::ALT + Qt::Key_S), this, SLOT(saveAs())); 418 | 419 | QMenu *menu = menuBar()->addMenu("&File"); 420 | menu->addAction("About textedit", this, SLOT(about())); 421 | menu->addAction("&Quit", this, SLOT(close())); 422 | 423 | QHBoxLayout *h = new QHBoxLayout; 424 | h->addWidget(box = new QSpinBox(centralWidget())); 425 | connect(textEdit->verticalScrollBar(), SIGNAL(valueChanged(int)), 426 | this, SLOT(onScrollBarValueChanged())); 427 | 428 | new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_G), this, SLOT(gotoPos())); 429 | 430 | box->setReadOnly(true); 431 | box->setRange(0, INT_MAX); 432 | l->addLayout(h); 433 | 434 | connect(textEdit, SIGNAL(sectionClicked(TextSection *, QPoint)), this, SLOT(onTextSectionClicked(TextSection *, QPoint))); 435 | 436 | textEdit->viewport()->setAutoFillBackground(true); 437 | connect(textEdit->document(), SIGNAL(modificationChanged(bool)), this, SLOT(onModificationChanged(bool))); 438 | } 439 | 440 | void timerEvent(QTimerEvent *e) 441 | { 442 | if (e->timerId() == appendTimer.timerId()) { 443 | static int line = 0; 444 | textEdit->append(QString("This is line number %1\n").arg(++line)); 445 | TextCursor curs = textEdit->textCursor(); 446 | curs.movePosition(TextCursor::End); 447 | curs.movePosition(TextCursor::PreviousBlock); 448 | textEdit->setTextCursor(curs); 449 | textEdit->ensureCursorVisible(); 450 | if (line % 100 == 0) { 451 | qDebug() << "memory used" << textEdit->document()->currentMemoryUsage() 452 | << "documentSize" << textEdit->document()->documentSize(); 453 | } 454 | } else if (e->timerId() == changeSelectionTimer.timerId()) { 455 | TextCursor &cursor = textEdit->textCursor(); 456 | cursor.setSelection(rand() % 1000, (rand() % 20) - 10); 457 | } else { 458 | QMainWindow::timerEvent(e); 459 | } 460 | } 461 | 462 | void closeEvent(QCloseEvent *e) 463 | { 464 | QSettings("LazyTextEditor", "LazyTextEditor").setValue("geometry", saveGeometry()); 465 | QMainWindow::closeEvent(e); 466 | } 467 | void showEvent(QShowEvent *e) 468 | { 469 | activateWindow(); 470 | raise(); 471 | restoreGeometry(QSettings("LazyTextEditor", "LazyTextEditor").value("geometry").toByteArray()); 472 | QMainWindow::showEvent(e); 473 | } 474 | public slots: 475 | void markLine() 476 | { 477 | TextCursor cursor = textEdit->textCursor(); 478 | cursor.movePosition(TextCursor::StartOfLine); 479 | cursor.movePosition(TextCursor::Down, TextCursor::KeepAnchor); 480 | cursor.movePosition(TextCursor::EndOfLine, TextCursor::KeepAnchor); 481 | QTextCharFormat format; 482 | format.setBackground(Qt::red); 483 | TextEdit::ExtraSelection selection = { cursor, format }; 484 | textEdit->setExtraSelections(QList() << selection); 485 | } 486 | void onModificationChanged(bool on) 487 | { 488 | QPalette pal = textEdit->viewport()->palette(); 489 | pal.setColor(textEdit->viewport()->backgroundRole(), on ? Qt::yellow : Qt::white); 490 | textEdit->viewport()->setPalette(pal); 491 | } 492 | 493 | void save() 494 | { 495 | textEdit->document()->save(); 496 | } 497 | 498 | void saveAs() 499 | { 500 | const QString str = QFileDialog::getSaveFileName(this, "Save as", "."); 501 | if (!str.isEmpty()) 502 | textEdit->document()->save(str); 503 | } 504 | void about() 505 | { 506 | textEdit->textCursor().setPosition(0); 507 | QMessageBox::information(this, "Textedit", QString("Current memory usage %1KB"). 508 | arg(textEdit->document()->currentMemoryUsage() / 1024), 509 | QMessageBox::Ok, QMessageBox::NoButton, QMessageBox::NoButton); 510 | } 511 | 512 | void onCursorPositionChanged(int pos) 513 | { 514 | QString text = QString("Position: %1\n" 515 | "Word: %2\n" 516 | "Column: %3\n"). 517 | arg(pos). 518 | arg(textEdit->textCursor().wordUnderCursor()). 519 | arg(textEdit->textCursor().columnNumber()); 520 | 521 | if (doLineNumbers) 522 | text += QString("Line number: %0").arg(textEdit->document()->lineNumber(pos)); 523 | lbl->setText(text); 524 | } 525 | 526 | void sendNextEvent() 527 | { 528 | QEvent *ev = events.takeFirst(); 529 | QWidget *target = textEdit->viewport(); 530 | switch (ev->type()) { 531 | case QEvent::Resize: 532 | resize(static_cast(ev)->size()); 533 | break; 534 | case QEvent::KeyPress: 535 | if (static_cast(ev)->matches(QKeySequence::Close)) 536 | break; 537 | // fall through 538 | target = textEdit; 539 | default: 540 | QApplication::postEvent(target, ev); 541 | break; 542 | } 543 | 544 | if (!events.isEmpty()) { 545 | QTimer::singleShot(0, this, SLOT(sendNextEvent())); 546 | #ifndef QT_NO_DEBUG 547 | } else if (add) { 548 | extern bool doLog; // from textedit.cpp 549 | doLog = true; 550 | #endif 551 | } 552 | } 553 | 554 | void createTextSection() 555 | { 556 | TextCursor cursor = textEdit->textCursor(); 557 | if (cursor.hasSelection()) { 558 | static bool first = true; 559 | QTextCharFormat format; 560 | if (first) { 561 | format.setForeground(Qt::blue); 562 | format.setFontUnderline(true); 563 | } else { 564 | format.setBackground(Qt::black); 565 | format.setForeground(Qt::white); 566 | } 567 | const int pos = cursor.selectionStart(); 568 | const int size = cursor.selectionEnd() - pos; 569 | TextSection *s = 0; 570 | if (first) { 571 | s = textEdit->insertTextSection(pos, size, format, cursor.selectedText()); 572 | if (s) { 573 | s->setCursor(Qt::PointingHandCursor); 574 | Q_UNUSED(s); 575 | Q_ASSERT(s); 576 | } 577 | 578 | Q_ASSERT(!otherEdit->sections().contains(s)); 579 | Q_ASSERT(!s || textEdit->sections().contains(s)); 580 | } else { 581 | s = textEdit->document()->insertTextSection(pos, size, format, cursor.selectedText()); 582 | } 583 | first = !first; 584 | } 585 | } 586 | 587 | void changeTextSectionFormat() 588 | { 589 | static bool first = true; 590 | QTextCharFormat format; 591 | format.setFontUnderline(true); 592 | if (first) { 593 | format.setForeground(Qt::white); 594 | format.setBackground(Qt::blue); 595 | } else { 596 | format.setForeground(Qt::blue); 597 | } 598 | first = !first; 599 | foreach(TextSection *s, textEdit->sections()) { 600 | s->setFormat(format); 601 | } 602 | 603 | } 604 | void onTextSectionClicked(TextSection *section, const QPoint &pos) 605 | { 606 | #ifndef QT_NO_DEBUG_STREAM 607 | qDebug() << section->text() << section->data() << pos; 608 | #endif 609 | } 610 | void onScrollBarValueChanged() 611 | { 612 | box->setValue(textEdit->viewportPosition()); 613 | } 614 | void gotoPos() 615 | { 616 | TextCursor &cursor = textEdit->textCursor(); 617 | bool ok; 618 | int pos = QInputDialog::getInt(this, "Goto pos", "Pos", cursor.position(), 0, 619 | textEdit->document()->documentSize(), 1, &ok); 620 | if (!ok) 621 | return; 622 | cursor.setPosition(pos); 623 | } 624 | 625 | void onCursorCharacterChanged(const QChar &ch) 626 | { 627 | setWindowTitle(ch); 628 | } 629 | 630 | public slots: 631 | void onFindEditTextChanged(const QString &string) 632 | { 633 | if (string.isEmpty()) { 634 | delete findHighlight; 635 | findHighlight = 0; 636 | } else if (!findHighlight) { 637 | findHighlight = new FindHighlight(string, textEdit); 638 | } else { 639 | findHighlight->setFindString(string); 640 | } 641 | } 642 | private: 643 | QSpinBox *box; 644 | TextEdit *textEdit, *otherEdit; 645 | QLabel *lbl; 646 | QLinkedList events; 647 | bool doLineNumbers; 648 | QBasicTimer appendTimer, changeSelectionTimer; 649 | FindHighlight *findHighlight; 650 | QLineEdit *findEdit; 651 | }; 652 | 653 | #include "main.moc" 654 | 655 | 656 | int main(int argc, char **argv) 657 | { 658 | QApplication a(argc, argv); 659 | MainWindow w; 660 | w.resize(500, 100); 661 | w.show(); 662 | return a.exec(); 663 | } 664 | -------------------------------------------------------------------------------- /textcursor.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include "textcursor.h" 16 | #include "textcursor_p.h" 17 | #include "textdocument.h" 18 | #include "textdocument_p.h" 19 | #include "textedit_p.h" 20 | #include "textlayout_p.h" 21 | #include "textedit.h" 22 | 23 | class SelectionChangedEmitter 24 | { 25 | public: 26 | SelectionChangedEmitter(TextEdit *t) 27 | : selectionStart(-1), selectionEnd(-1), textEdit(t) 28 | { 29 | if (textEdit) { 30 | selectionStart = textEdit->textCursor().selectionStart(); 31 | selectionEnd = textEdit->textCursor().selectionEnd(); 32 | } 33 | } 34 | 35 | ~SelectionChangedEmitter() 36 | { 37 | if (textEdit) { 38 | const TextCursor &cursor = textEdit->textCursor(); 39 | if (((selectionStart != selectionEnd) != cursor.hasSelection()) 40 | || (selectionStart != selectionEnd 41 | && (selectionStart != cursor.selectionStart() 42 | || selectionEnd != cursor.selectionEnd()))) { 43 | QMetaObject::invokeMethod(textEdit, "selectionChanged"); 44 | } 45 | } 46 | } 47 | private: 48 | int selectionStart, selectionEnd; 49 | TextEdit *textEdit; 50 | }; 51 | 52 | TextCursor::TextCursor() 53 | : d(0), textEdit(0) 54 | { 55 | } 56 | 57 | TextCursor::TextCursor(const TextDocument *document, int pos, int anc) 58 | : d(0), textEdit(0) 59 | { 60 | if (document) { 61 | const int documentSize = document->d->documentSize; 62 | if (pos < 0 || pos > documentSize || anc < -1 || anc > documentSize) { 63 | #ifndef LAZYTEXTEDIT_AUTOTEST 64 | qWarning("Invalid cursor data %d %d - %d\n", 65 | pos, anc, documentSize); 66 | Q_ASSERT(0); 67 | #endif 68 | return; 69 | } 70 | d = new TextCursorSharedPrivate; 71 | d->document = const_cast(document); 72 | d->position = pos; 73 | d->anchor = anc == -1 ? pos : anc; 74 | d->document->d->textCursors.insert(d); 75 | } 76 | } 77 | 78 | TextCursor::TextCursor(const TextEdit *edit, int pos, int anc) 79 | : d(0), textEdit(0) 80 | { 81 | if (edit) { 82 | TextDocument *document = edit->document(); 83 | const int documentSize = document->d->documentSize; 84 | if (pos < 0 || pos > documentSize || anc < -1 || anc > documentSize) { 85 | #ifndef LAZYTEXTEDIT_AUTOTEST 86 | qWarning("Invalid cursor data %d %d - %d\n", 87 | pos, anc, documentSize); 88 | Q_ASSERT(0); 89 | #endif 90 | return; 91 | } 92 | d = new TextCursorSharedPrivate; 93 | d->document = const_cast(document); 94 | d->position = pos; 95 | d->anchor = anc == -1 ? pos : anc; 96 | d->document->d->textCursors.insert(d); 97 | } 98 | } 99 | 100 | TextCursor::TextCursor(const TextCursor &cursor) 101 | : d(cursor.d), textEdit(0) 102 | { 103 | ref(); 104 | } 105 | 106 | TextCursor &TextCursor::operator=(const TextCursor &other) 107 | { 108 | deref(); 109 | d = other.d; 110 | textEdit = 0; 111 | ref(); 112 | return *this; 113 | } 114 | 115 | TextCursor::~TextCursor() 116 | { 117 | deref(); 118 | } 119 | 120 | bool TextCursor::isNull() const 121 | { 122 | return !d || !d->document; 123 | } 124 | 125 | TextDocument *TextCursor::document() const 126 | { 127 | return d ? d->document : 0; 128 | } 129 | 130 | void TextCursor::setSelection(int pos, int length) // can be negative 131 | { 132 | setPosition(pos + length); 133 | if (length != 0) 134 | setPosition(pos, KeepAnchor); 135 | } 136 | 137 | void TextCursor::setPosition(int pos, MoveMode mode) 138 | { 139 | Q_ASSERT(!isNull()); 140 | d->overrideColumn = -1; 141 | if (pos < 0 || pos > d->document->documentSize()) { 142 | clearSelection(); 143 | return; 144 | } else if (pos == d->position && (mode == KeepAnchor || d->anchor == d->position)) { 145 | return; 146 | } 147 | 148 | SelectionChangedEmitter emitter(textEdit); 149 | detach(); 150 | cursorChanged(false); 151 | 152 | #if 0 153 | Link *link = d->document->links(pos, 1).value(0, 0); 154 | if (link && link->position < pos && link->position + link->size > pos) { // inside a link 155 | pos = (pos > d->position ? link->position + link->size : link->position); 156 | } 157 | #endif 158 | 159 | if (mode == TextCursor::MoveAnchor) { 160 | clearSelection(); 161 | } 162 | 163 | d->position = pos; 164 | if (mode == MoveAnchor) { 165 | d->anchor = pos; 166 | } 167 | cursorChanged(true); 168 | } 169 | 170 | int TextCursor::position() const 171 | { 172 | return isNull() ? -1 : d->position; 173 | } 174 | 175 | int TextCursor::anchor() const 176 | { 177 | return isNull() ? -1 : d->anchor; 178 | } 179 | 180 | void TextCursor::insertText(const QString &text) 181 | { 182 | Q_ASSERT(!isNull()); 183 | const bool doJoin = hasSelection(); 184 | if (doJoin) { 185 | removeSelectedText(); 186 | } 187 | const bool old = d->document->d->cursorCommand; 188 | d->document->d->cursorCommand = true; 189 | if (d->document->insert(d->position, text) && textEdit) { 190 | emit textEdit->cursorPositionChanged(d->position); 191 | } 192 | if (doJoin) 193 | d->document->d->joinLastTwoCommands(); 194 | d->document->d->cursorCommand = old; 195 | } 196 | 197 | bool TextCursor::movePosition(TextCursor::MoveOperation op, TextCursor::MoveMode mode, int n) 198 | { 199 | if (!d || !d->document || n <= 0) 200 | return false; 201 | 202 | switch (op) { 203 | case Start: 204 | case StartOfLine: 205 | case End: 206 | case EndOfLine: 207 | n = 1; 208 | break; 209 | default: 210 | break; 211 | } 212 | 213 | if (n > 1) { 214 | while (n > 0 && movePosition(op, mode, 1)) 215 | --n; 216 | return n == 0; 217 | } 218 | detach(); 219 | 220 | switch (op) { 221 | case NoMove: 222 | return true; 223 | 224 | case PreviousCharacter: 225 | case NextCharacter: 226 | return movePosition(op == TextCursor::PreviousCharacter 227 | ? TextCursor::Left : TextCursor::Right, mode); 228 | 229 | case End: 230 | case Start: 231 | setPosition(op == TextCursor::Start ? 0 : d->document->documentSize(), mode); 232 | break; 233 | 234 | case Up: 235 | case Down: { 236 | TextLayout *textLayout = TextLayoutCacheManager::requestLayout(*this, 1); // should I use 1? 237 | Q_ASSERT(textLayout); 238 | int index; 239 | bool currentIsLast; 240 | const QTextLine currentLine = textLayout->lineForPosition(d->position, 0, 241 | &index, 242 | ¤tIsLast); 243 | Q_ASSERT(textLayout->lines.size() <= 1 || (index != -1 && currentLine.isValid())); 244 | if (!currentLine.isValid()) 245 | return false; 246 | const int col = columnNumber(); 247 | int targetLinePos; 248 | if (op == Up) { 249 | targetLinePos = d->position - col - 1; 250 | // qDebug() << "I was at column" << col << "and position" 251 | // << d->position << "so naturally I can find the previous line around" 252 | // << (d->position - col - 1); 253 | } else { 254 | targetLinePos = d->position + currentLine.textLength() - col 255 | + (currentIsLast ? 1 : 0); 256 | // qDebug() << "currentLine.textLength" << currentLine.textLength() << "col" << col 257 | // << "currentIsLast" << currentIsLast; 258 | // ### probably need to add only if last line in layout 259 | } 260 | if (targetLinePos < 0) { 261 | if (d->position == 0) { 262 | return false; 263 | } else { 264 | setPosition(0, mode); 265 | return true; 266 | } 267 | } else if (targetLinePos >= d->document->documentSize()) { 268 | if (d->position == d->document->documentSize()) { 269 | return false; 270 | } else { 271 | setPosition(d->document->documentSize(), mode); 272 | return true; 273 | } 274 | } 275 | int offsetInLine; 276 | 277 | const QTextLine targetLine = textLayout->lineForPosition(targetLinePos, &offsetInLine); 278 | if (!targetLine.isValid()) 279 | return false; 280 | targetLinePos -= offsetInLine; // targetLinePos should now be at col 0 281 | 282 | // qDebug() << "finding targetLine at" << targetLinePos 283 | // << d->document->read(targetLinePos, 7) 284 | // << "d->position" << d->position 285 | // << "col" << col 286 | // << "offsetInLine" << offsetInLine; 287 | 288 | int gotoCol = qMax(d->overrideColumn, col); 289 | if (gotoCol > targetLine.textLength()) { 290 | d->overrideColumn = qMax(d->overrideColumn, gotoCol); 291 | gotoCol = targetLine.textLength(); 292 | } 293 | const int overrideColumn = d->overrideColumn; 294 | setPosition(targetLinePos + gotoCol, mode); 295 | d->overrideColumn = overrideColumn; 296 | break; } 297 | 298 | case EndOfLine: 299 | case StartOfLine: { 300 | int offset; 301 | bool lastLine; 302 | TextLayout *textLayout = TextLayoutCacheManager::requestLayout(*this, 1); 303 | QTextLine line = textLayout->lineForPosition(position(), &offset, 304 | 0, &lastLine); 305 | if (!line.isValid()) 306 | return false; 307 | if (op == TextCursor::StartOfLine) { 308 | setPosition(position() - offset, mode); 309 | } else { 310 | int pos = position() - offset + line.textLength(); 311 | if (!lastLine) 312 | --pos; 313 | setPosition(pos, mode); 314 | } 315 | break; } 316 | 317 | case StartOfBlock: { 318 | TextDocumentIterator it(d->document->d, d->position); 319 | const QLatin1Char newline('\n'); 320 | while (it.hasPrevious() && it.previous() != newline) ; 321 | if (it.hasPrevious()) 322 | it.next(); 323 | setPosition(it.position(), mode); 324 | break; } 325 | case EndOfBlock: { 326 | TextDocumentIterator it(d->document->d, d->position); 327 | const QLatin1Char newline('\n'); 328 | while (it.current() != newline && it.hasNext() && it.next() != newline) ; 329 | setPosition(it.position(), mode); 330 | break; } 331 | 332 | case StartOfWord: 333 | case PreviousWord: 334 | case WordLeft: { 335 | TextDocumentIterator it(d->document->d, d->position); 336 | 337 | while (it.hasPrevious()) { 338 | const QChar ch = it.previous(); 339 | if (d->document->isWordCharacter(ch, it.position())) 340 | break; 341 | } 342 | while (it.hasPrevious()) { 343 | const QChar ch = it.previous(); 344 | if (!d->document->isWordCharacter(ch, it.position())) 345 | break; 346 | } 347 | if (it.hasPrevious()) 348 | it.next(); 349 | setPosition(it.position(), mode); 350 | d->overrideColumn = -1; 351 | break; } 352 | 353 | case NextWord: 354 | case WordRight: 355 | case EndOfWord: { 356 | TextDocumentIterator it(d->document->d, d->position); 357 | while (it.hasNext()) { 358 | const QChar ch = it.next(); 359 | if (d->document->isWordCharacter(ch, it.position())) 360 | break; 361 | } 362 | while (it.hasNext()) { 363 | const QChar ch = it.next(); 364 | if (!d->document->isWordCharacter(ch, it.position())) 365 | break; 366 | } 367 | setPosition(it.position(), mode); 368 | d->overrideColumn = -1; 369 | break; } 370 | 371 | case PreviousBlock: 372 | movePosition(TextCursor::StartOfBlock, mode); 373 | movePosition(TextCursor::Left, mode); 374 | movePosition(TextCursor::StartOfBlock, mode); 375 | break; 376 | 377 | case NextBlock: 378 | movePosition(TextCursor::EndOfBlock, mode); 379 | movePosition(TextCursor::Right, mode); 380 | return true; 381 | 382 | case Left: 383 | case Right: 384 | d->overrideColumn = -1; 385 | setPosition(qBound(0, position() + (op == TextCursor::Left ? -1 : 1), 386 | d->document->documentSize()), mode); 387 | break; 388 | }; 389 | 390 | return true; 391 | } 392 | 393 | void TextCursor::deleteChar() 394 | { 395 | Q_ASSERT(!isNull()); 396 | if (hasSelection()) { 397 | removeSelectedText(); 398 | } else if (d->position < d->document->documentSize()) { 399 | const bool old = d->document->d->cursorCommand; 400 | d->document->d->cursorCommand = true; 401 | d->document->remove(d->position, 1); 402 | d->document->d->cursorCommand = old; 403 | } 404 | } 405 | 406 | void TextCursor::deletePreviousChar() 407 | { 408 | Q_ASSERT(!isNull()); 409 | if (hasSelection()) { 410 | removeSelectedText(); 411 | } else if (d->position > 0) { 412 | const bool old = d->document->d->cursorCommand; 413 | d->document->d->cursorCommand = true; 414 | d->document->remove(d->position - 1, 1); 415 | d->document->d->cursorCommand = old; 416 | d->anchor = --d->position; 417 | } 418 | } 419 | 420 | void TextCursor::select(SelectionType selection) 421 | { 422 | if (!d || !d->document) 423 | return; 424 | 425 | clearSelection(); 426 | 427 | switch (selection) { 428 | case LineUnderCursor: 429 | movePosition(StartOfLine); 430 | movePosition(EndOfLine, KeepAnchor); 431 | break; 432 | case WordUnderCursor: 433 | movePosition(StartOfWord); 434 | movePosition(EndOfWord, KeepAnchor); 435 | break; 436 | case BlockUnderCursor: 437 | movePosition(StartOfBlock); 438 | // also select the paragraph separator 439 | if (movePosition(PreviousBlock)) { 440 | movePosition(EndOfBlock); 441 | movePosition(NextBlock, KeepAnchor); 442 | } 443 | movePosition(EndOfBlock, KeepAnchor); 444 | break; 445 | } 446 | } 447 | 448 | bool TextCursor::hasSelection() const 449 | { 450 | return !isNull() && d->anchor != d->position; 451 | } 452 | 453 | void TextCursor::removeSelectedText() 454 | { 455 | Q_ASSERT(!isNull()); 456 | if (d->anchor == d->position) 457 | return; 458 | 459 | SelectionChangedEmitter emitter(textEdit); 460 | detach(); 461 | cursorChanged(false); 462 | const int min = qMin(d->anchor, d->position); 463 | const int max = qMax(d->anchor, d->position); 464 | d->anchor = d->position = min; 465 | const bool old = d->document->d->cursorCommand; 466 | d->document->d->cursorCommand = true; 467 | d->document->remove(min, max - min); 468 | d->document->d->cursorCommand = old; 469 | cursorChanged(true); 470 | } 471 | 472 | void TextCursor::clearSelection() 473 | { 474 | Q_ASSERT(!isNull()); 475 | if (hasSelection()) { 476 | detach(); 477 | SelectionChangedEmitter emitter(textEdit); 478 | d->anchor = d->position; 479 | } 480 | } 481 | 482 | int TextCursor::selectionStart() const 483 | { 484 | return qMin(d->anchor, d->position); 485 | } 486 | 487 | int TextCursor::selectionEnd() const 488 | { 489 | return qMax(d->anchor, d->position); 490 | } 491 | 492 | int TextCursor::selectionSize() const 493 | { 494 | return selectionEnd() - selectionStart(); 495 | } 496 | 497 | 498 | QString TextCursor::selectedText() const 499 | { 500 | if (isNull() || d->anchor == d->position) 501 | return QString(); 502 | 503 | const int min = qMin(d->anchor, d->position); 504 | const int max = qMax(d->anchor, d->position); 505 | return d->document->read(min, max - min); 506 | } 507 | 508 | bool TextCursor::atBlockStart() const 509 | { 510 | Q_ASSERT(!isNull()); 511 | return atStart() || d->document->read(d->position - 1, 1).at(0) == '\n'; 512 | } 513 | 514 | bool TextCursor::atBlockEnd() const 515 | { 516 | Q_ASSERT(!isNull()); 517 | return atEnd() || d->document->read(d->position, 1).at(0) == '\n'; // ### is this right? 518 | } 519 | 520 | bool TextCursor::atStart() const 521 | { 522 | Q_ASSERT(!isNull()); 523 | return d->position == 0; 524 | } 525 | 526 | bool TextCursor::atEnd() const 527 | { 528 | Q_ASSERT(!isNull()); 529 | return d->position == d->document->documentSize(); 530 | } 531 | 532 | 533 | bool TextCursor::operator!=(const TextCursor &rhs) const 534 | { 535 | return !(*this == rhs); 536 | } 537 | 538 | bool TextCursor::operator<(const TextCursor &rhs) const 539 | { 540 | if (!d) 541 | return true; 542 | 543 | if (!rhs.d) 544 | return false; 545 | 546 | Q_ASSERT_X(d->document == rhs.d->document, "TextCursor::operator<", 547 | "cannot compare cursors attached to different documents"); 548 | 549 | return d->position < rhs.d->position; 550 | } 551 | 552 | bool TextCursor::operator<=(const TextCursor &rhs) const 553 | { 554 | return *this < rhs || *this == rhs; 555 | } 556 | 557 | bool TextCursor::operator==(const TextCursor &rhs) const 558 | { 559 | if (isCopyOf(rhs)) 560 | return true; 561 | 562 | if (!d || !rhs.d) 563 | return false; 564 | 565 | return (d->position == rhs.d->position 566 | && d->anchor == rhs.d->anchor 567 | && d->document == rhs.d->document); 568 | } 569 | 570 | bool TextCursor::operator>=(const TextCursor &rhs) const 571 | { 572 | return *this > rhs || *this == rhs; 573 | } 574 | 575 | bool TextCursor::operator>(const TextCursor &rhs) const 576 | { 577 | if (!d) 578 | return false; 579 | 580 | if (!rhs.d) 581 | return true; 582 | 583 | Q_ASSERT_X(d->document == rhs.d->document, "TextCursor::operator>=", 584 | "cannot compare cursors attached to different documents"); 585 | 586 | return d->position > rhs.d->position; 587 | } 588 | 589 | bool TextCursor::isCopyOf(const TextCursor &other) const 590 | { 591 | return d == other.d; 592 | } 593 | 594 | int TextCursor::columnNumber() const 595 | { 596 | Q_ASSERT(d && d->document); 597 | TextLayout *textLayout = TextLayoutCacheManager::requestLayout(*this, 0); 598 | int col; 599 | textLayout->lineForPosition(d->position, &col); 600 | return col; 601 | } 602 | 603 | int TextCursor::lineNumber() const 604 | { 605 | Q_ASSERT(d && d->document); 606 | return d->document->lineNumber(position()); 607 | } 608 | 609 | void TextCursor::detach() 610 | { 611 | Q_ASSERT(d); 612 | if (d->ref > 1) { 613 | d->ref.deref(); 614 | TextCursorSharedPrivate *p = new TextCursorSharedPrivate; 615 | p->position = d->position; 616 | p->overrideColumn = d->overrideColumn; 617 | p->anchor = d->anchor; 618 | p->document = d->document; 619 | d->document->d->textCursors.insert(p); 620 | d = p; 621 | } 622 | } 623 | 624 | bool TextCursor::ref() 625 | { 626 | return d && d->ref.ref(); 627 | } 628 | 629 | bool TextCursor::deref() 630 | { 631 | TextCursorSharedPrivate *dd = d; 632 | d = 0; 633 | if (dd && !dd->ref.deref()) { 634 | if (dd->document) { 635 | const bool removed = dd->document->d->textCursors.remove(dd); 636 | Q_ASSERT(removed); 637 | Q_UNUSED(removed) 638 | } 639 | delete dd; 640 | return false; 641 | } 642 | return true; 643 | } 644 | 645 | void TextCursor::cursorChanged(bool ensureVisible) 646 | { 647 | if (textEdit) { 648 | if (textEdit->d->cursorBlinkTimer.isActive()) 649 | textEdit->d->cursorVisible = true; 650 | if (ensureVisible) { 651 | emit textEdit->cursorPositionChanged(d->position); 652 | textEdit->ensureCursorVisible(); 653 | } 654 | const QRect r = textEdit->cursorBlockRect(*this) & textEdit->viewport()->rect(); 655 | if (!r.isNull()) { 656 | textEdit->viewport()->update(r); 657 | } 658 | } 659 | } 660 | 661 | bool TextCursor::cursorMoveKeyEvent(QKeyEvent *e) 662 | { 663 | MoveMode mode = MoveAnchor; 664 | MoveOperation op = NoMove; 665 | 666 | if (e == QKeySequence::MoveToNextChar) { 667 | op = Right; 668 | } else if (e == QKeySequence::MoveToPreviousChar) { 669 | op = Left; 670 | } else if (e == QKeySequence::SelectNextChar) { 671 | op = Right; 672 | mode = KeepAnchor; 673 | } else if (e == QKeySequence::SelectPreviousChar) { 674 | op = Left; 675 | mode = KeepAnchor; 676 | } else if (e == QKeySequence::SelectNextWord) { 677 | op = WordRight; 678 | mode = KeepAnchor; 679 | } else if (e == QKeySequence::SelectPreviousWord) { 680 | op = WordLeft; 681 | mode = KeepAnchor; 682 | } else if (e == QKeySequence::SelectStartOfLine) { 683 | op = StartOfLine; 684 | mode = KeepAnchor; 685 | } else if (e == QKeySequence::SelectEndOfLine) { 686 | op = EndOfLine; 687 | mode = KeepAnchor; 688 | } else if (e == QKeySequence::SelectStartOfBlock) { 689 | op = StartOfBlock; 690 | mode = KeepAnchor; 691 | } else if (e == QKeySequence::SelectEndOfBlock) { 692 | op = EndOfBlock; 693 | mode = KeepAnchor; 694 | } else if (e == QKeySequence::SelectStartOfDocument) { 695 | op = Start; 696 | mode = KeepAnchor; 697 | } else if (e == QKeySequence::SelectEndOfDocument) { 698 | op = End; 699 | mode = KeepAnchor; 700 | } else if (e == QKeySequence::SelectPreviousLine) { 701 | op = Up; 702 | mode = KeepAnchor; 703 | } else if (e == QKeySequence::SelectNextLine) { 704 | op = Down; 705 | mode = KeepAnchor; 706 | #if 0 707 | { 708 | QTextBlock block = cursor.block(); 709 | QTextLine line = currentTextLine(cursor); 710 | if (!block.next().isValid() 711 | && line.isValid() 712 | && line.lineNumber() == block.layout()->lineCount() - 1) 713 | op = End; 714 | } 715 | #endif 716 | } else if (e == QKeySequence::MoveToNextWord) { 717 | op = WordRight; 718 | } else if (e == QKeySequence::MoveToPreviousWord) { 719 | op = WordLeft; 720 | } else if (e == QKeySequence::MoveToEndOfBlock) { 721 | op = EndOfBlock; 722 | } else if (e == QKeySequence::MoveToStartOfBlock) { 723 | op = StartOfBlock; 724 | } else if (e == QKeySequence::MoveToNextLine) { 725 | op = Down; 726 | } else if (e == QKeySequence::MoveToPreviousLine) { 727 | op = Up; 728 | } else if (e == QKeySequence::MoveToStartOfLine) { 729 | op = StartOfLine; 730 | } else if (e == QKeySequence::MoveToEndOfLine) { 731 | op = EndOfLine; 732 | } else if (e == QKeySequence::MoveToStartOfDocument) { 733 | op = Start; 734 | } else if (e == QKeySequence::MoveToEndOfDocument) { 735 | op = End; 736 | } else if (e == QKeySequence::MoveToNextPage || e == QKeySequence::MoveToPreviousPage 737 | || e == QKeySequence::SelectNextPage || e == QKeySequence::SelectPreviousPage) { 738 | const MoveMode mode = (e == QKeySequence::MoveToNextPage 739 | || e == QKeySequence::MoveToPreviousPage 740 | ? MoveAnchor : KeepAnchor); 741 | const MoveOperation operation = (e == QKeySequence::MoveToNextPage 742 | || e == QKeySequence::SelectNextPage 743 | ? Down : Up); 744 | int visibleLines = 10; 745 | if (textEdit) 746 | visibleLines = textEdit->d->visibleLines; 747 | for (int i=0; iviewport()->width() : d->viewportWidth; 761 | } 762 | 763 | void TextCursor::setViewportWidth(int width) 764 | { 765 | if (textEdit) { 766 | qWarning("It makes no sense to set the viewportWidth of a text cursor that belongs to a text edit. The actual viewportWidth will be used"); 767 | return; 768 | } 769 | d->viewportWidth = width; 770 | } 771 | 772 | QChar TextCursor::cursorCharacter() const 773 | { 774 | Q_ASSERT(d && d->document); 775 | return d->document->readCharacter(d->anchor); 776 | } 777 | 778 | QString TextCursor::cursorLine() const 779 | { 780 | Q_ASSERT(d && d->document); 781 | int layoutIndex = -1; 782 | TextLayout *textLayout = TextLayoutCacheManager::requestLayout(*this, 2); // should I use 1? 783 | 784 | QTextLine line = textLayout->lineForPosition(position(), 0, &layoutIndex); 785 | Q_ASSERT(line.isValid()); // ### could this be legitimate? 786 | Q_ASSERT(textLayout); 787 | return textLayout->textLayouts.at(layoutIndex)->text() 788 | .mid(line.textStart(), line.textLength()); 789 | // ### there is a bug here. It doesn't seem to use the right 790 | // ### viewportWidth even though it is the textEdit's cursor 791 | } 792 | 793 | int TextCursor::lineHeight() const 794 | { 795 | int res = -1; 796 | Q_ASSERT(d && d->document); 797 | int layoutIndex = -1; 798 | TextLayout *textLayout = TextLayoutCacheManager::requestLayout(*this, 2); 799 | QTextLine line = textLayout->lineForPosition(position(), 0, &layoutIndex); 800 | if (line.isValid()) 801 | res = line.height(); 802 | return res; 803 | } 804 | 805 | 806 | QString TextCursor::wordUnderCursor() const 807 | { 808 | Q_ASSERT(!isNull()); 809 | return d->document->d->wordAt(d->position); 810 | } 811 | 812 | QString TextCursor::paragraphUnderCursor() const 813 | { 814 | Q_ASSERT(!isNull()); 815 | return d->document->d->paragraphAt(d->position); 816 | } 817 | 818 | QDebug operator<<(QDebug dbg, const TextCursor &cursor) 819 | { 820 | QString ret = QString::fromLatin1("TextCursor("); 821 | if (cursor.isNull()) { 822 | ret.append(QLatin1String("null)")); 823 | dbg.maybeSpace() << ret; 824 | } else { 825 | if (!cursor.hasSelection()) { 826 | dbg << "anchor/position:" << cursor.anchor() << "character:" << cursor.cursorCharacter(); 827 | } else { 828 | dbg << "anchor:" << cursor.anchor() << "position:" << cursor.position() 829 | << "selectionSize:" << cursor.selectionSize(); 830 | QString selectedText; 831 | if (cursor.selectionSize() > 10) { 832 | selectedText = cursor.document()->read(cursor.selectionStart(), 7); 833 | selectedText.append("..."); 834 | } else { 835 | selectedText = cursor.selectedText(); 836 | } 837 | dbg << "selectedText:" << selectedText; 838 | } 839 | } 840 | return dbg.space(); 841 | } 842 | 843 | 844 | -------------------------------------------------------------------------------- /tests/textdocument/tst_textdocument.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Anders Bakken 2 | // 3 | // Licensed under the Apache License, Version 2.0(the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | 17 | #define private public 18 | // to be able to access private data in textdocument 19 | #include 20 | #include 21 | QT_FORWARD_DECLARE_CLASS(TextDocument) 22 | 23 | //TESTED_CLASS= 24 | //TESTED_FILES= 25 | 26 | Q_DECLARE_METATYPE(TextDocument::Options); 27 | Q_DECLARE_METATYPE(TextDocument::DeviceMode); 28 | Q_DECLARE_METATYPE(QVariant); 29 | 30 | class tst_TextDocument : public QObject 31 | { 32 | Q_OBJECT 33 | 34 | public: 35 | tst_TextDocument(); 36 | virtual ~tst_TextDocument(); 37 | 38 | private: 39 | int convertRowColumnToIndex(const TextDocument *doc, int row, int column); 40 | 41 | private slots: 42 | void cursor_data(); 43 | void cursor(); 44 | void undoRedo(); 45 | void save(); 46 | void documentSize(); 47 | void setText(); 48 | void setText_data(); 49 | void readCheck(); 50 | void readCheck_data(); 51 | void find(); 52 | void find2(); 53 | void find3_data(); 54 | void find3(); 55 | void find4(); 56 | void find5(); 57 | void find6(); 58 | void findWrap(); 59 | void abortFind_data(); 60 | void abortFind(); 61 | void abortFindSleep(); 62 | void findAll_data(); 63 | void findAll(); 64 | void findQChar_data(); 65 | void findQChar(); 66 | void findWholeWordsRecursionCrash(); 67 | void sections(); 68 | void iterator(); 69 | void modified(); 70 | void unicode(); 71 | void lineNumbers(); 72 | void lineNumbersGenerated(); 73 | void lineNumbersGenerated_data(); 74 | void carriageReturns_data(); 75 | void carriageReturns(); 76 | void isWordOverride(); 77 | void chunkBacktrack(); 78 | void insertText(); 79 | void memUsage(); 80 | void textDocmentIteratorOnDocumentSize(); 81 | }; 82 | 83 | tst_TextDocument::tst_TextDocument() 84 | { 85 | } 86 | 87 | tst_TextDocument::~tst_TextDocument() 88 | { 89 | } 90 | 91 | static const int chunkSizes[] = { -1, /* default */ 1000, 100, 10, 0 }; 92 | void tst_TextDocument::readCheck_data() 93 | { 94 | QTest::addColumn("fileName"); 95 | QTest::addColumn("deviceMode"); 96 | QTest::addColumn("chunkSize"); 97 | QTest::addColumn("useIODevice"); 98 | 99 | 100 | static const char *fileNames[] = { /*"tst_textdocument.cpp", */"unicode.txt", 0 }; 101 | struct { 102 | const char *name; 103 | int deviceMode; 104 | } modes[] = { { "LoadAll", TextDocument::LoadAll }, 105 | { "Sparse", TextDocument::Sparse }, 106 | { 0, -1 } }; 107 | 108 | for (int f=0; fileNames[f]; ++f) { 109 | for (int i=0; chunkSizes[i] != 0; ++i) { 110 | for (int j=0; modes[j].name; ++j) { 111 | for (int k=0; k<2; ++k) { 112 | QTest::newRow(QString("%0 %1 %2 %3"). 113 | arg(QString::fromLatin1(fileNames[f])). 114 | arg(modes[j].name).arg(chunkSizes[i]). 115 | arg(bool(k)).toLatin1().constData()) 116 | << QString::fromLatin1(fileNames[f]) 117 | << modes[j].deviceMode 118 | << chunkSizes[i] 119 | << bool(k); 120 | } 121 | } 122 | } 123 | } 124 | } 125 | 126 | void tst_TextDocument::readCheck() 127 | { 128 | QFETCH(QString, fileName); 129 | QFETCH(int, deviceMode); 130 | QFETCH(int, chunkSize); 131 | QFETCH(bool, useIODevice); 132 | TextDocument doc; 133 | QFile file(fileName); 134 | QFile file2(useIODevice ? fileName : QString()); 135 | QVERIFY(file.open(QIODevice::ReadOnly)); 136 | if (useIODevice) 137 | QVERIFY(file2.open(QIODevice::ReadOnly)); 138 | if (chunkSize != -1) 139 | doc.setChunkSize(chunkSize); 140 | if (useIODevice) { 141 | QVERIFY(doc.load(&file2, TextDocument::DeviceMode(deviceMode))); 142 | } else { 143 | QVERIFY(doc.load(file.fileName(), TextDocument::DeviceMode(deviceMode))); 144 | } 145 | static const int sizes[] = { 1, 100, -1 }; 146 | 147 | const QString fileData = QTextStream(&file).readAll(); 148 | if (deviceMode == TextDocument::Sparse && doc.chunkSize() < 1000) 149 | QEXPECT_FAIL("", "I don't know how to fix this", Abort); 150 | 151 | QCOMPARE(fileData.size(), doc.documentSize()); 152 | for (int i=0; sizes[i] != -1; ++i) { 153 | for (int j=0; j("regExp"); 230 | QTest::addColumn("position"); 231 | QTest::addColumn("flags"); 232 | QTest::addColumn("expectedAnchor"); 233 | QTest::addColumn("expectedPosition"); 234 | QTest::addColumn("expectedText"); 235 | 236 | QTest::newRow("1") << QRegExp("This") << 0 << 0 << 0 << 4 << "This"; 237 | QTest::newRow("2") << QRegExp("This") << 1 << 0 << 17 << 21 << "This"; 238 | QTest::newRow("3") << QRegExp("This") << -1 << int(TextDocument::FindBackward) << 17 << 21 << "This"; 239 | QTest::newRow("4") << QRegExp("This") << 16 << int(TextDocument::FindBackward) << 0 << 4 << "This"; 240 | QTest::newRow("5") << QRegExp("\\bis") << 16 << int(TextDocument::FindBackward) << 5 << 7 << "is"; 241 | QTest::newRow("6") << QRegExp("is") << 0 << 0 << 2 << 4 << "is"; 242 | QTest::newRow("7") << QRegExp("\\bis") << 0 << 0 << 5 << 7 << "is"; 243 | } 244 | 245 | 246 | void tst_TextDocument::find3() 247 | { 248 | TextDocument doc; 249 | doc.setText("This is one line\nThis is another"); 250 | QFETCH(QRegExp, regExp); 251 | QFETCH(int, position); 252 | QFETCH(int, flags); 253 | QFETCH(int, expectedAnchor); 254 | QFETCH(int, expectedPosition); 255 | QFETCH(QString, expectedText); 256 | if (position == -1) 257 | position = doc.documentSize(); 258 | 259 | TextCursor cursor = doc.find(regExp, position, (TextDocument::FindMode)flags); 260 | QCOMPARE(cursor.anchor(), expectedAnchor); 261 | QCOMPARE(cursor.position(), expectedPosition); 262 | QCOMPARE(cursor.selectedText(), expectedText); 263 | QCOMPARE(doc.find(regExp, position, (TextDocument::FindMode)flags).selectedText(), expectedText); 264 | 265 | } 266 | 267 | 268 | void tst_TextDocument::setText_data() 269 | { 270 | QTest::addColumn("chunkSize"); 271 | for (int i=0; chunkSizes[i] != 0; ++i) { 272 | QTest::newRow(QString::number(chunkSizes[i]).toLatin1().constData()) << chunkSizes[i]; 273 | } 274 | } 275 | 276 | void tst_TextDocument::setText() 277 | { 278 | QString ba; 279 | const QString line("abcdefghijklmnopqrstuvwxyz\n"); 280 | for (int i=0; i<2; ++i) { 281 | ba.append(line); 282 | } 283 | 284 | QFETCH(int, chunkSize); 285 | TextDocument doc; 286 | if (chunkSize != -1) 287 | doc.setChunkSize(chunkSize); 288 | doc.setText(ba); 289 | 290 | QCOMPARE(doc.read(0, doc.documentSize()), ba); 291 | } 292 | 293 | void tst_TextDocument::save() 294 | { 295 | TextDocument doc; 296 | // doc.load("tst_textdocument.cpp"); 297 | doc.setText("testing testing testing testing testing"); 298 | doc.insert(0, "This is inserted at 0\n"); 299 | doc.insert(10, "This is inserted at 10\n"); 300 | QVERIFY(doc.read(0, doc.documentSize()).contains("This is inserted at 10")); 301 | QBuffer buffer; 302 | buffer.open(QIODevice::WriteOnly); 303 | QVERIFY(doc.save(&buffer)); 304 | QCOMPARE(doc.documentSize(), buffer.data().size()); 305 | const QString docSaved = doc.read(0, doc.documentSize()); 306 | QVERIFY(docSaved.contains("This is inserted")); 307 | buffer.close(); 308 | buffer.open(QIODevice::ReadOnly); 309 | const QString buf = QTextStream(&buffer).readAll(); 310 | // if (doc.read(0, doc.documentSize()) != buf) { 311 | if (docSaved != buf) { 312 | QVERIFY(docSaved != buf); 313 | QVERIFY(docSaved == doc.read(0, doc.documentSize())); 314 | { 315 | QFile file("buf"); 316 | file.open(QIODevice::WriteOnly); 317 | QTextStream(&file) << buf; 318 | } 319 | { 320 | QFile file("doc"); 321 | file.open(QIODevice::WriteOnly); 322 | QTextStream(&file) << docSaved; 323 | } 324 | } 325 | // QVERIFY(doc.read(0, doc.documentSize()) == buf); 326 | QVERIFY(docSaved == buf); 327 | } 328 | 329 | void tst_TextDocument::documentSize() 330 | { 331 | QBuffer buffer; 332 | buffer.setData("foobar\n"); 333 | buffer.open(QIODevice::ReadOnly); 334 | int bufferSize = buffer.size(); 335 | TextDocument doc; 336 | doc.load(&buffer); 337 | QCOMPARE(int(buffer.size()), doc.documentSize()); 338 | doc.remove(0, 2); 339 | bufferSize -= 2; 340 | QCOMPARE(bufferSize, doc.documentSize()); 341 | doc.insert(0, "foobar"); 342 | bufferSize += 6; 343 | QCOMPARE(bufferSize, doc.documentSize()); 344 | 345 | doc.append("foobar"); 346 | bufferSize += 6; 347 | QCOMPARE(bufferSize, doc.documentSize()); 348 | } 349 | 350 | void tst_TextDocument::undoRedo() 351 | { 352 | TextDocument doc; 353 | const QString initial = "Initial text"; 354 | doc.setText(initial); 355 | QVERIFY(doc.isUndoRedoEnabled()); 356 | QVERIFY(!doc.isUndoAvailable()); 357 | QVERIFY(!doc.isRedoAvailable()); 358 | TextCursor cursor(&doc); 359 | cursor.insertText("f"); 360 | QVERIFY(doc.isUndoAvailable()); 361 | QVERIFY(!doc.isRedoAvailable()); 362 | doc.undo(); 363 | QVERIFY(!doc.isUndoAvailable()); 364 | QVERIFY(doc.isRedoAvailable()); 365 | QCOMPARE(initial, doc.read(0, doc.documentSize())); 366 | doc.redo(); 367 | QVERIFY(doc.isUndoAvailable()); 368 | QVERIFY(!doc.isRedoAvailable()); 369 | QCOMPARE("f" + initial, doc.read(0, doc.documentSize())); 370 | 371 | doc.undo(); 372 | QVERIFY(!doc.isUndoAvailable()); 373 | cursor.setPosition(0); 374 | cursor.movePosition(TextCursor::WordRight, TextCursor::KeepAnchor); 375 | QCOMPARE(cursor.selectedText(), QString("Initial")); 376 | cursor.insertText("B"); 377 | QCOMPARE(doc.read(0, doc.documentSize()), QString("B text")); 378 | 379 | QVERIFY(doc.isUndoAvailable()); 380 | doc.undo(); 381 | QCOMPARE(initial, doc.read(0, doc.documentSize())); 382 | QVERIFY(doc.isRedoAvailable()); 383 | doc.setUndoRedoEnabled(false); 384 | QVERIFY(!doc.isRedoAvailable()); 385 | } 386 | 387 | struct Command { 388 | Command(int p = -1, const QString &t = QString()) : pos(p), removeCount(-1), text(t) {} 389 | Command(int p, int r) : pos(p), removeCount(r) {} 390 | 391 | const int pos; 392 | const int removeCount; 393 | const QString text; 394 | }; 395 | 396 | Q_DECLARE_METATYPE(Command); 397 | 398 | void tst_TextDocument::cursor_data() 399 | { 400 | QTest::addColumn("position"); 401 | QTest::addColumn("anchor"); 402 | QTest::addColumn("command"); 403 | QTest::addColumn("expectedPosition"); 404 | QTest::addColumn("expectedAnchor"); 405 | 406 | QTest::newRow("1") << 0 << 0 << Command(1, "foo") << 0 << 0; 407 | QTest::newRow("2") << 0 << 0 << Command(1, 1) << 0 << 0; 408 | QTest::newRow("3") << 0 << 0 << Command(0, "foo") << 3 << 3; 409 | QTest::newRow("4") << 0 << 0 << Command(0, 1) << 0 << 0; 410 | QTest::newRow("5") << 1 << 1 << Command(0, 1) << 0 << 0; 411 | QTest::newRow("6") << 0 << 3 << Command(1, 1) << 0 << 2; 412 | QTest::newRow("7") << 0 << 3 << Command(1, "b") << 0 << 4; 413 | 414 | } 415 | 416 | void tst_TextDocument::cursor() 417 | { 418 | static const char *text = "abcdefghijklmnopqrstuvwxyz\n"; 419 | TextDocument doc; 420 | doc.setText(QString::fromLatin1(text)); 421 | 422 | QFETCH(int, position); 423 | QFETCH(int, anchor); 424 | QFETCH(Command, command); 425 | QFETCH(int, expectedPosition); 426 | QFETCH(int, expectedAnchor); 427 | 428 | TextCursor cursor(&doc, position, anchor); 429 | if (command.removeCount > 0) { 430 | doc.remove(command.pos, command.removeCount); 431 | } else { 432 | doc.insert(command.pos, command.text); 433 | } 434 | QCOMPARE(cursor.position(), expectedPosition); 435 | QCOMPARE(cursor.anchor(), expectedAnchor); 436 | } 437 | 438 | void tst_TextDocument::sections() 439 | { 440 | static const char *text = "abcdefghijklmnopqrstuvwxyz\n"; 441 | TextDocument doc; 442 | doc.setText(QString::fromLatin1(text)); 443 | TextSection *section = doc.insertTextSection(0, 4); 444 | QVERIFY(section); 445 | QVERIFY(doc.insertTextSection(2, 5)); 446 | QCOMPARE(doc.sections(0, 4).value(0), section); 447 | QCOMPARE(doc.sections(0, 7).value(0), section); 448 | QCOMPARE(doc.sections(0, 3).size(), 0); 449 | QCOMPARE(doc.sections(0, 3, TextSection::IncludePartial).value(0), section); 450 | QCOMPARE(doc.sections(2, 6, TextSection::IncludePartial).value(0), section); 451 | TextSection *section2 = doc.insertTextSection(4, 2); 452 | QVERIFY(section2); 453 | QCOMPARE(doc.sections(3, 2, TextSection::IncludePartial).size(), 3); 454 | QCOMPARE(doc.sections(3, 2).size(), 0); 455 | 456 | // Insert identical sections, make sure we pull out the right one 457 | TextSection *section3 = doc.insertTextSection(5, 5); 458 | TextSection *section4 = doc.insertTextSection(5, 5); 459 | doc.takeTextSection(section4); 460 | QVERIFY(doc.sections().contains(section3)); 461 | // "[abcd][e]fghijklmnopqrstuvwxyz\n"; 462 | } 463 | 464 | void tst_TextDocument::findWholeWordsRecursionCrash() 465 | { 466 | QTemporaryFile file; 467 | file.setAutoRemove(true); 468 | file.open(); 469 | QTextStream ts(&file); 470 | for (int i=0; i<100000; ++i) { 471 | ts << "_sometext_" << endl; 472 | } 473 | 474 | TextDocument document; 475 | QVERIFY(document.load(file.fileName())); 476 | QVERIFY(document.find("omet", 0, TextDocument::FindWholeWords).isNull()); 477 | } 478 | 479 | void tst_TextDocument::findQChar_data() 480 | { 481 | QTest::addColumn("ch"); 482 | QTest::addColumn("position"); 483 | QTest::addColumn("flags"); 484 | QTest::addColumn("expected"); 485 | 486 | QTest::newRow("1") << QChar('t') << 0 << 0 << 0; 487 | QTest::newRow("2") << QChar('t') << 0 << int(TextDocument::FindCaseSensitively) << 28; 488 | QTest::newRow("3") << QChar('T') << 0 << 0 << 0; 489 | QTest::newRow("4") << QChar('T') << 0 << int(TextDocument::FindCaseSensitively) << 0; 490 | 491 | QTest::newRow("5") << QChar('t') << -1 << int(TextDocument::FindBackward) << 28; 492 | QTest::newRow("6") << QChar('t') << -1 << int(TextDocument::FindBackward|TextDocument::FindCaseSensitively) << 28; 493 | QTest::newRow("7") << QChar('T') << -1 << int(TextDocument::FindBackward) << 28; 494 | QTest::newRow("8") << QChar('T') << -1 << int(TextDocument::FindBackward|TextDocument::FindCaseSensitively) << 17; 495 | 496 | QTest::newRow("9") << QChar('t') << 28 << int(TextDocument::FindBackward) << 28; 497 | } 498 | 499 | 500 | void tst_TextDocument::findQChar() 501 | { 502 | TextDocument doc; 503 | doc.setText("This is one line\nThis is another"); 504 | QFETCH(QChar, ch); 505 | QFETCH(int, position); 506 | QFETCH(int, flags); 507 | QFETCH(int, expected); 508 | if (position == -1) 509 | position = doc.documentSize(); 510 | 511 | // qDebug() << ch << doc.readCharacter(position) << position << doc.read(qMax(0, position - 3), 7) << position; 512 | // qDebug() << ch << position << doc.documentSize() << flags << expected; 513 | TextCursor cursor = doc.find(ch, position, (TextDocument::FindMode)flags);; 514 | QCOMPARE(cursor.anchor(), expected); 515 | QCOMPARE(cursor.selectedText().toUpper(), QString(ch).toUpper()); 516 | cursor = doc.find(QString(ch), position, (TextDocument::FindMode)flags); 517 | QCOMPARE(cursor.position(), expected + 1); 518 | QCOMPARE(cursor.selectedText().toUpper(), QString(ch).toUpper()); 519 | QRegExp rx(ch); 520 | if (flags & TextDocument::FindCaseSensitively) { 521 | flags &= ~TextDocument::FindCaseSensitively; 522 | rx.setCaseSensitivity(Qt::CaseSensitive); 523 | } else { 524 | rx.setCaseSensitivity(Qt::CaseInsensitive); 525 | } 526 | 527 | cursor = doc.find(rx, position, (TextDocument::FindMode)flags); 528 | QCOMPARE(cursor.anchor(), expected); 529 | QCOMPARE(cursor.position(), expected + 1); 530 | QCOMPARE(cursor.selectedText().toUpper(), QString(ch).toUpper()); 531 | QCOMPARE(cursor.selectedText().toUpper(), rx.capturedTexts().value(0).toUpper()); 532 | 533 | } 534 | 535 | 536 | void tst_TextDocument::iterator() 537 | { 538 | TextDocument doc; 539 | doc.setChunkSize(100); 540 | QVERIFY(doc.load("./Script", TextDocument::Sparse)); 541 | TextDocumentIterator it(doc.d, 0); 542 | while (it.hasNext()) { 543 | const QChar ch = doc.readCharacter(it.position() + 1); 544 | QCOMPARE(it.next(), ch); 545 | } 546 | } 547 | 548 | void tst_TextDocument::modified() 549 | { 550 | TextDocument doc; 551 | QCOMPARE(doc.isModified(), false); 552 | doc.setText("foo bar"); 553 | TextCursor cursor(&doc); 554 | QCOMPARE(doc.isModified(), false); 555 | cursor.insertText("1"); 556 | QCOMPARE(doc.isModified(), true); 557 | doc.setModified(false); 558 | QCOMPARE(doc.isModified(), false); 559 | cursor.deletePreviousChar(); 560 | QCOMPARE(doc.isModified(), true); 561 | QCOMPARE(doc.isUndoAvailable(), true); 562 | doc.undo(); 563 | QCOMPARE(doc.isModified(), false); 564 | doc.redo(); 565 | QCOMPARE(doc.isModified(), true); 566 | doc.setModified(false); 567 | QCOMPARE(doc.isModified(), false); 568 | doc.undo(); 569 | QCOMPARE(doc.isModified(), true); 570 | } 571 | 572 | void tst_TextDocument::unicode() 573 | { 574 | 575 | 576 | } 577 | 578 | void tst_TextDocument::lineNumbers() 579 | { 580 | TextDocument doc; 581 | QCOMPARE(doc.lineNumber(0), 0); // Shouldn't assert with empty doc 582 | doc.setChunkSize(16); 583 | QString line = "f\n"; 584 | for (int i=0; i<1024; ++i) { 585 | doc.append(line); 586 | } 587 | QCOMPARE(8, doc.read(0, 16).count(QLatin1Char('\n'))); 588 | for (int i=0; i<1024; ++i) { 589 | QCOMPARE(doc.lineNumber(i * 2), i); // 1-indexed 590 | } 591 | } 592 | 593 | 594 | void tst_TextDocument::carriageReturns_data() 595 | { 596 | QTest::addColumn("options"); 597 | QTest::addColumn("deviceMode"); 598 | QTest::addColumn("text"); 599 | QTest::addColumn("containsCarriageReturns"); 600 | 601 | QTest::newRow("1") << TextDocument::Options(TextDocument::NoOptions) << TextDocument::Sparse << QByteArray("foo\nbar") << false; 602 | QTest::newRow("2") << TextDocument::Options(TextDocument::NoOptions) << TextDocument::LoadAll << QByteArray("foo\nbar") << false; 603 | QTest::newRow("3") << TextDocument::Options(TextDocument::NoOptions) << TextDocument::Sparse << QByteArray("foo\r\nbar") << true; 604 | QTest::newRow("4") << TextDocument::Options(TextDocument::NoOptions) << TextDocument::LoadAll << QByteArray("foo\r\nbar") << true; 605 | QTest::newRow("5") << TextDocument::Options(TextDocument::NoOptions) << TextDocument::Sparse << QByteArray("foo\r\nbar") << true; 606 | QTest::newRow("6") << TextDocument::Options(TextDocument::NoOptions) << TextDocument::LoadAll << QByteArray("foo\r\nbar") << true; 607 | QTest::newRow("7") << TextDocument::Options(TextDocument::DefaultOptions) << TextDocument::Sparse << QByteArray("foo\r\nbar") << true; 608 | QTest::newRow("8") << TextDocument::Options(TextDocument::DefaultOptions) << TextDocument::LoadAll << QByteArray("foo\r\nbar") << false; 609 | QTest::newRow("9") << TextDocument::Options(TextDocument::ConvertCarriageReturns) << TextDocument::LoadAll << QByteArray("foo\r\nbar") << false; 610 | } 611 | 612 | void tst_TextDocument::carriageReturns() 613 | { 614 | QFETCH(TextDocument::Options, options); 615 | QFETCH(TextDocument::DeviceMode, deviceMode); 616 | QFETCH(QByteArray, text); 617 | QFETCH(bool, containsCarriageReturns); 618 | 619 | TextDocument doc; 620 | doc.setOptions(options|TextDocument::NoImplicitLoadAll); 621 | QBuffer buffer; 622 | buffer.setData(text); 623 | buffer.open(QIODevice::ReadOnly); 624 | doc.load(&buffer, deviceMode); 625 | QCOMPARE(doc.find(QLatin1Char('\r')).isValid(), containsCarriageReturns); 626 | } 627 | 628 | static inline int count(const QString &string, int from, int size, const QChar &ch) 629 | { 630 | Q_ASSERT(from + size <= string.size()); 631 | const ushort needle = ch.unicode(); 632 | const ushort *haystack = string.utf16() + from; 633 | int num = 0; 634 | for (int i=0; i("chunkSize"); 645 | QTest::addColumn("seed"); 646 | QTest::addColumn("size"); 647 | QTest::addColumn("tests"); 648 | QTest::newRow("a1") << 1 << 100 << 1000 << 100; 649 | QTest::newRow("a10") << 10 << 100 << 1000 << 100; 650 | QTest::newRow("a100") << 100 << 100 << 1000 << 100; 651 | QTest::newRow("a1000") << 1000 << 100 << 1000 << 100; 652 | } 653 | 654 | void tst_TextDocument::lineNumbersGenerated() 655 | { 656 | QFETCH(int, chunkSize); 657 | QFETCH(int, seed); 658 | QFETCH(int, size); 659 | QFETCH(int, tests); 660 | 661 | TextDocument doc; 662 | doc.setChunkSize(chunkSize); 663 | srand(seed); 664 | QString string; 665 | string.fill('x', size); 666 | for (int i=0; i 0) { 733 | c = doc.find('\n', c.position()-1, TextDocument::FindBackward); 734 | if (c.isNull()) { 735 | break; 736 | } 737 | } 738 | */ 739 | QCOMPARE(doc.load("Script"), true); 740 | QString s("Sarah"); 741 | 742 | int index = 0; 743 | TextCursor tc; 744 | while (!(tc = doc.find(s, index, 0)).isNull()) { 745 | // printf(" Found at %d\n", tc.position()); 746 | index = tc.position() + 1; 747 | 748 | TextCursor c(&doc, index); 749 | int row = c.lineNumber()-1; 750 | int col = c.columnNumber(); 751 | int otherindex = convertRowColumnToIndex(&doc, row, col); 752 | Q_ASSERT(index == otherindex); 753 | } 754 | } 755 | 756 | int tst_TextDocument::convertRowColumnToIndex(const TextDocument *doc, int row, int column) 757 | { 758 | // NOTE: this function works, but is slow. Quantify points 759 | // the finger at the underlying lazy document counting lines. 760 | // The line-numbering cache in there helps, but it's still 761 | // much slower than motif. Needs tuned. 762 | if (row < 0) { 763 | printf("Requested row index %d must be greater than 0\n", row); 764 | return -1; 765 | } 766 | if (column < 0) { 767 | printf("Requested column index %d must be greater than 0\n", row); 768 | return -1; 769 | } 770 | 771 | 772 | int max = doc->documentSize(); 773 | 774 | // The Qt widget seems to start row numbering at 1, not 0... 775 | int docrow = row+1; 776 | 777 | // This is a bit lame. Keep skipping through until we hit it. 778 | int index = -1; 779 | TextCursor tc(doc); 780 | tc.setPosition(0); 781 | 782 | if (getenv("QT_LOOKUP")) { 783 | // TODO: I started to look at this, since it seems like a cleaner way 784 | // to do things, but it ends up being noticably slower (with 10 operations 785 | // in the test I was creating taking about 10 seconds, versus 4 with the code below). 786 | // Needs further investigation, but this is really what we should be doing, 787 | // (or rather, what TextCursor should be doing). 788 | 789 | // Get to the right row 790 | for (int i=0; i < docrow; ++i) { 791 | if (tc.position() >= max) { 792 | printf("Requested line number %d is out of range - the document has %d lines\n", row, i); 793 | return -1; 794 | } 795 | tc.movePosition(TextCursor::NextBlock); 796 | } 797 | 798 | // At this stage, we're in the right row. We should also be at the beginning, 799 | // since we advanced from the start. 800 | 801 | // Cache the number of columns in this row in case we need to print an error message 802 | int rowStart = tc.position(); 803 | tc.movePosition(TextCursor::EndOfLine, TextCursor::KeepAnchor); 804 | int actualColumns = tc.selectionSize(); 805 | if (column > actualColumns) { 806 | printf("Requested column number %d is out of range - line %d has %d columns\n", 807 | column, row, actualColumns); 808 | return -1; 809 | } 810 | 811 | return rowStart + column; 812 | } else { 813 | 814 | int inc = 100; 815 | while (index < 0) { 816 | int pos = tc.position(); 817 | int thisrow = tc.lineNumber(); 818 | 819 | if (thisrow >= docrow) { 820 | // If we passed the row we want - back up 821 | while (tc.position() && tc.lineNumber() > docrow) { 822 | tc.movePosition(TextCursor::PreviousBlock); 823 | } 824 | // Now we're in the right row. 825 | // First, determine whether we need to go back or forwards 826 | int incr = (tc.columnNumber() > column ? -1 : 1); 827 | while (tc.columnNumber() != column) { 828 | tc.setPosition(tc.position() + incr); 829 | if (tc.position() <=0 830 | || tc.position() >= max 831 | || tc.lineNumber() != docrow) { 832 | // Make sure we didn't 'wrap around' to another row. 833 | printf("Requested column number %d is out of range for line %d\n", 834 | column, row); 835 | return -1; 836 | } 837 | } 838 | index = tc.position() - (tc.columnNumber() - column); 839 | break; 840 | } else { 841 | // Advance, but make sure we don't shoot off the end of the document 842 | int incr = (pos + inc < max ? inc : max - pos); 843 | if (incr) { 844 | tc.setPosition(pos + incr); 845 | } else { 846 | // We already hit the end of the document 847 | printf("Requested line number %d is out of range - the document has %d lines\n", row, tc.lineNumber()-1); 848 | break; 849 | } 850 | } 851 | } 852 | } 853 | 854 | return index; 855 | } 856 | 857 | 858 | void tst_TextDocument::insertText() 859 | { 860 | TextDocument d; 861 | d.setText("This is a short\nlittle\ndocument\nwith\nline\nnumbers\n"); 862 | // Calling 'lineNumber' initializes TextDocumentPrivate::hasChunksWithLineNumbers 863 | // and Chunk::firstLineIndex, but not Chunk::lines, leading to an abort 864 | // in TextDocument::insert. It asserts that 'lines' has been set just before 865 | // it attempts to increment Chunk::lines by the number of newlines in the text 866 | // being added. 867 | d.lineNumber(4); 868 | TextCursor c = TextCursor(&d); 869 | c.setPosition(20); 870 | c.insertText("text\nwith\nnewlines\n"); 871 | 872 | d.clear(); 873 | const int blockSize = 3; 874 | QString firstBlock = QString(blockSize, '1'); 875 | QString secondBlock = QString(blockSize, '_'); 876 | QString thirdBlock = QString(blockSize, '^'); 877 | c = TextCursor(&d); 878 | c.insertText(firstBlock); 879 | c.movePosition(TextCursor::End); 880 | QCOMPARE(c.position(), blockSize); 881 | c.insertText(secondBlock); 882 | c.movePosition(TextCursor::End); 883 | QCOMPARE(c.position(), blockSize*2); 884 | c.insertText(thirdBlock); 885 | 886 | QString out = d.read(0, blockSize*3); 887 | QString expected = firstBlock + secondBlock + thirdBlock; 888 | QCOMPARE(out, expected); 889 | if (out != expected) { 890 | printf("Generated output string was: %s\n" \ 891 | "Expected result was : %s\n", 892 | qPrintable(out), qPrintable(expected)); 893 | } 894 | 895 | QCOMPARE(d.load("Script"), true); 896 | 897 | TextCursor c2(&d); 898 | c2.movePosition(TextCursor::End); 899 | c2.insertText("\n"); 900 | 901 | c2.setPosition(d.documentSize()); 902 | c2.movePosition(TextCursor::PreviousBlock); 903 | } 904 | 905 | #if 0 906 | void printStatistics(const TextDocument *doc) 907 | { 908 | const int chunkCount = doc->chunkCount(); 909 | const int instantiatedChunkCount = doc->instantiatedChunkCount(); 910 | 911 | printf("Memory usage: %d bytes, spread across %d chunks(%d instantiated, %d swapped)\n", 912 | doc->currentMemoryUsage(), 913 | chunkCount, 914 | instantiatedChunkCount, 915 | chunkCount - instantiatedChunkCount); 916 | } 917 | #endif 918 | 919 | void tst_TextDocument::memUsage() 920 | { 921 | TextDocument d; 922 | d.setOptions(TextDocument::SwapChunks); 923 | QString s = "%1 +------------------------------------------------------+\n"; 924 | const int iterations = 1000000; 925 | 926 | // printStatistics(&d); 927 | 928 | for (int i=0; iabortFind(); 1037 | } 1038 | private: 1039 | TextDocument *document; 1040 | }; 1041 | 1042 | void tst_TextDocument::abortFind_data() 1043 | { 1044 | QTest::addColumn("needle"); 1045 | QTest::newRow("QRegExp") << QVariant(QRegExp(" bcd")); 1046 | QTest::newRow("QString") << QVariant(QString::fromLatin1(" bcd")); 1047 | QTest::newRow("QChar") << QVariant(QChar('z')); 1048 | } 1049 | 1050 | void tst_TextDocument::abortFind() 1051 | { 1052 | QFETCH(QVariant, needle); 1053 | TextDocument doc; 1054 | doc.setText("abcdefg\nabcdefg\n bcd z\n"); 1055 | Aborter aborter(&doc); 1056 | switch (needle.type()) { 1057 | case QVariant::RegExp: 1058 | QVERIFY(doc.find(needle.toRegExp(), 0, 0).isValid()); 1059 | QVERIFY(!doc.find(needle.toRegExp(), 0, TextDocument::FindAllowInterrupt).isValid()); 1060 | break; 1061 | case QVariant::String: 1062 | QVERIFY(doc.find(needle.toString(), 0, 0).isValid()); 1063 | QVERIFY(!doc.find(needle.toString(), 0, TextDocument::FindAllowInterrupt).isValid()); 1064 | break; 1065 | case QVariant::Char: 1066 | QVERIFY(doc.find(needle.toChar(), 0, 0).isValid()); 1067 | QVERIFY(!doc.find(needle.toChar(), 0, TextDocument::FindAllowInterrupt).isValid()); 1068 | break; 1069 | default: 1070 | qFatal("huh?"); 1071 | break; 1072 | } 1073 | } 1074 | 1075 | class Aborter2 : public QObject 1076 | { 1077 | Q_OBJECT 1078 | public: 1079 | Aborter2(TextDocument *doc) 1080 | : document(doc), prcnt(-1) 1081 | { 1082 | connect(document, SIGNAL(findProgress(qreal,int)), this, SLOT(onFindProgress(qreal))); 1083 | } 1084 | qreal percentage() const { return prcnt; } 1085 | public slots: 1086 | void onFindProgress(qreal percentage) 1087 | { 1088 | prcnt = percentage; 1089 | document->abortFind(); 1090 | } 1091 | public: 1092 | TextDocument *document; 1093 | qreal prcnt; 1094 | }; 1095 | 1096 | 1097 | void tst_TextDocument::abortFindSleep() 1098 | { 1099 | QTemporaryFile tmpFile; 1100 | tmpFile.setAutoRemove(true); 1101 | tmpFile.open(); 1102 | QTextStream ts(&tmpFile); 1103 | const QString line = "abcdefghijklmnopqrstuvwxyz0123456789\n"; 1104 | for (int i=0; i<1024 * 1024; ++i) 1105 | ts << line; 1106 | ts << '_'; 1107 | 1108 | TextDocument doc; 1109 | doc.setProperty("TEXTDOCUMENT_FIND_SLEEP", 10); 1110 | { 1111 | Aborter2 aborter(&doc); 1112 | QVERIFY(doc.find('_', 0, TextDocument::FindAllowInterrupt).isNull()); 1113 | QVERIFY(aborter.percentage() < 1.0); 1114 | } 1115 | { 1116 | Aborter2 aborter(&doc); 1117 | QVERIFY(doc.find("_", 0, TextDocument::FindAllowInterrupt).isNull()); 1118 | QVERIFY(aborter.percentage() < 1.0); 1119 | } 1120 | { 1121 | Aborter2 aborter(&doc); 1122 | QVERIFY(doc.find(QRegExp("[^a-z0-9]"), 0, TextDocument::FindAllowInterrupt).isNull()); 1123 | QVERIFY(aborter.percentage() < 1.0); 1124 | } 1125 | } 1126 | 1127 | void tst_TextDocument::findAll_data() 1128 | { 1129 | QTest::addColumn("needle"); 1130 | QTest::newRow("QRegExp") << QVariant(QRegExp("z")); 1131 | QTest::newRow("QString") << QVariant(QString::fromLatin1("z")); 1132 | QTest::newRow("QChar") << QVariant(QChar('z')); 1133 | } 1134 | 1135 | class Doc : public TextDocument 1136 | { 1137 | Q_OBJECT 1138 | public: 1139 | Doc() 1140 | { 1141 | connect(this, SIGNAL(entryFound(TextCursor)), this, SLOT(onEntryFound(TextCursor))); 1142 | } 1143 | 1144 | public slots: 1145 | void onEntryFound(const TextCursor &cursor) 1146 | { 1147 | positions.append(cursor.anchor()); 1148 | if (positions.size() >= 5) 1149 | abortFind(); 1150 | } 1151 | public: 1152 | QList positions; 1153 | }; 1154 | 1155 | void tst_TextDocument::findAll() 1156 | { 1157 | QFETCH(QVariant, needle); 1158 | Doc doc; 1159 | QString alphabet; 1160 | for (char ch='a'; ch<='z'; ++ch) { 1161 | alphabet.append(QLatin1Char(ch)); 1162 | } 1163 | for (int i=0; i<100; ++i) { 1164 | doc.append(alphabet); 1165 | } 1166 | QList expected; 1167 | for (int i=1; i<=5; ++i) { 1168 | expected.append((i * 26) - 1); 1169 | } 1170 | 1171 | switch (needle.type()) { 1172 | case QVariant::RegExp: 1173 | QVERIFY(doc.find(needle.toRegExp(), 0, 0).isValid()); 1174 | QVERIFY(!doc.find(needle.toRegExp(), 0, TextDocument::FindAll|TextDocument::FindAllowInterrupt).isValid()); 1175 | QCOMPARE(doc.positions, expected); 1176 | break; 1177 | case QVariant::String: 1178 | QVERIFY(doc.find(needle.toString(), 0, 0).isValid()); 1179 | QVERIFY(!doc.find(needle.toString(), 0, TextDocument::FindAll|TextDocument::FindAllowInterrupt).isValid()); 1180 | QCOMPARE(doc.positions, expected); 1181 | break; 1182 | case QVariant::Char: 1183 | QVERIFY(doc.find(needle.toChar(), 0, 0).isValid()); 1184 | QVERIFY(!doc.find(needle.toChar(), 0, TextDocument::FindAll|TextDocument::FindAllowInterrupt).isValid()); 1185 | QCOMPARE(doc.positions, expected); 1186 | break; 1187 | default: 1188 | qFatal("huh?"); 1189 | break; 1190 | } 1191 | } 1192 | 1193 | 1194 | 1195 | QTEST_MAIN(tst_TextDocument) 1196 | #include "tst_textdocument.moc" 1197 | --------------------------------------------------------------------------------