File indexing completed on 2024-05-12 16:36:02
0001 /* This file is part of the KDE project 0002 Copyright 1999-2006 The KSpread Team <calligra-devel@kde.org> 0003 0004 This library is free software; you can redistribute it and/or 0005 modify it under the terms of the GNU Library General Public 0006 License as published by the Free Software Foundation; either 0007 version 2 of the License, or (at your option) any later version. 0008 0009 This library is distributed in the hope that it will be useful, 0010 but WITHOUT ANY WARRANTY; without even the implied warranty of 0011 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 0012 Library General Public License for more details. 0013 0014 You should have received a copy of the GNU Library General Public License 0015 along with this library; see the file COPYING.LIB. If not, write to 0016 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 0017 Boston, MA 02110-1301, USA. 0018 */ 0019 0020 #include "CellEditor.h" 0021 0022 // Sheets 0023 #include "ApplicationSettings.h" 0024 #include "CellToolBase.h" 0025 #include "Formula.h" 0026 #include "FormulaEditorHighlighter.h" 0027 #include "Function.h" 0028 #include "FunctionRepository.h" 0029 #include "FunctionCompletion.h" 0030 #include "Selection.h" 0031 #include "Sheet.h" 0032 #include "Style.h" 0033 #include "Cell.h" 0034 #include "ValueConverter.h" 0035 #include "RectStorage.h" 0036 #include "Map.h" 0037 #include "Value.h" 0038 #include "CellStorage.h" 0039 #include "SheetsDebug.h" 0040 0041 // Calligra 0042 #include <KoDpi.h> 0043 #include <KoUnit.h> 0044 #include <KoViewConverter.h> 0045 // KF5 0046 #include <ktextedit.h> 0047 0048 // Qt 0049 #include <QFocusEvent> 0050 #include <QKeyEvent> 0051 #include <QCompleter> 0052 #include <QAbstractItemView> 0053 #include <QStringListModel> 0054 #include <QScrollBar> 0055 #include <QList> 0056 0057 using namespace Calligra::Sheets; 0058 0059 class CellEditor::Private 0060 { 0061 public: 0062 CellToolBase* cellTool; 0063 Selection* selection; 0064 KTextEdit* textEdit; 0065 FormulaEditorHighlighter* highlighter; 0066 FunctionCompletion* functionCompletion; 0067 QTimer* functionCompletionTimer; 0068 QHash<int, QString> *wordCollection; 0069 QPoint globalCursorPos; 0070 QCompleter *complete; 0071 bool captureAllKeyEvents : 1; 0072 bool selectionChangedLocked : 1; 0073 int currentToken; 0074 0075 public: 0076 void updateActiveSubRegion(const Tokens &tokens); 0077 void rebuildSelection(); 0078 }; 0079 0080 void CellEditor::Private::updateActiveSubRegion(const Tokens &tokens) 0081 { 0082 // Index of the token, at which the text cursor is positioned. 0083 // For sub-regions it is the start range. 0084 currentToken = 0; 0085 0086 if (tokens.isEmpty()) { 0087 selection->setActiveSubRegion(0, 0); // also set the active element 0088 return; 0089 } 0090 0091 const int cursorPosition = textEdit->textCursor().position() - 1; // without '=' 0092 debugSheets << "cursorPosition:" << cursorPosition << "textLength:" << textEdit->toPlainText().length() - 1; 0093 0094 uint rangeCounter = 0; // counts the ranges in the sub-region 0095 uint currentRange = 0; // range index denoting the current range 0096 int regionStart = 0; // range index denoting the sub-region start 0097 uint regionEnd = 0; // range index denoting the sub-region end 0098 enum { Anywhere, InRegion, BeyondCursor } state = Anywhere; 0099 0100 Token token; 0101 Token::Type type; 0102 // Search the current range the text cursor is positioned to. 0103 // Determine the subregion start and end, in which the range is located. 0104 for (int i = 0; i < tokens.count(); ++i) { 0105 token = tokens[i]; 0106 type = token.type(); 0107 0108 // If not in a subregion, we may already quit the loop here. 0109 if (state == Anywhere) { 0110 // Already beyond the cursor position? 0111 if (token.pos() > cursorPosition) { 0112 state = BeyondCursor; 0113 break; // for loop 0114 } 0115 } else if (state == InRegion) { 0116 // Loop to the end of the subregion. 0117 if (type == Token::Cell || type == Token::Range) { 0118 regionEnd = rangeCounter++; 0119 continue; // keep going until the referenced region ends 0120 } 0121 if (type == Token::Operator) { 0122 if (tokens[i].asOperator() == Token::Semicolon) { 0123 continue; // keep going until the referenced region ends 0124 } 0125 } 0126 state = Anywhere; 0127 continue; 0128 } 0129 0130 // Can the token be replaced by a reference? 0131 switch (type) { 0132 case Token::Cell: 0133 case Token::Range: 0134 if (state == Anywhere) { 0135 currentToken = i; 0136 regionStart = rangeCounter; 0137 state = InRegion; 0138 } 0139 regionEnd = rangeCounter; // length = 1 0140 currentRange = ++rangeCounter; // point behind the last 0141 continue; 0142 case Token::Unknown: 0143 case Token::Boolean: 0144 case Token::Integer: 0145 case Token::Float: 0146 case Token::String: 0147 case Token::Error: 0148 // Set the active sub-region start to the next range but 0149 // with a length of 0, which results in inserting a new range 0150 // to the selection on calling Selection::initialize() or 0151 // Selection::update(). 0152 currentToken = i; 0153 regionStart = rangeCounter; // position of the next range 0154 regionEnd = rangeCounter - 1; // length = 0 0155 currentRange = rangeCounter; 0156 continue; 0157 case Token::Operator: 0158 case Token::Identifier: 0159 continue; 0160 } 0161 } 0162 0163 // Cursor not reached? I.e. the cursor is placed at the last token's end. 0164 if (state == Anywhere) { 0165 token = tokens.last(); 0166 type = token.type(); 0167 // Check the last token. 0168 // It was processed, but maybe a reference can be placed behind it. 0169 // Check, if the token can be replaced by a reference. 0170 switch (type) { 0171 case Token::Operator: 0172 // Possible to place a reference behind the operator? 0173 switch (token.asOperator()) { 0174 case Token::Plus: 0175 case Token::Minus: 0176 case Token::Asterisk: 0177 case Token::Slash: 0178 case Token::Caret: 0179 case Token::LeftPar: 0180 case Token::Semicolon: 0181 case Token::Equal: 0182 case Token::NotEqual: 0183 case Token::Less: 0184 case Token::Greater: 0185 case Token::LessEqual: 0186 case Token::GreaterEqual: 0187 case Token::Intersect: 0188 case Token::Union: 0189 // Append new references by pointing behind the last. 0190 currentToken = tokens.count(); 0191 regionStart = rangeCounter; 0192 regionEnd = rangeCounter - 1; // length = 0 0193 currentRange = rangeCounter; 0194 break; 0195 case Token::InvalidOp: 0196 case Token::RightPar: 0197 case Token::Comma: 0198 case Token::Ampersand: 0199 case Token::Percent: 0200 case Token::CurlyBra: 0201 case Token::CurlyKet: 0202 case Token::Pipe: 0203 // reference cannot be placed behind 0204 break; 0205 } 0206 break; 0207 case Token::Unknown: 0208 case Token::Boolean: 0209 case Token::Integer: 0210 case Token::Float: 0211 case Token::String: 0212 case Token::Identifier: 0213 case Token::Error: 0214 // currentToken = tokens.count() - 1; // already set 0215 // Set the active sub-region start to the end of the selection 0216 // with a length of 0, which results in appending a new range 0217 // to the selection on calling Selection::initialize() or 0218 // Selection::update(). 0219 regionStart = rangeCounter; 0220 regionEnd = rangeCounter - 1; // length = 0 0221 currentRange = rangeCounter; 0222 break; 0223 case Token::Cell: 0224 case Token::Range: 0225 // currentToken = tokens.count() - 1; // already set 0226 // Set the last range as active one. It is not a sub-region, 0227 // otherwise the state would have been InRegion. 0228 regionStart = rangeCounter - 1; 0229 regionEnd = rangeCounter - 1; // length = 1 0230 currentRange = rangeCounter; // point behind the last 0231 break; 0232 } 0233 } 0234 0235 const int regionLength = regionEnd - regionStart + 1; 0236 debugSheets << "currentRange:" << currentRange << "regionStart:" << regionStart 0237 << "regionEnd:" << regionEnd << "regionLength:" << regionLength; 0238 0239 selection->setActiveSubRegion(regionStart, regionLength, currentRange); 0240 } 0241 0242 0243 CellEditor::CellEditor(CellToolBase *cellTool,QHash<int,QString> &wordList, QWidget* parent) 0244 : KTextEdit(parent) 0245 , d(new Private) 0246 { 0247 d->cellTool = cellTool; 0248 d->selection = cellTool->selection(); 0249 d->textEdit = this; 0250 d->globalCursorPos = QPoint(); 0251 d->captureAllKeyEvents = false; 0252 d->selectionChangedLocked = false; 0253 d->currentToken = 0; 0254 d->wordCollection = &wordList; 0255 0256 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 0257 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 0258 setFrameStyle(QFrame::NoFrame); 0259 setLineWidth(0); 0260 document()->setDocumentMargin(0); 0261 // setMinimumHeight(fontMetrics().height()); 0262 0263 d->highlighter = new FormulaEditorHighlighter(this, d->selection); 0264 0265 d->functionCompletion = new FunctionCompletion(this); 0266 d->functionCompletionTimer = new QTimer(this); 0267 0268 const Cell cell(d->selection->activeSheet(), d->selection->marker()); 0269 const bool wrapText = cell.style().wrapText(); 0270 d->textEdit->setWordWrapMode(wrapText ? QTextOption::WordWrap : QTextOption::NoWrap); 0271 0272 #if 0 // FIXME Implement a completion aware KTextEdit. 0273 setCompletionMode(selection()->view()->doc()->completionMode()); 0274 setCompletionObject(&selection()->view()->doc()->map()->stringCompletion(), true); 0275 #endif 0276 0277 //populateWordCollection(); 0278 0279 //AutoCompletion Code 0280 d->complete = new QCompleter(this); 0281 d->complete->setModel(model()); 0282 //completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel); 0283 d->complete->setCaseSensitivity(Qt::CaseInsensitive); 0284 d->complete->setWrapAround(false); 0285 this->setCompleter(d->complete); 0286 0287 connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(slotCursorPositionChanged())); 0288 connect(this, SIGNAL(textChanged()), this, SLOT(slotTextChanged())); 0289 } 0290 0291 CellEditor::~CellEditor() 0292 { 0293 if (selection()){ 0294 selection()->endReferenceSelection(); 0295 } 0296 delete d; 0297 } 0298 0299 Selection* CellEditor::selection() const 0300 { 0301 return d->selection; 0302 } 0303 0304 QPoint CellEditor::globalCursorPosition() const 0305 { 0306 return d->globalCursorPos; 0307 } 0308 0309 bool CellEditor::captureArrowKeys() const 0310 { 0311 return d->captureAllKeyEvents; 0312 } 0313 0314 void CellEditor::setCaptureArrowKeys(bool capture) 0315 { 0316 d->captureAllKeyEvents = capture; 0317 } 0318 0319 //AutoCompletion Functions 0320 0321 QAbstractItemModel *CellEditor::model() 0322 { 0323 //ValueConverter *conv; 0324 QList<QString> words; 0325 QList<QString> wordlist; 0326 const Cell cell(d->selection->activeSheet(), d->selection->marker()); 0327 int col = cell.column(); 0328 words = d->wordCollection->values(col); 0329 for (int i = 0; i < 3 && (!words.isEmpty()) ; i++) { 0330 wordlist.push_back(words.front()); 0331 words.pop_front(); 0332 } 0333 return new QStringListModel(wordlist, d->complete); 0334 } 0335 0336 void CellEditor::setCompleter(QCompleter *completer) 0337 { 0338 if (d->complete) 0339 QObject::disconnect(d->complete, 0, this, 0); 0340 0341 d->complete = completer; 0342 0343 if (!d->complete) 0344 return; 0345 0346 d->complete->setWidget(this); 0347 d->complete->setCompletionMode(QCompleter::PopupCompletion); 0348 d->complete->setCaseSensitivity(Qt::CaseInsensitive); 0349 QObject::connect(d->complete, SIGNAL(activated(QString)), 0350 this, SLOT(insertCompletion(QString))); 0351 } 0352 0353 QCompleter *CellEditor::completer() const 0354 { 0355 return d->complete; 0356 } 0357 0358 void CellEditor::insertCompletion(const QString& completion) 0359 { 0360 if (d->complete->widget() != this) 0361 return; 0362 QTextCursor tc = textCursor(); 0363 int extra = completion.length() - d->complete->completionPrefix().length(); 0364 tc.movePosition(QTextCursor::Left); 0365 tc.movePosition(QTextCursor::EndOfWord); 0366 tc.insertText(completion.right(extra)); 0367 setTextCursor(tc); 0368 } 0369 0370 QString CellEditor::textUnderCursor() const 0371 { 0372 QTextCursor tc = textCursor(); 0373 tc.select(QTextCursor::WordUnderCursor); 0374 return tc.selectedText(); 0375 } 0376 0377 void CellEditor::slotCursorPositionChanged() 0378 { 0379 // Nothing to do, if not in reference selection mode. 0380 if (!selection()->referenceSelection()) { 0381 return; 0382 } 0383 // NOTE On text changes TextEdit::cursorPositionChanged() is triggered 0384 // before TextEdit::textChanged(). The text is already up-to-date. 0385 0386 // Save the global position for the function auto-completion popup. 0387 d->globalCursorPos = mapToGlobal(cursorRect().bottomLeft()); 0388 0389 // Needs up-to-date tokens; QSyntaxHighlighter::rehighlight() gets called 0390 // automatically on text changes, which does the update. 0391 d->updateActiveSubRegion(d->highlighter->formulaTokens()); 0392 } 0393 0394 void CellEditor::Private::rebuildSelection() 0395 { 0396 // Do not react on selection changes, that update the formula's expression, 0397 // because the selection gets already build based on the current formula. 0398 selectionChangedLocked = true; 0399 0400 Sheet *const originSheet = selection->originSheet(); 0401 Map *const map = originSheet->map(); 0402 0403 // Rebuild the reference selection by using the formula tokens. 0404 Tokens tokens = highlighter->formulaTokens(); 0405 selection->update(); // set the old cursor dirty; updates the editors 0406 selection->clear(); 0407 0408 //A list of regions which have already been highlighted on the spreadsheet. 0409 //This is so that we don't end up highlighting the same region twice in two different 0410 //colors. 0411 QSet<QString> alreadyUsedRegions; 0412 0413 int counter = 0; 0414 for (int i = 0; i < tokens.count(); ++i) { 0415 const Token token = tokens[i]; 0416 const Token::Type type = token.type(); 0417 0418 if (type == Token::Cell || type == Token::Range) { 0419 const Region region(token.text(), map, originSheet); 0420 0421 if (!region.isValid() || region.isEmpty()) { 0422 continue; 0423 } 0424 if (alreadyUsedRegions.contains(region.name())) { 0425 continue; 0426 } 0427 alreadyUsedRegions.insert(region.name()); 0428 0429 const QRect range = region.firstRange(); 0430 Sheet *const sheet = region.firstSheet(); 0431 0432 selection->initialize(range, sheet); 0433 // Always append the next range by pointing behind the last item. 0434 selection->setActiveSubRegion(++counter, 0); 0435 } 0436 } 0437 0438 // Set the active sub-region. 0439 // Needs up-to-date tokens; QSyntaxHighlighter::rehighlight() gets called 0440 // automatically on text changes, which does the update. 0441 updateActiveSubRegion(highlighter->formulaTokens()); 0442 0443 selectionChangedLocked = false; 0444 } 0445 0446 void CellEditor::setEditorFont(QFont const & font, bool updateSize, const KoViewConverter *viewConverter) 0447 { 0448 const qreal scaleY = POINT_TO_INCH(static_cast<qreal>((KoDpi::dpiY()))); 0449 setFont(QFont(font.family(), viewConverter->documentToViewY(font.pointSizeF()) / scaleY)); 0450 0451 if (updateSize) { 0452 QFontMetrics fontMetrics(this->font()); 0453 int width = fontMetrics.width(toPlainText()) + fontMetrics.averageCharWidth(); 0454 // don't make it smaller: then we would have to repaint the obscured cells 0455 if (width < this->width()) 0456 width = this->width(); 0457 int height = fontMetrics.height(); 0458 if (height < this->height()) 0459 height = this->height(); 0460 setGeometry(x(), y(), width, height); 0461 } 0462 } 0463 0464 void CellEditor::slotCompletionModeChanged(KCompletion::CompletionMode _completion) 0465 { 0466 selection()->activeSheet()->map()->settings()->setCompletionMode(_completion); 0467 } 0468 0469 void CellEditor::slotTextChanged() 0470 { 0471 // NOTE On text changes TextEdit::cursorPositionChanged() is triggered 0472 // before TextEdit::textChanged(). 0473 0474 // Fix the position. 0475 verticalScrollBar()->setValue(1); 0476 0477 const QString text = toPlainText(); 0478 0479 const QFontMetricsF fontMetrics(font()); 0480 // TODO Adjust size depending on which cells can be obscured (see CellView)? 0481 // The following line would result in an unchanged width for cells with 0482 // enabled word wrapping, but after the user input got applied it may 0483 // obscure cells horizontally. 0484 // Passing no flags will only change the height, if a manual line break is 0485 // entered (Shift + Return). 0486 // const int flags = wordWrapMode() == QTextOption::NoWrap ? 0 : Qt::TextWordWrap; 0487 const QRectF rect = fontMetrics.boundingRect(this->rect(), 0, text); 0488 const int requiredWidth = rect.width(); 0489 const int requiredHeight = rect.height() - 1; // -1 to fit into a default cell 0490 if (text.isRightToLeft()) { 0491 setGeometry(x() - requiredWidth + width(), y(), requiredWidth, requiredHeight); 0492 } else { 0493 setGeometry(x(), y(), requiredWidth, requiredHeight); 0494 } 0495 0496 // FIXME Stefan: Is this really wanted? The percent char does not get 0497 // removed on applying the user input. If the style changes afterwards, 0498 // the user input is still indicating a percent value. If the digit gets 0499 // deleted while editing the percent char also stays. Disabling for now. 0500 #if 0 // CALLIGRA_SHEETS_WIP_EDITOR_OVERHAUL 0501 const Cell cell(d->selection->activeSheet(), d->selection->marker()); 0502 if ((cell.style().formatType()) == Format::Percentage) { 0503 if ((text.length() == 1) && text[0].isDigit()) { 0504 setPlainText(text + " %"); 0505 setCursorPosition(1); 0506 return; 0507 } 0508 } 0509 #endif // CALLIGRA_SHEETS_WIP_EDITOR_OVERHAUL 0510 0511 // update the external editor, but only if we have focus 0512 if (hasFocus()) { 0513 emit textChanged(text); 0514 } 0515 0516 // Enable/disable the reference selection. 0517 if (!text.isEmpty() && text[0] == '=') { 0518 selection()->startReferenceSelection(); 0519 } else { 0520 selection()->endReferenceSelection(); 0521 return; 0522 } 0523 0524 // The expression highlighting got updated automatically. 0525 // If a reference has changed since the last update ... 0526 if (d->highlighter->rangeChanged()) { 0527 // Reset the flag, that indicates range changes after text changes. 0528 d->highlighter->resetRangeChanged(); 0529 // Rebuild the reference selection by using the formula tokens. 0530 d->rebuildSelection(); 0531 } 0532 } 0533 0534 // Called on selection (and sheet) changes. 0535 void CellEditor::selectionChanged() 0536 { 0537 if (d->selectionChangedLocked) { 0538 return; 0539 } 0540 0541 Selection* choice = selection(); 0542 0543 if (choice->isEmpty()) 0544 return; 0545 0546 const QString text = toPlainText(); 0547 const int textLength = text.length(); 0548 0549 // Find the start text cursor position for the active sub-region within 0550 // the formula's expression and determine the length of the sub-region. 0551 Tokens tokens = d->highlighter->formulaTokens(); 0552 uint start = 1; 0553 uint length = 0; 0554 if (!tokens.empty()) { 0555 if (d->currentToken < tokens.count()) { 0556 Token token = tokens[d->currentToken]; 0557 Token::Type type = token.type(); 0558 if (type == Token::Cell || type == Token::Range) { 0559 start = token.pos() + 1; // don't forget the '='! 0560 length = token.text().length(); 0561 // Iterate to the end of the sub-region. 0562 for (int i = d->currentToken + 1; i < tokens.count(); ++i) { 0563 token = tokens[i]; 0564 type = token.type(); 0565 switch (type) { 0566 case Token::Cell: 0567 case Token::Range: 0568 length += token.text().length(); 0569 continue; 0570 case Token::Operator: 0571 if (token.asOperator() == Token::Semicolon) { 0572 ++length; 0573 continue; 0574 } 0575 default: 0576 break; 0577 } 0578 break; 0579 } 0580 } else { 0581 start = token.pos() + 1; // don't forget the '='! 0582 length = token.text().length(); 0583 } 0584 } else { 0585 // sanitize 0586 d->currentToken = tokens.count(); 0587 start = textLength; 0588 } 0589 } 0590 0591 // Replace the formula's active sub-region with the selection's one. 0592 const QString address = choice->activeSubRegionName(); 0593 const QString newExpression = QString(text).replace(start, length, address); 0594 // The expression highlighting gets updated automatically by the next call, 0595 // even though signals are blocked (must be connected to QTextDocument). 0596 blockSignals(true); 0597 setText(newExpression, start + address.length()); 0598 blockSignals(false); 0599 0600 // Ranges have changed. 0601 // Reset the flag, that indicates range changes after text changes. 0602 d->highlighter->resetRangeChanged(); 0603 // Mirror the behaviour of slotCursorPositionChanged(), but here the tokens 0604 // are already up-to-date. 0605 d->globalCursorPos = mapToGlobal(cursorRect().bottomLeft()); 0606 // Set the active sub-region. 0607 // Needs up-to-date tokens; QSyntaxHighlighter::rehighlight() gets called 0608 // automatically on text changes, which does the update. 0609 d->updateActiveSubRegion(d->highlighter->formulaTokens()); 0610 0611 // Always emit, because this editor may be hidden or does not have focus, 0612 // but the external one needs an update. 0613 emit textChanged(toPlainText()); 0614 } 0615 0616 void CellEditor::keyPressEvent(QKeyEvent *event) 0617 { 0618 const Cell cell_temp(d->selection->activeSheet(), d->selection->marker()); 0619 0620 switch (event->key()) { 0621 case Qt::Key_Left: 0622 case Qt::Key_Right: 0623 case Qt::Key_Up: 0624 case Qt::Key_Down: 0625 case Qt::Key_PageDown: 0626 case Qt::Key_PageUp: 0627 // Forward left/right arrows to parent, so that pressing left/right 0628 // in this editor leaves editing mode, unless this editor has been 0629 // set to capture arrow key events. 0630 if (d->captureAllKeyEvents) { 0631 break; // pass to TextEdit 0632 } 0633 event->ignore(); // pass to parent 0634 return; 0635 case Qt::Key_Tab: 0636 case Qt::Key_Backtab: 0637 // Always forward tab/backtab to parent, so that pressing them leaves 0638 // editing mode. To insert literal tabs you can always use the external 0639 // editor. 0640 0641 if (!textUnderCursor().isEmpty() && !d->wordCollection->values(cell_temp.column()).contains(textUnderCursor())) { 0642 d->wordCollection->insertMulti(cell_temp.column(), textUnderCursor()); 0643 } 0644 event->ignore(); 0645 return; 0646 case Qt::Key_Return: 0647 case Qt::Key_Enter: 0648 // Shift + Return: manual line wrap 0649 if (event->modifiers() & Qt::ShiftModifier) { 0650 break; // pass to TextEdit 0651 } 0652 if (!textUnderCursor().isEmpty() && !d->wordCollection->values(cell_temp.column()).contains(textUnderCursor())) { 0653 d->wordCollection->insertMulti(cell_temp.column(), textUnderCursor()); 0654 } 0655 event->ignore(); // pass to parent 0656 return; 0657 } 0658 0659 if (d->complete && d->complete->popup()->isVisible()) { 0660 // The following keys are forwarded by the completer to the widget 0661 switch (event->key()) { 0662 case Qt::Key_Enter: 0663 case Qt::Key_Return: 0664 case Qt::Key_Escape: 0665 case Qt::Key_Tab: 0666 case Qt::Key_Backtab: 0667 event->ignore(); 0668 return; // let the completer do default behavior 0669 default: 0670 break; 0671 } 0672 } 0673 0674 0675 bool isShortcut = ((event->modifiers() & Qt::ControlModifier) && event->key() == Qt::Key_E); // CTRL+E 0676 if (!d->complete || !isShortcut) // do not process the shortcut when we have a completer 0677 QTextEdit::keyPressEvent(event); 0678 0679 const bool ctrlOrShift = event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier); 0680 if (!d->complete || (ctrlOrShift && event->text().isEmpty())) 0681 return; 0682 0683 static QString eow("~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-="); // end of word 0684 bool hasModifier = (event->modifiers() != Qt::NoModifier) && !ctrlOrShift; 0685 QString completionPrefix = textUnderCursor(); 0686 0687 if (!isShortcut && (hasModifier || event->text().isEmpty()|| completionPrefix.length() < 3||eow.contains(event->text().right(1)))) { 0688 d->complete->popup()->hide(); 0689 return; 0690 } 0691 if (completionPrefix != d->complete->completionPrefix()) { 0692 d->complete->setCompletionPrefix(completionPrefix); 0693 d->complete->popup()->setCurrentIndex(d->complete->completionModel()->index(0, 0)); 0694 } 0695 QRect cr = cursorRect(); 0696 cr.setWidth(d->complete->popup()->sizeHintForColumn(0) + d->complete->popup()->verticalScrollBar()->sizeHint().width()); 0697 d->complete->complete(); // pop it up! 0698 0699 } 0700 0701 void CellEditor::focusInEvent(QFocusEvent *event) 0702 { 0703 if (d->complete) 0704 d->complete->setWidget(this); 0705 KTextEdit::focusInEvent(event); 0706 // If the focussing is user induced. 0707 if (event->reason() != Qt::OtherFocusReason) { 0708 debugSheets << "induced by user"; 0709 d->cellTool->setLastEditorWithFocus(CellToolBase::EmbeddedEditor); 0710 } 0711 KTextEdit::focusInEvent(event); 0712 } 0713 0714 void CellEditor::focusOutEvent(QFocusEvent *event) 0715 { 0716 KTextEdit::focusOutEvent(event); 0717 } 0718 0719 void CellEditor::setText(const QString& text, int cursorPos) 0720 { 0721 if (text == toPlainText()) return; 0722 0723 setPlainText(text); 0724 0725 // Only update the cursor position, if a non-negative value was set. 0726 // The default parameter value is -1, i.e. the cursor does not get touched. 0727 if (cursorPos >= 0) { 0728 if (cursorPos > text.length()) { 0729 // Usability: It is usually more convenient, if the cursor is 0730 // positioned at the end of the text so it can be quickly deleted 0731 // using the backspace key. 0732 cursorPos = text.length(); 0733 } 0734 setCursorPosition(cursorPos); 0735 } 0736 } 0737 0738 // helper for CellEditor::permuteFixation() 0739 QString permuteLocationFixation(const QString ®ionName, int &i, 0740 bool columnFixed, bool rowFixed) 0741 { 0742 QString result; 0743 if (columnFixed) { 0744 result += '$'; 0745 } 0746 // copy the column letter(s) 0747 while (i < regionName.count()) { 0748 if (!regionName[i].isLetter()) { 0749 if (regionName[i] == '$') { 0750 // swallow the old fixation 0751 ++i; 0752 continue; 0753 } 0754 // stop, if not a column letter 0755 break; 0756 } 0757 result += regionName[i++]; 0758 } 0759 if (rowFixed) { 0760 result += '$'; 0761 } 0762 // copy the row number(s) 0763 while (i < regionName.count()) { 0764 if (!regionName[i].isNumber()) { 0765 if (regionName[i] == '$') { 0766 // swallow the old fixation 0767 ++i; 0768 continue; 0769 } 0770 // stop, if not a row number 0771 break; 0772 } 0773 result += regionName[i++]; 0774 } 0775 return result; 0776 } 0777 0778 void CellEditor::permuteFixation() 0779 { 0780 // Nothing to do, if not in reference selection mode. 0781 if (!d->selection->referenceSelection()) { 0782 return; 0783 } 0784 0785 // Search for the last range before or the range at the cursor. 0786 int index = -1; 0787 const int cursorPosition = textCursor().position() - 1; // - '=' 0788 const Tokens tokens = d->highlighter->formulaTokens(); 0789 for (int i = 0; i < tokens.count(); ++i) { 0790 const Token token = tokens[i]; 0791 if (token.pos() > cursorPosition) { 0792 break; // for loop 0793 } 0794 if (token.type() == Token::Cell || token.type() == Token::Range) { 0795 index = i; 0796 } 0797 } 0798 // Quit, if no range was found. 0799 if (index == -1) { 0800 return; 0801 } 0802 0803 const Token token = tokens[index]; 0804 Map *const map = d->selection->activeSheet()->map(); 0805 QString regionName = token.text(); 0806 // Filter sheet; truncates regionName; range without sheet name resides. 0807 Sheet *const sheet = Region(QString(), map).filterSheetName(regionName); 0808 const Region region(regionName, map, 0); 0809 // TODO Stefan: Skip named areas. 0810 if (!region.isValid()) { 0811 return; 0812 } 0813 // FIXME Stefan: need access to fixation, thus to Region::Range; must use iterator 0814 Region::Element *range = (*region.constBegin()); 0815 QString result(sheet ? (sheet->sheetName() + '!') : QString()); 0816 // Permute fixation. 0817 if (region.isSingular()) { 0818 char fixation = 0x00; 0819 if (range->isRowFixed()) { 0820 fixation += 0x01; 0821 } 0822 if (range->isColumnFixed()) { 0823 fixation += 0x02; 0824 } 0825 fixation += 0x01; 0826 0827 int i = 0; 0828 result += permuteLocationFixation(regionName, i, fixation & 0x02, fixation & 0x01); 0829 } else { 0830 char fixation = 0x00; 0831 if (range->isBottomFixed()) { 0832 fixation += 0x01; 0833 } 0834 if (range->isRightFixed()) { 0835 fixation += 0x02; 0836 } 0837 if (range->isTopFixed()) { 0838 fixation += 0x04; 0839 } 0840 if (range->isLeftFixed()) { 0841 fixation += 0x08; 0842 } 0843 fixation += 0x01; 0844 0845 int i = 0; 0846 result += permuteLocationFixation(regionName, i, fixation & 0x08, fixation & 0x04); 0847 Q_ASSERT(regionName[i] == ':'); 0848 ++i; 0849 result += ':'; 0850 result += permuteLocationFixation(regionName, i, fixation & 0x02, fixation & 0x01); 0851 } 0852 // Replace the range in the formula's expression. 0853 QString text = toPlainText(); 0854 const int start = token.pos() + 1; // + '=' 0855 const int length = token.text().length(); 0856 setPlainText(text.replace(start, length, result)); 0857 // Set the cursor to the end of the range. 0858 QTextCursor textCursor = this->textCursor(); 0859 textCursor.setPosition(start + result.length()); 0860 setTextCursor(textCursor); 0861 } 0862 0863 int CellEditor::cursorPosition() const 0864 { 0865 return textCursor().position(); 0866 } 0867 0868 void CellEditor::setCursorPosition(int pos) 0869 { 0870 QTextCursor textCursor(this->textCursor()); 0871 textCursor.setPosition(pos); 0872 setTextCursor(textCursor); 0873 } 0874 0875 // Called by the cell tool when setting the active element with a cell location. 0876 void CellEditor::setActiveSubRegion(int index) 0877 { 0878 index = qBound(0, index, (int)d->highlighter->rangeCount()); 0879 int counter = 0; 0880 bool subRegion = false; 0881 const Tokens tokens = d->highlighter->formulaTokens(); 0882 for (int i = 0; i < tokens.count(); ++i) { 0883 const Token token = tokens[i]; 0884 switch (token.type()) { 0885 case Token::Cell: 0886 case Token::Range: 0887 if (!subRegion) { 0888 d->currentToken = i; 0889 subRegion = true; 0890 } 0891 if (counter == index) { 0892 setCursorPosition(token.pos() + token.text().length() + 1); 0893 return; 0894 } 0895 ++counter; 0896 continue; 0897 case Token::Operator: 0898 if (token.asOperator() == Token::Semicolon) { 0899 if (subRegion) { 0900 continue; 0901 } 0902 } 0903 subRegion = false; 0904 continue; 0905 default: 0906 subRegion = false; 0907 continue; 0908 } 0909 } 0910 }