/*
This is part of TeXworks, an environment for working with TeX documents
Copyright (C) 2007-08 Jonathan Kew
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
For links to further information, or to contact the author,
see .
*/
#include "CompletingEdit.h"
#include "TWUtils.h"
#include "TWApp.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
CompletingEdit::CompletingEdit(QWidget *parent)
: QTextEdit(parent),
clickCount(0),
autoIndentMode(-1), prefixLength(0),
smartQuotesMode(-1),
c(NULL), cmpCursor(QTextCursor()),
pHunspell(NULL), spellingCodec(NULL)
{
if (sharedCompleter == NULL) { // initialize shared (static) members
sharedCompleter = new QCompleter(qApp);
sharedCompleter->setCompletionMode(QCompleter::InlineCompletion);
sharedCompleter->setCaseSensitivity(Qt::CaseInsensitive);
loadCompletionFiles(sharedCompleter);
currentCompletionFormat = new QTextCharFormat;
currentCompletionFormat->setBackground(QColor("yellow").lighter(160));
braceMatchingFormat = new QTextCharFormat;
braceMatchingFormat->setBackground(QColor("orange"));
currentLineFormat = new QTextCharFormat;
currentLineFormat->setBackground(QColor("yellow").lighter(180));
currentLineFormat->setProperty(QTextFormat::FullWidthSelection, true);
QSETTINGS_OBJECT(settings);
highlightCurrentLine = settings.value("highlightCurrentLine", true).toBool();
}
loadIndentModes();
loadSmartQuotesModes();
connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(cursorPositionChangedSlot()));
connect(this, SIGNAL(selectionChanged()), this, SLOT(cursorPositionChangedSlot()));
lineNumberArea = new LineNumberArea(this);
connect(document(), SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth(int)));
connect(this, SIGNAL(updateRequest(const QRect&, int)), this, SLOT(updateLineNumberArea(const QRect&, int)));
connect(this, SIGNAL(textChanged()), lineNumberArea, SLOT(update()));
connect(TWApp::instance(), SIGNAL(highlightLineOptionChanged()), this, SLOT(resetExtraSelections()));
cursorPositionChangedSlot();
updateLineNumberAreaWidth(0);
}
CompletingEdit::~CompletingEdit()
{
setCompleter(NULL);
}
void CompletingEdit::setCompleter(QCompleter *completer)
{
c = completer;
if (!c)
return;
c->setWidget(this);
}
void CompletingEdit::cursorPositionChangedSlot()
{
setCompleter(NULL);
if (!currentCompletionRange.isNull()) {
QTextCursor curs = textCursor();
if (curs.selectionStart() < currentCompletionRange.selectionStart() || curs.selectionEnd() >= currentCompletionRange.selectionEnd())
currentCompletionRange = QTextCursor();
}
resetExtraSelections();
prefixLength = 0;
}
void CompletingEdit::mousePressEvent(QMouseEvent *e)
{
if (e->buttons() != Qt::LeftButton) {
mouseMode = none;
QTextEdit::mousePressEvent(e);
return;
}
if (e->modifiers() == Qt::ControlModifier) {
mouseMode = synctexClick;
e->accept();
return;
}
mouseMode = normalSelection;
clickPos = e->pos();
if (e->modifiers() & Qt::ShiftModifier) {
mouseMode = extendingSelection;
clickCount = 1;
QTextEdit::mousePressEvent(e);
return;
}
if (clickTimer.isActive() && (e->pos() - clickPos).manhattanLength() < qApp->startDragDistance())
++clickCount;
else
clickCount = 1;
clickTimer.start(qApp->doubleClickInterval(), this);
QTextCursor curs;
switch (clickCount) {
case 1:
curs = cursorForPosition(clickPos);
break;
case 2:
curs = wordSelectionForPos(clickPos);
break;
default:
curs = blockSelectionForPos(clickPos);
break;
}
if (clickCount > 1) {
setTextCursor(curs);
setSelectionClipboard(curs);
mouseMode = dragSelecting;
}
dragStartCursor = curs;
e->accept();
}
void CompletingEdit::mouseMoveEvent(QMouseEvent *e)
{
switch (mouseMode) {
case none:
QTextEdit::mouseMoveEvent(e);
return;
case synctexClick:
case ignoring:
e->accept();
return;
case extendingSelection:
QTextEdit::mouseMoveEvent(e);
return;
case normalSelection:
if (clickCount == 1
&& dragStartCursor.position() >= textCursor().selectionStart()
&& dragStartCursor.position() < textCursor().selectionEnd()) {
if ((e->pos() - clickPos).manhattanLength() >= qApp->startDragDistance()) {
int sourceStart = textCursor().selectionStart();
int sourceEnd = textCursor().selectionEnd();
QTextCursor source = textCursor();
QDrag *drag = new QDrag(this);
drag->setMimeData(createMimeDataFromSelection());
QTextCursor dropCursor = textCursor();
textCursor().beginEditBlock();
Qt::DropAction action = drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::MoveAction);
if (action != Qt::IgnoreAction) {
dropCursor.setPosition(droppedOffset);
dropCursor.setPosition(droppedOffset + droppedLength, QTextCursor::KeepAnchor);
if (action == Qt::MoveAction && (this == drag->target() || this->isAncestorOf(drag->target()))) {
if (droppedOffset >= sourceStart && droppedOffset <= sourceEnd) {
source.setPosition(droppedOffset + droppedLength);
source.setPosition(sourceEnd + droppedLength, QTextCursor::KeepAnchor);
source.removeSelectedText();
source.setPosition(sourceStart);
source.setPosition(droppedOffset, QTextCursor::KeepAnchor);
source.removeSelectedText();
}
else
source.removeSelectedText();
}
}
textCursor().endEditBlock();
setTextCursor(dropCursor);
mouseMode = ignoring;
}
e->accept();
return;
}
setTextCursor(dragStartCursor);
mouseMode = dragSelecting;
// fall through to dragSelecting
case dragSelecting:
QPoint pos = e->pos();
int scrollValue = -1;
if (verticalScrollBar() != NULL) {
if (pos.y() < frameRect().top())
verticalScrollBar()->triggerAction(QScrollBar::SliderSingleStepSub);
else if (pos.y() > frameRect().bottom())
verticalScrollBar()->triggerAction(QScrollBar::SliderSingleStepAdd);
scrollValue = verticalScrollBar()->value();
}
QTextCursor curs;
switch (clickCount) {
case 1:
curs = cursorForPosition(pos);
break;
case 2:
curs = wordSelectionForPos(pos);
break;
default:
curs = blockSelectionForPos(pos);
break;
}
int start = qMin(dragStartCursor.selectionStart(), curs.selectionStart());
int end = qMax(dragStartCursor.selectionEnd(), curs.selectionEnd());
curs.setPosition(start);
curs.setPosition(end, QTextCursor::KeepAnchor);
setTextCursor(curs);
setSelectionClipboard(curs);
if (scrollValue != -1)
verticalScrollBar()->setValue(scrollValue);
e->accept();
return;
}
}
void CompletingEdit::mouseReleaseEvent(QMouseEvent *e)
{
switch (mouseMode) {
case none:
QTextEdit::mouseReleaseEvent(e);
return;
case ignoring:
e->accept();
return;
case synctexClick:
{
QTextCursor curs = cursorForPosition(e->pos());
emit syncClick(curs.blockNumber() + 1);
}
e->accept();
return;
case dragSelecting:
e->accept();
return;
case normalSelection:
setTextCursor(dragStartCursor);
setSelectionClipboard(dragStartCursor);
e->accept();
return;
case extendingSelection:
QTextEdit::mouseReleaseEvent(e);
return;
}
}
QTextCursor CompletingEdit::blockSelectionForPos(const QPoint& pos)
{
QTextCursor curs = cursorForPosition(pos);
curs.setPosition(curs.block().position());
curs.setPosition(curs.block().position() + curs.block().length(), QTextCursor::KeepAnchor);
return curs;
}
bool CompletingEdit::selectWord(QTextCursor& cursor)
{
if (cursor.selectionEnd() - cursor.selectionStart() > 1)
return false; // actually an error by the caller
const QTextBlock block = document()->findBlock(cursor.selectionStart());
const QString text = block.text();
if (text.length() < 1) // empty line
return false;
int start, end;
bool result = TWUtils::findNextWord(text, cursor.selectionStart() - block.position(), start, end);
cursor.setPosition(block.position() + start);
cursor.setPosition(block.position() + end, QTextCursor::KeepAnchor);
return result;
}
QTextCursor CompletingEdit::wordSelectionForPos(const QPoint& mousePos)
{
QTextCursor cursor;
QPoint pos = mousePos + QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value());
int cursorPos = document()->documentLayout()->hitTest(pos, Qt::FuzzyHit);
if (cursorPos == -1)
return cursor;
cursor = QTextCursor(document());
cursor.setPosition(cursorPos);
cursor.setPosition(cursorPos + 1, QTextCursor::KeepAnchor);
// check if click was within the char to the right of cursor; if so we select forwards
QRect r = cursorRect(cursor);
if (r.contains(pos)) {
// Currently I don't seem to be getting a useful answer from cursorRect(), it's always zero-width :-(
// and so this path will not be used, but leaving it here in hopes of fixing it some day
// QString s = cursor.selectedText();
// if (isPairedChar(s)) ...
(void)selectWord(cursor);
return cursor;
}
if (cursorPos > 0) {
cursorPos -= 1;
cursor.setPosition(cursorPos);
cursor.setPosition(cursorPos + 1, QTextCursor::KeepAnchor);
// don't test because the rect will be zero width (see above)!
// r = cursorRect(cursor);
// if (r.contains(pos)) {
const QString plainText = toPlainText();
QChar curChr = plainText[cursorPos];
QChar c;
if ((c = TWUtils::closerMatching(curChr)) != 0) {
int balancePos = TWUtils::balanceDelim(plainText, cursorPos + 1, c, 1);
if (balancePos < 0)
QApplication::beep();
else
cursor.setPosition(balancePos + 1, QTextCursor::KeepAnchor);
}
else if ((c = TWUtils::openerMatching(curChr)) != 0) {
int balancePos = TWUtils::balanceDelim(plainText, cursorPos - 1, c, -1);
if (balancePos < 0)
QApplication::beep();
else {
cursor.setPosition(balancePos);
cursor.setPosition(cursorPos + 1, QTextCursor::KeepAnchor);
}
}
else
(void)selectWord(cursor);
// }
}
return cursor;
}
void CompletingEdit::mouseDoubleClickEvent(QMouseEvent *e)
{
if (e->modifiers() == Qt::ControlModifier)
e->accept();
else if (e->modifiers() != Qt::NoModifier)
QTextEdit::mouseDoubleClickEvent(e);
else
mousePressEvent(e); // don't like QTextEdit's selection behavior, so we try to improve it
}
void
CompletingEdit::setSelectionClipboard(const QTextCursor& curs)
{
if (!curs.hasSelection())
return;
QClipboard *c = QApplication::clipboard();
if (!c->supportsSelection())
return;
c->setText(curs.selectedText().replace(QChar(0x2019), QChar('\n')),
QClipboard::Selection);
}
void CompletingEdit::timerEvent(QTimerEvent *e)
{
if (e->timerId() == clickTimer.timerId()) {
clickTimer.stop();
e->accept();
}
else
QTextEdit::timerEvent(e);
}
void CompletingEdit::focusInEvent(QFocusEvent *e)
{
if (c)
c->setWidget(this);
QTextEdit::focusInEvent(e);
}
void CompletingEdit::resetExtraSelections()
{
QList selections;
if (highlightCurrentLine && !textCursor().hasSelection()) {
ExtraSelection sel;
sel.format = *currentLineFormat;
sel.cursor = textCursor();
selections.append(sel);
}
if (!currentCompletionRange.isNull()) {
ExtraSelection sel;
sel.cursor = currentCompletionRange;
sel.format = *currentCompletionFormat;
selections.append(sel);
}
setExtraSelections(selections);
}
void CompletingEdit::keyPressEvent(QKeyEvent *e)
{
// Shortcut key for command completion
bool isShortcut = (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Backtab);
if (isShortcut) {
handleCompletionShortcut(e);
return;
}
if (e->text() != "")
cmpCursor = QTextCursor();
switch (e->key()) {
case Qt::Key_Return:
handleReturn(e);
break;
case Qt::Key_Backspace:
handleBackspace(e);
break;
default:
handleOtherKey(e);
break;
}
}
void CompletingEdit::handleReturn(QKeyEvent *e)
{
QString prefix;
if (autoIndentMode >= 0 && autoIndentMode < indentModes->count() && e->modifiers() == Qt::NoModifier) {
QRegExp &re = (*indentModes)[autoIndentMode].regex;
QString blockText = textCursor().block().text();
if (blockText.indexOf(re) == 0 && re.matchedLength() > 0)
prefix = blockText.left(re.matchedLength());
}
QTextEdit::keyPressEvent(e);
if (!prefix.isEmpty()) {
insertPlainText(prefix);
prefixLength = prefix.length();
}
}
void CompletingEdit::handleBackspace(QKeyEvent *e)
{
QTextCursor curs = textCursor();
if (e->modifiers() == Qt::NoModifier && prefixLength > 0 && !curs.hasSelection()) {
curs.beginEditBlock();
// note that prefixLength will get reset on the first deletion,
// so it is important that the loop counts down rather than up!
for (int i = prefixLength; i > 0; --i)
curs.deletePreviousChar();
curs.endEditBlock();
}
else
QTextEdit::keyPressEvent(e);
}
void CompletingEdit::handleOtherKey(QKeyEvent *e)
{
QTextCursor cursor = textCursor();
int pos = textCursor().selectionStart(); // remember cursor before the keystroke
int end = textCursor().selectionEnd();
QTextEdit::keyPressEvent(e);
cursor = textCursor();
bool arrowKey = false;
if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Right) {
arrowKey = true;
if (pos < end && !cursor.hasSelection()) { // collapsed selection
if (cursor.position() == end + 1)
pos = end;
cursor.setPosition(pos);
setTextCursor(cursor);
}
}
if ((e->modifiers() & Qt::ControlModifier) == 0) {
// not a command key - maybe do brace matching or smart quotes
if (!cursor.hasSelection()) {
if (!arrowKey && cursor.selectionStart() == pos + 1) {
if (smartQuotesMode >= 0)
maybeSmartenQuote(cursor.position() - 1);
}
if (cursor.selectionStart() == pos + 1 || cursor.selectionStart() == pos - 1) {
if (cursor.selectionStart() == pos - 1) // we moved backward, set pos to look at the char we just passed over
--pos;
const QString text = document()->toPlainText();
int match = -2;
QChar c;
if (pos > 0 && pos < text.length() && (c = TWUtils::openerMatching(text[pos])) != 0)
match = TWUtils::balanceDelim(text, pos - 1, c, -1);
else if (pos < text.length() - 1 && (c = TWUtils::closerMatching(text[pos])) != 0)
match = TWUtils::balanceDelim(text, pos + 1, c, 1);
if (match >= 0) {
QList selList = extraSelections();
ExtraSelection sel;
sel.cursor = QTextCursor(document());
sel.cursor.setPosition(match);
sel.cursor.setPosition(match + 1, QTextCursor::KeepAnchor);
sel.format = *braceMatchingFormat;
selList.append(sel);
setExtraSelections(selList);
QTimer::singleShot(250, this, SLOT(resetExtraSelections()));
}
}
}
}
}
void CompletingEdit::setSmartQuotesMode(int index)
{
smartQuotesMode = (index >= 0 && index < quotesModes->count()) ? index : -1;
}
QStringList CompletingEdit::smartQuotesModes()
{
loadSmartQuotesModes();
QStringList modes;
foreach (const QuotesMode& mode, *quotesModes)
modes << mode.name;
return modes;
}
void CompletingEdit::loadSmartQuotesModes()
{
if (quotesModes == NULL) {
QDir configDir(TWUtils::getLibraryPath("configuration"));
quotesModes = new QList;
QFile quotesModesFile(configDir.filePath("smart-quotes-modes.txt"));
if (quotesModesFile.open(QIODevice::ReadOnly)) {
QRegExp modeName("\\[([^]]+)\\]");
QRegExp quoteLine("([^ \\t])\\s+([^ \\t]+)\\s+([^ \\t]+)");
QuotesMode newMode;
while (1) {
QByteArray ba = quotesModesFile.readLine();
if (ba.size() == 0)
break;
if (ba[0] == '#' || ba[0] == '\n')
continue;
QString line = QString::fromUtf8(ba.data(), ba.size()).trimmed();
if (modeName.exactMatch(line)) {
if (newMode.mappings.count() > 0) {
quotesModes->append(newMode);
newMode.mappings.clear();
}
newMode.name = modeName.cap(1);
continue;
}
if (quoteLine.exactMatch(line) && newMode.name.length() > 0) {
QChar key = quoteLine.cap(1)[0];
const QString& open = quoteLine.cap(2);
const QString& close = quoteLine.cap(3);
newMode.mappings[key] = QuotePair(open,close);
continue;
}
}
if (newMode.mappings.count() > 0) {
quotesModes->append(newMode);
}
}
}
}
void CompletingEdit::maybeSmartenQuote(int offset)
{
if (smartQuotesMode < 0 || smartQuotesMode >= quotesModes->count())
return;
const QuoteMapping& mappings = quotesModes->at(smartQuotesMode).mappings;
const QString& text = document()->toPlainText();
if (offset < 0 || offset >= text.length())
return;
QTextCursor cursor(document());
cursor.setPosition(offset);
cursor.setPosition(offset + 1, QTextCursor::KeepAnchor);
QChar keyChar = cursor.selectedText()[0];
QuoteMapping::const_iterator iter = mappings.find(keyChar);
if (iter == mappings.end())
return;
if (offset == 0) {
cursor.insertText(iter.value().first);
return;
}
if (offset == text.length() - 1) {
cursor.insertText(iter.value().second);
return;
}
QChar prevChar = text[offset - 1];
QChar nextChar = text[offset + 1];
if (prevChar.isSpace()) {
cursor.insertText(iter.value().first);
return;
}
cursor.insertText(iter.value().second);
}
void CompletingEdit::handleCompletionShortcut(QKeyEvent *e)
{
// usage:
// unmodified: next completion
// shift : previous completion
// ctl/alt : skip to next placeholder (alt on Mac, ctl elsewhere)
// ctl/alt-shift : skip to previous placeholder
#ifdef Q_WS_MAC
if ((e->modifiers() & ~Qt::ShiftModifier) == Qt::AltModifier)
#else
if ((e->modifiers() & ~Qt::ShiftModifier) == Qt::ControlModifier)
#endif
{
if (!find(QString(0x2022), (e->modifiers() & Qt::ShiftModifier)
? QTextDocument::FindBackward : (QTextDocument::FindFlags)0))
QApplication::beep();
return;
}
if (c == NULL) {
cmpCursor = textCursor();
if (!selectWord(cmpCursor) && textCursor().selectionStart() > 0) {
cmpCursor.setPosition(textCursor().selectionStart() - 1);
selectWord(cmpCursor);
}
// check if the word is preceded by open-brace; if so try with that included
int start = cmpCursor.selectionStart();
int end = cmpCursor.selectionEnd();
if (start > 0) { // special cases: possibly look back to include brace or hyphen(s)
if (cmpCursor.selectedText() == "-") {
QTextCursor hyphCursor(cmpCursor);
int hyphPos = start;
while (hyphPos > 0) {
hyphCursor.setPosition(hyphPos - 1);
hyphCursor.setPosition(hyphPos, QTextCursor::KeepAnchor);
if (hyphCursor.selectedText() != "-")
break;
--hyphPos;
}
cmpCursor.setPosition(hyphPos);
cmpCursor.setPosition(end, QTextCursor::KeepAnchor);
}
else if (cmpCursor.selectedText() != "{") {
QTextCursor braceCursor(cmpCursor);
braceCursor.setPosition(start - 1);
braceCursor.setPosition(start, QTextCursor::KeepAnchor);
if (braceCursor.selectedText() == "{") {
cmpCursor.setPosition(start - 1);
cmpCursor.setPosition(end, QTextCursor::KeepAnchor);
}
}
}
while (1) {
QString completionPrefix = cmpCursor.selectedText();
if (completionPrefix != "") {
setCompleter(sharedCompleter);
c->setCompletionPrefix(completionPrefix);
if (c->completionCount() == 0) {
if (cmpCursor.selectionStart() < start) {
// we must have included a preceding brace or hyphen; now try without it
cmpCursor.setPosition(start);
cmpCursor.setPosition(end, QTextCursor::KeepAnchor);
continue;
}
setCompleter(NULL);
}
else {
if (e->modifiers() == Qt::ShiftModifier)
c->setCurrentRow(c->completionCount() - 1);
showCurrentCompletion();
return;
}
}
break;
}
}
if (c != NULL && c->completionCount() > 0) {
if (e->modifiers() == Qt::ShiftModifier) {
if (c->currentRow() == 0) {
showCompletion(c->completionPrefix());
setCompleter(NULL);
}
else {
c->setCurrentRow(c->currentRow() - 1);
showCurrentCompletion();
}
}
else {
if (c->currentRow() == c->completionCount() - 1) {
showCompletion(c->completionPrefix());
setCompleter(NULL);
}
else {
c->setCurrentRow(c->currentRow() + 1);
showCurrentCompletion();
}
}
return;
}
QTextEdit::keyPressEvent(e);
}
void CompletingEdit::showCompletion(const QString& completion, int insOffset)
{
disconnect(this, SIGNAL(cursorPositionChanged()), this, SLOT(cursorPositionChangedSlot()));
if (c->widget() != this)
return;
QTextCursor tc = cmpCursor;
if (tc.isNull()) {
tc = textCursor();
tc.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, c->completionPrefix().length());
}
tc.insertText(completion);
cmpCursor = tc;
cmpCursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, completion.length());
if (insOffset != -1)
tc.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, completion.length() - insOffset);
setTextCursor(tc);
currentCompletionRange = cmpCursor;
resetExtraSelections();
connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(cursorPositionChangedSlot()));
}
void CompletingEdit::showCurrentCompletion()
{
if (c->widget() != this)
return;
QStandardItemModel *model = qobject_cast(c->model());
QList items = model->findItems(c->currentCompletion());
if (items.count() > 1) {
if (c->currentCompletion() == prevCompletion) {
if (c->currentIndex().row() > prevRow)
++itemIndex;
else
--itemIndex;
if (itemIndex < 0)
itemIndex = items.count() - 1;
else if (itemIndex > items.count() - 1)
itemIndex = 0;
}
else
itemIndex = 0;
prevRow = c->currentIndex().row();
prevCompletion = c->currentCompletion();
}
else {
prevCompletion = QString();
itemIndex = 0;
}
QString completion = model->item(items[itemIndex]->row(), 1)->text();
int insOffset = completion.indexOf("#INS#");
if (insOffset != -1)
completion.replace("#INS#", "");
showCompletion(completion, insOffset);
}
void CompletingEdit::loadCompletionsFromFile(QStandardItemModel *model, const QString& filename)
{
QFile completionFile(filename);
if (completionFile.exists() && completionFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QTextStream in(&completionFile);
in.setCodec("UTF-8");
in.setAutoDetectUnicode(true);
QList row;
while (1) {
QString line = in.readLine();
if (line.isNull())
break;
if (line[0] == '%')
continue;
line.replace("#RET#", "\n");
QStringList parts = line.split(":=");
if (parts.count() > 2)
continue;
if (parts.count() == 1)
parts.append(parts[0]);
parts[0].replace("#INS#", "");
row.append(new QStandardItem(parts[0]));
row.append(new QStandardItem(parts[1]));
model->appendRow(row);
row.clear();
}
completionFile.close();
}
}
void CompletingEdit::loadCompletionFiles(QCompleter *theCompleter)
{
QStandardItemModel *model = new QStandardItemModel(0, 2, theCompleter); // columns are abbrev, expansion
QDir completionDir(TWUtils::getLibraryPath("completion"));
foreach (QFileInfo fileInfo, completionDir.entryInfoList(QDir::Files | QDir::Readable, QDir::Name)) {
loadCompletionsFromFile(model, fileInfo.canonicalFilePath());
}
theCompleter->setModel(model);
}
void CompletingEdit::jumpToPdf()
{
QAction *act = qobject_cast(sender());
if (act != NULL)
emit syncClick(act->data().toInt());
}
void CompletingEdit::contextMenuEvent(QContextMenuEvent *event)
{
QMenu *menu = createStandardContextMenu();
QAction *act = new QAction(tr("Jump to PDF"), menu);
act->setData(QVariant(cursorForPosition(event->pos()).blockNumber() + 1));
connect(act, SIGNAL(triggered()), this, SLOT(jumpToPdf()));
menu->insertSeparator(menu->actions().first());
menu->insertAction(menu->actions().first(), act);
if (pHunspell != NULL) {
currentWord = cursorForPosition(event->pos());
currentWord.setPosition(currentWord.position());
if (selectWord(currentWord)) {
QByteArray word = spellingCodec->fromUnicode(currentWord.selectedText());
int spellResult = Hunspell_spell(pHunspell, word.data());
if (spellResult == 0) {
char **suggestionList;
int count = Hunspell_suggest(pHunspell, &suggestionList, word.data());
QAction *sep = menu->insertSeparator(menu->actions().first());
if (count == 0)
menu->insertAction(sep, new QAction(tr("No suggestions"), menu));
else {
QSignalMapper *mapper = new QSignalMapper(menu);
for (int i = 0; i < count; ++i) {
QString str = spellingCodec->toUnicode(suggestionList[i]);
act = new QAction(str, menu);
connect(act, SIGNAL(triggered()), mapper, SLOT(map()));
mapper->setMapping(act, str);
menu->insertAction(sep, act);
free(suggestionList[i]);
}
free(suggestionList);
connect(mapper, SIGNAL(mapped(const QString&)), this, SLOT(correction(const QString&)));
}
sep = menu->insertSeparator(menu->actions().first());
// QAction *add = new QAction(tr("Add to dictionary"), menu);
// connect(add, SIGNAL(triggered()), this, SLOT(addToDictionary()));
// menu->insertAction(sep, add);
QAction *ignore = new QAction(tr("Ignore word"), menu);
connect(ignore, SIGNAL(triggered()), this, SLOT(ignoreWord()));
menu->insertAction(sep, ignore);
}
}
}
menu->exec(event->globalPos());
delete menu;
}
void CompletingEdit::setSpellChecker(Hunhandle* h, QTextCodec *codec)
{
pHunspell = h;
spellingCodec = codec;
}
void CompletingEdit::setAutoIndentMode(int index)
{
autoIndentMode = (index >= 0 && index < indentModes->count()) ? index : -1;
}
void CompletingEdit::correction(const QString& suggestion)
{
currentWord.insertText(suggestion);
}
void CompletingEdit::addToDictionary()
{
// For this to be useful, we need to be able to store a user dictionary (per language).
// Prefer to switch to Enchant first, before looking into this further.
}
void CompletingEdit::ignoreWord()
{
// note that this is not persistent after quitting TW
QByteArray word = spellingCodec->fromUnicode(currentWord.selectedText());
(void)Hunspell_add(pHunspell, word.data());
emit rehighlight();
}
void CompletingEdit::loadIndentModes()
{
if (indentModes == NULL) {
QDir configDir(TWUtils::getLibraryPath("configuration"));
indentModes = new QList;
QFile indentPatternFile(configDir.filePath("auto-indent-patterns.txt"));
if (indentPatternFile.open(QIODevice::ReadOnly)) {
QRegExp re("\"([^\"]+)\"\\s+(.+)");
while (1) {
QByteArray ba = indentPatternFile.readLine();
if (ba.size() == 0)
break;
if (ba[0] == '#' || ba[0] == '\n')
continue;
QString line = QString::fromUtf8(ba.data(), ba.size()).trimmed();
if (re.exactMatch(line)) {
IndentMode mode;
mode.name = re.cap(1);
mode.regex = QRegExp(re.cap(2).trimmed());
if (!mode.name.isEmpty() && mode.regex.isValid())
indentModes->append(mode);
}
}
}
}
}
QStringList CompletingEdit::autoIndentModes()
{
loadIndentModes();
QStringList modes;
foreach (const IndentMode& mode, *indentModes)
modes << mode.name;
return modes;
}
void CompletingEdit::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasUrls())
event->ignore();
else
QTextEdit::dragEnterEvent(event);
}
void CompletingEdit::dropEvent(QDropEvent *event)
{
QTextCursor dropCursor = cursorForPosition(event->pos());
if (!dropCursor.isNull()) {
droppedOffset = dropCursor.position();
droppedLength = event->mimeData()->text().length();
}
else
droppedOffset = -1;
int scrollX = horizontalScrollBar()->value();
int scrollY = verticalScrollBar()->value();
QTextEdit::dropEvent(event);
verticalScrollBar()->setValue(scrollY);
horizontalScrollBar()->setValue(scrollX);
}
void CompletingEdit::insertFromMimeData(const QMimeData *source)
{
if (source->hasText()) {
QTextCursor curs = textCursor();
curs.insertText(source->text());
}
}
bool CompletingEdit::canInsertFromMimeData(const QMimeData *source) const
{
return source->hasText();
}
// support for the line-number area
// from Qt tutorial "Code Editor"
void CompletingEdit::setLineNumberDisplay(bool displayNumbers)
{
lineNumberArea->setVisible(displayNumbers);
updateLineNumberAreaWidth(0);
}
int CompletingEdit::lineNumberAreaWidth()
{
int digits = 1;
int max = qMax(1, document()->blockCount());
while (max >= 10) {
max /= 10;
++digits;
}
int space = 3 + fontMetrics().width(QLatin1Char('9')) * digits;
return space;
}
void CompletingEdit::updateLineNumberAreaWidth(int /* newBlockCount */)
{
if (lineNumberArea->isVisible()) {
setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
lineNumberArea->update();
}
else {
setViewportMargins(0, 0, 0, 0);
}
}
void CompletingEdit::updateLineNumberArea(const QRect &rect, int dy)
{
if (dy)
lineNumberArea->scroll(0, dy);
else
lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
if (rect.contains(viewport()->rect()))
updateLineNumberAreaWidth(0);
}
void CompletingEdit::resizeEvent(QResizeEvent *e)
{
QTextEdit::resizeEvent(e);
QRect cr = contentsRect();
lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
}
void CompletingEdit::lineNumberAreaPaintEvent(QPaintEvent *event)
{
QPainter painter(lineNumberArea);
painter.fillRect(event->rect(), Qt::lightGray);
QTextBlock block = document()->begin();
int blockNumber = 1;
QAbstractTextDocumentLayout *layout = document()->documentLayout();
int top = layout->blockBoundingRect(block).top() - verticalScrollBar()->value();
int bottom = top + layout->blockBoundingRect(block).height();
while (block.isValid() && top <= event->rect().bottom()) {
if (bottom >= event->rect().top()) {
QString number = QString::number(blockNumber);
painter.drawText(0, top, lineNumberArea->width() - 1, fontMetrics().height(),
Qt::AlignRight, number);
}
block = block.next();
if (block == document()->end())
break;
top = bottom;
bottom = top + (int)layout->blockBoundingRect(block).height();
++blockNumber;
}
}
bool CompletingEdit::event(QEvent *e)
{
if (e->type() == QEvent::UpdateRequest) {
// should be limiting the rect to what actually needs updating
// but don't know how to get that from the event :(
emit updateRequest(viewport()->rect(), 0);
}
return QTextEdit::event(e);
}
void CompletingEdit::scrollContentsBy(int dx, int dy)
{
if (dy != 0) {
emit updateRequest(viewport()->rect(), dy);
}
QTextEdit::scrollContentsBy(dx, dy);
}
void CompletingEdit::setHighlightCurrentLine(bool highlight)
{
if (highlight != highlightCurrentLine) {
highlightCurrentLine = highlight;
TWApp::instance()->emitHighlightLineOptionChanged();
}
}
QTextCharFormat *CompletingEdit::currentCompletionFormat = NULL;
QTextCharFormat *CompletingEdit::braceMatchingFormat = NULL;
QTextCharFormat *CompletingEdit::currentLineFormat = NULL;
bool CompletingEdit::highlightCurrentLine = true;
QCompleter *CompletingEdit::sharedCompleter = NULL;
QList *CompletingEdit::indentModes = NULL;
QList *CompletingEdit::quotesModes = NULL;