1 // Copyright (C) 2015 OPEN CASCADE
3 // This library is free software; you can redistribute it and/or
4 // modify it under the terms of the GNU Lesser General Public
5 // License as published by the Free Software Foundation; either
6 // version 2.1 of the License, or (at your option) any later version.
8 // This library is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 // Lesser General Public License for more details.
13 // You should have received a copy of the GNU Lesser General Public
14 // License along with this library; if not, write to the Free Software
15 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 // See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
19 // File : PyEditor_Editor.cxx
20 // Author : Maxim GLIBIN, Open CASCADE S.A.S. (maxim.glibin@opencascade.com)
24 #include "PyEditor_Editor.h"
25 #include "PyEditor_LineNumberArea.h"
26 #include "PyEditor_PyHighlighter.h"
27 #include "PyEditor_Settings.h"
30 #include <QtxResourceMgr.h>
37 \class PyEditor_Editor
38 \brief Viewer/Editor is used to edit and show advanced plain text.
43 \param isSingle flag determined single application or reccesed.
44 \param theParent parent widget
46 PyEditor_Editor::PyEditor_Editor( bool isSingle, QtxResourceMgr* theMgr, QWidget* theParent )
47 : QPlainTextEdit( theParent )
49 my_Settings = new PyEditor_Settings( theMgr, isSingle );
51 // Create line number area
52 my_LineNumberArea = new PyEditor_LineNumberArea( this );
53 my_LineNumberArea->setMouseTracking( true );
55 my_SyntaxHighlighter = new PyEditor_PyHighlighter( this->document() );
58 connect( this, SIGNAL( blockCountChanged( int ) ), this, SLOT( updateLineNumberAreaWidth( int ) ) );
59 connect( this, SIGNAL( updateRequest( QRect, int ) ), this, SLOT( updateLineNumberArea( QRect, int ) ) );
60 connect( this, SIGNAL( cursorPositionChanged() ), this, SLOT( updateHighlightCurrentLine() ) );
61 connect( this, SIGNAL( cursorPositionChanged() ), this, SLOT( matchParentheses() ) );
69 PyEditor_Editor::~PyEditor_Editor()
76 void PyEditor_Editor::updateStatement()
80 aFont.setFamily( settings()->p_Font.family() );
81 aFont.setPointSize( settings()->p_Font.pointSize() );
85 setLineWrapMode( settings()->p_TextWrapping ? QPlainTextEdit::WidgetWidth : QPlainTextEdit::NoWrap );
87 // Center the cursor on screen
88 setCenterOnScroll( settings()->p_CenterCursorOnScroll );
90 // Set size white spaces
91 setTabStopWidth( settings()->p_TabSize * 10 );
93 // Update current line highlight
94 updateHighlightCurrentLine();
96 // Update line numbers area
97 updateLineNumberAreaWidth( 0 );
99 my_SyntaxHighlighter->rehighlight();
100 viewport()->update();
104 SLOT: Delete the current selection's contents
106 void PyEditor_Editor::deleteSelected()
108 QTextCursor aCursor = textCursor();
109 if ( aCursor.hasSelection() )
110 aCursor.removeSelectedText();
111 setTextCursor( aCursor );
115 \brief Reimplemented calss is to receive key press events for the plain text widget.
116 \param theEvent event
118 void PyEditor_Editor::keyPressEvent( QKeyEvent* theEvent )
120 if ( theEvent->type() == QEvent::KeyPress )
122 int aKey = theEvent->key();
123 Qt::KeyboardModifiers aCtrl = theEvent->modifiers() & Qt::ControlModifier;
124 Qt::KeyboardModifiers aShift = theEvent->modifiers() & Qt::ShiftModifier;
126 if ( aKey == Qt::Key_Tab || ( aKey == Qt::Key_Backtab || ( aKey == Qt::Key_Tab && aShift ) ) )
128 QTextCursor aCursor = textCursor();
129 aCursor.beginEditBlock();
130 tabIndentation( aKey == Qt::Key_Backtab );
131 aCursor.endEditBlock();
134 else if ( aKey == Qt::Key_Enter || aKey == Qt::Key_Return )
136 QTextCursor aCursor = textCursor();
137 aCursor.beginEditBlock();
138 if ( lineIndent() == 0 )
140 QPlainTextEdit::keyPressEvent( theEvent );
142 aCursor.endEditBlock();
145 else if ( theEvent == QKeySequence::MoveToStartOfLine || theEvent == QKeySequence::SelectStartOfLine )
147 QTextCursor aCursor = this->textCursor();
148 if ( QTextLayout* aLayout = aCursor.block().layout() )
150 if ( aLayout->lineForTextPosition( aCursor.position() - aCursor.block().position() ).lineNumber() == 0 )
152 handleHome( theEvent == QKeySequence::SelectStartOfLine );
156 else if ( ( aKey == Qt::Key_Colon || ( aKey == Qt::Key_Space && !aCtrl && !aShift ) ) &&
157 !textCursor().hasSelection() )
159 QTextCursor aCursor = textCursor();
160 aCursor.movePosition( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
162 QString aSelectedText = aCursor.selectedText();
163 int numSpaces = findFirstNonSpace( aSelectedText );
164 int amountChars = aSelectedText.size() - findFirstNonSpace( aSelectedText );
165 QString aLeadingText = aSelectedText.right( amountChars );
167 QStringList aReservedWords;
168 aReservedWords.append( "except" );
169 if ( aKey == Qt::Key_Colon )
171 aReservedWords.append( "else" );
172 aReservedWords.append( "finally" );
174 else if ( aKey == Qt::Key_Space )
176 aReservedWords.append( "elif" );
179 if ( aReservedWords.contains( aLeadingText ) )
181 QString aPreviousText = aCursor.block().previous().text();
182 int numSpacesPrevious = findFirstNonSpace( aPreviousText );
183 if ( numSpaces == numSpacesPrevious )
185 tabIndentation( true );
186 aCursor.movePosition( QTextCursor::EndOfBlock );
187 setTextCursor( aCursor );
190 QPlainTextEdit::keyPressEvent( theEvent );
194 QPlainTextEdit::keyPressEvent( theEvent );
200 \brief Reimplemented calss is to receive plain text widget resize events
201 which are passed in the event parameter.
202 \param theEvent event
204 void PyEditor_Editor::resizeEvent( QResizeEvent* theEvent )
206 QPlainTextEdit::resizeEvent( theEvent );
208 // Change size geometry of line number area
209 QRect aContentsRect = contentsRect();
210 my_LineNumberArea->setGeometry(
211 QRect( aContentsRect.left(),
213 lineNumberAreaWidth(),
214 aContentsRect.height() ) );
218 \brief Reimplemented calss is to receive paint events passed in theEvent.
219 \param theEvent event
221 void PyEditor_Editor::paintEvent( QPaintEvent* theEvent )
223 QPlainTextEdit::paintEvent( theEvent );
225 QTextBlock aBlock( firstVisibleBlock() );
226 QPointF anOffset( contentOffset() );
227 QPainter aPainter( this->viewport() );
229 int aTabSpaces = this->tabStopWidth() / 10;
231 // Visualization tab spaces
232 if ( settings()->p_TabSpaceVisible )
234 qreal aTop = blockBoundingGeometry( aBlock ).translated( anOffset ).top();
235 while ( aBlock.isValid() && aTop <= theEvent->rect().bottom() )
237 if ( aBlock.isVisible() && blockBoundingGeometry( aBlock ).translated( anOffset ).toRect().intersects( theEvent->rect() ) )
239 QString aText = aBlock.text();
240 if ( aText.contains( QRegExp( "\\w+" ) ) )
241 aText.remove( QRegExp( "(?!\\w+)\\s+$" ) );
245 while ( anIndex != -1 )
247 anIndex = aText.indexOf( QRegExp( QString( "^\\s{%1}" ).arg( aTabSpaces ) ), 0 );
250 aColumn = aColumn + aTabSpaces;
251 aText = aText.mid( aTabSpaces );
253 if ( aText.startsWith( ' ' ) )
255 QTextCursor aCursor( aBlock );
256 aCursor.setPosition( aBlock.position() + aColumn );
258 QRect aRect = cursorRect( aCursor );
259 aPainter.setPen( QPen( Qt::darkGray, 1, Qt::DotLine ) );
260 aPainter.drawLine( aRect.x() + 1, aRect.top(), aRect.x() + 1, aRect.bottom() );
265 aBlock = aBlock.next();
269 // Vertical edge line
270 if ( settings()->p_VerticalEdge )
272 const QRect aRect = theEvent->rect();
273 const QFont aFont = currentCharFormat().font();
274 int aNumberColumn = QFontMetrics( aFont ).averageCharWidth() * settings()->p_NumberColumns + anOffset.x() + document()->documentMargin();
275 aPainter.setPen( QPen( Qt::lightGray, 1, Qt::SolidLine ) );
276 aPainter.drawLine( aNumberColumn, aRect.top(), aNumberColumn, aRect.bottom() );
281 \return manager of setting values.
283 PyEditor_Settings* PyEditor_Editor::settings()
289 \brief Indenting and tabbing of the text
290 \param isShift flag defines reverse tab
292 void PyEditor_Editor::tabIndentation( bool isShift )
294 QTextCursor aCursor = textCursor();
295 int aTabSpaces = this->tabStopWidth()/10;
297 if ( !aCursor.hasSelection() )
301 int N = aCursor.columnNumber() % aTabSpaces;
302 aCursor.insertText( QString( aTabSpaces - N, QLatin1Char( ' ' ) ) );
306 QTextBlock aCurrentBlock = document()->findBlock( aCursor.position() );
307 int anIndentPos = findFirstNonSpace( aCurrentBlock.text() );
308 aCursor.setPosition( aCurrentBlock.position() + anIndentPos );
309 setTextCursor( aCursor );
311 //if ( aCurrCursorColumnPos <= anIndentPos )
313 int aColumnPos = aCursor.columnNumber();
314 if ( aColumnPos != 0 )
316 int N = aCursor.columnNumber() % aTabSpaces;
317 if ( N == 0 ) N = aTabSpaces;
318 aCursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, N );
319 aCursor.removeSelectedText();
321 setTextCursor( aCursor );
327 indentSelection( isShift );
332 \brief Indenting and tabbing of the selected text
333 \param isShift flag defines reverse tab
335 void PyEditor_Editor::indentSelection( bool isShift )
337 QTextCursor aCursor = this->textCursor();
339 int aCursorStart = aCursor.selectionStart();
340 int aCursorEnd = aCursor.selectionEnd();
342 QTextBlock aStartBlock = document()->findBlock( aCursorStart );
343 QTextBlock anEndBlock = document()->findBlock( aCursorEnd - 1 ).next();
345 int aTabSpaces = this->tabStopWidth()/10;
347 for ( QTextBlock aBlock = aStartBlock; aBlock.isValid() && aBlock != anEndBlock; aBlock = aBlock.next() )
349 QString aText = aBlock.text();
350 int anIndentPos = findFirstNonSpace( aText );
351 int N = ( anIndentPos % aTabSpaces );
353 aCursor.setPosition( aBlock.position() + anIndentPos );
356 aCursor.insertText( QString( aTabSpaces - N, QLatin1Char( ' ' ) ) );
357 setTextCursor( aCursor );
361 int aColumnPos = aCursor.columnNumber();
362 if ( aColumnPos != 0 )
364 int blockN = aColumnPos % aTabSpaces;
365 if ( blockN == 0 ) blockN = aTabSpaces;
366 aCursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, blockN );
367 aCursor.removeSelectedText();
368 setTextCursor( aCursor );
373 // Reselect the selected lines
374 aCursor.setPosition( aStartBlock.position() );
375 aCursor.setPosition( anEndBlock.previous().position(), QTextCursor::KeepAnchor );
376 aCursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
377 setTextCursor( aCursor );
381 \brief Finds the first non-white sapce in theText.
382 \param theText string
383 \return index of the first non-white space
385 int PyEditor_Editor::findFirstNonSpace( const QString& theText )
388 while ( i < theText.size() )
390 if ( !theText.at(i).isSpace() )
398 \brief Auto line indenting
401 int PyEditor_Editor::lineIndent()
403 int aTabSpaces = this->tabStopWidth() / 10;
405 QTextCursor aCursor = textCursor();
406 aCursor.insertBlock();
407 setTextCursor( aCursor );
409 QTextBlock aCurrentBlock = aCursor.block();
410 if ( aCurrentBlock == document()->begin() )
413 QTextBlock aPreviousBlock = aCurrentBlock.previous();
415 QString aPreviousText;
418 if ( aPreviousBlock == document()->begin() )
420 aPreviousText = aPreviousBlock.text();
421 if ( aPreviousText.isEmpty() && aPreviousText.trimmed().isEmpty() )
426 // If the text of this block is not empty then break the loop.
427 aPreviousText = aPreviousBlock.text();
428 if ( !aPreviousText.isEmpty() && !aPreviousText.trimmed().isEmpty() )
431 aPreviousBlock = aPreviousBlock.previous();
434 int aTabIndentation = 0;
435 int anAmountIndentation = -1;
437 while ( i < aPreviousText.size() )
439 if ( !aPreviousText.at(i).isSpace() )
441 anAmountIndentation = findFirstNonSpace( aPreviousText );
451 if ( anAmountIndentation == -1 )
453 if ( aTabIndentation > 0 )
454 anAmountIndentation = aTabIndentation;
459 const QString aPreviousTrimmed = aPreviousText.trimmed();
460 if ( aPreviousTrimmed.endsWith( ":" ) )
462 anAmountIndentation += aTabSpaces;
466 if ( aPreviousTrimmed == "continue"
467 || aPreviousTrimmed == "break"
468 || aPreviousTrimmed == "pass"
469 || aPreviousTrimmed == "return"
470 || aPreviousTrimmed == "raise"
471 || aPreviousTrimmed.startsWith( "raise " )
472 || aPreviousTrimmed.startsWith( "return " ) )
473 anAmountIndentation -= aTabSpaces;
476 aCursor.insertText( QString( anAmountIndentation, QLatin1Char(' ') ) );
477 setTextCursor( aCursor );
483 \brief Set text cursor on home position.
485 void PyEditor_Editor::handleHome( bool isExtendLine )
487 QTextCursor aCursor = textCursor();
488 QTextCursor::MoveMode aMode = QTextCursor::MoveAnchor;
491 aMode = QTextCursor::KeepAnchor;
493 int anInitPos = aCursor.position();
494 int aBlockPos = aCursor.block().position();
496 QChar aCharacter = document()->characterAt( aBlockPos );
497 while ( aCharacter.category() == QChar::Separator_Space )
500 if ( aBlockPos == anInitPos )
502 aCharacter = document()->characterAt( aBlockPos );
505 if ( aBlockPos == anInitPos )
506 aBlockPos = aCursor.block().position();
508 aCursor.setPosition( aBlockPos, aMode );
509 setTextCursor( aCursor );
513 SLOT: Updates the highlight current line.
515 void PyEditor_Editor::updateHighlightCurrentLine()
517 QList<QTextEdit::ExtraSelection> anExtraSelections;
518 if ( !isReadOnly() && settings()->p_HighlightCurrentLine )
520 QTextEdit::ExtraSelection selection;
522 QColor lineColor = QColor( Qt::gray ).lighter( 155 );
524 selection.format.setBackground( lineColor );
525 selection.format.setProperty( QTextFormat::FullWidthSelection, QVariant( true ) );
526 selection.cursor = textCursor();
527 selection.cursor.clearSelection();
528 anExtraSelections.append( selection );
530 setExtraSelections( anExtraSelections );
534 \brief Creates line number area.
535 \param theEvent event for paint events.
537 void PyEditor_Editor::lineNumberAreaPaintEvent( QPaintEvent* theEvent )
539 QPainter aPainter( my_LineNumberArea );
540 aPainter.fillRect( theEvent->rect(), QColor( Qt::lightGray ).lighter( 125 ) );
542 QTextBlock aBlock = firstVisibleBlock();
543 int aBlockNumber = aBlock.blockNumber();
544 int aTop = (int)blockBoundingGeometry( aBlock ).translated( contentOffset() ).top();
545 int aBottom = aTop + (int)blockBoundingRect( aBlock ).height();
546 int aCurrentLine = document()->findBlock( textCursor().position() ).blockNumber();
548 QFont aFont = aPainter.font();
549 aPainter.setPen( this->palette().color( QPalette::Text ) );
551 while ( aBlock.isValid() && aTop <= theEvent->rect().bottom() )
553 if ( aBlock.isVisible() && aBottom >= theEvent->rect().top() )
555 if ( aBlockNumber == aCurrentLine )
557 aPainter.setPen( Qt::darkGray );
558 aFont.setBold( true );
559 aPainter.setFont( aFont );
563 aPainter.setPen( Qt::gray ) ;
564 aFont.setBold( false );
565 aPainter.setFont( aFont );
567 QString aNumber = QString::number( aBlockNumber + 1 );
568 aPainter.drawText( 0, aTop, my_LineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, aNumber );
571 aBlock = aBlock.next();
573 aBottom = aTop + (int)blockBoundingRect( aBlock ).height();
579 \return width of line number area
581 int PyEditor_Editor::lineNumberAreaWidth()
586 int aMaximum = qMax( 1, blockCount() );
587 while ( aMaximum >= 10 )
593 if ( settings()->p_LineNumberArea )
594 aSpace += 5 + fontMetrics().width( QLatin1Char( '9' ) ) * aDigits;
600 SLOT: Updates the width of line number area.
602 void PyEditor_Editor::updateLineNumberAreaWidth( int /*theNewBlockCount*/ )
604 setViewportMargins( lineNumberAreaWidth(), 0, 0, 0 );
608 SLOT: When the editor viewport has been scrolled.
610 void PyEditor_Editor::updateLineNumberArea( const QRect& theRect, int theDY )
613 my_LineNumberArea->scroll( 0, theDY );
615 my_LineNumberArea->update( 0, theRect.y(), my_LineNumberArea->width(), theRect.height() );
617 if ( theRect.contains( viewport()->rect() ) )
618 updateLineNumberAreaWidth( 0 );
622 \brief Parenthesis management.
623 SLOT: Walk through and check that we don't exceed 80 chars per line.
625 void PyEditor_Editor::matchParentheses()
627 PyEditor_PyHighlighter::TextBlockData* data =
628 static_cast<PyEditor_PyHighlighter::TextBlockData*>( textCursor().block().userData() );
632 QVector<PyEditor_PyHighlighter::ParenthesisInfo*> infoEntries = data->parentheses();
634 int aPos = textCursor().block().position();
636 for ( int i = 0; i < infoEntries.size(); ++i )
638 PyEditor_PyHighlighter::ParenthesisInfo* info = infoEntries.at(i);
640 int currentColumnPosition = textCursor().columnNumber();
641 if ( info->position == currentColumnPosition - 1 && isLeftBrackets( info->character ) )
643 if ( matchLeftParenthesis( textCursor().block(), i + 1, 0 ) )
644 createParenthesisSelection( aPos + info->position );
646 else if ( info->position == currentColumnPosition && isLeftBrackets( info->character ) )
650 if ( matchLeftParenthesis( textCursor().block(), i + 1, 0 ) )
651 createParenthesisSelection( aPos + info->position );
654 else if ( info->position == currentColumnPosition - 1 && isRightBrackets( info->character ) )
656 if ( matchRightParenthesis( textCursor().block(), i - 1, 0 ) )
657 createParenthesisSelection( aPos + info->position );
660 else if ( info->position == currentColumnPosition && isRightBrackets( info->character ) )
662 if ( matchRightParenthesis( textCursor().block(), i - 1, 0 ) )
663 createParenthesisSelection( aPos + info->position );
670 \brief Matches the left brackets.
671 \param theCurrentBlock text block
673 \param theNumLeftParentheses number of left parentheses
674 \return \c true if the left match
676 bool PyEditor_Editor::matchLeftParenthesis(
677 const QTextBlock& theCurrentBlock, int theI, int theNumLeftParentheses )
679 PyEditor_PyHighlighter::TextBlockData* data =
680 static_cast<PyEditor_PyHighlighter::TextBlockData*>( theCurrentBlock.userData() );
681 QVector<PyEditor_PyHighlighter::ParenthesisInfo*> infos = data->parentheses();
683 int docPos = theCurrentBlock.position();
684 for ( ; theI < infos.size(); ++theI )
686 PyEditor_PyHighlighter::ParenthesisInfo* info = infos.at(theI);
688 if ( isLeftBrackets( info->character ) )
690 ++theNumLeftParentheses;
694 if ( isRightBrackets( info->character ) && theNumLeftParentheses == 0 )
696 createParenthesisSelection( docPos + info->position );
700 --theNumLeftParentheses;
703 QTextBlock nextBlock = theCurrentBlock.next();
704 if ( nextBlock.isValid() )
705 return matchLeftParenthesis( nextBlock, 0, theNumLeftParentheses );
711 \brief Matches the right brackets.
712 \param theCurrentBlock text block
714 \param theNumRightParentheses number of right parentheses
715 \return \c true if the right match
717 bool PyEditor_Editor::matchRightParenthesis( const QTextBlock& theCurrentBlock, int theI, int theNumRightParentheses )
719 PyEditor_PyHighlighter::TextBlockData* data = static_cast<PyEditor_PyHighlighter::TextBlockData*>( theCurrentBlock.userData() );
720 QVector<PyEditor_PyHighlighter::ParenthesisInfo*> parentheses = data->parentheses();
722 int docPos = theCurrentBlock.position();
723 for ( ; theI > -1 && parentheses.size() > 0; --theI )
725 PyEditor_PyHighlighter::ParenthesisInfo* info = parentheses.at(theI);
726 if ( isRightBrackets( info->character ) )
728 ++theNumRightParentheses;
731 if ( isLeftBrackets( info->character ) && theNumRightParentheses == 0 )
733 createParenthesisSelection( docPos + info->position );
737 --theNumRightParentheses;
740 QTextBlock prevBlock = theCurrentBlock.previous();
741 if ( prevBlock.isValid() )
743 PyEditor_PyHighlighter::TextBlockData* data = static_cast<PyEditor_PyHighlighter::TextBlockData*>( prevBlock.userData() );
744 QVector<PyEditor_PyHighlighter::ParenthesisInfo*> parentheses = data->parentheses();
745 return matchRightParenthesis( prevBlock, parentheses.size() - 1, theNumRightParentheses );
752 \brief Creates brackets selection.
753 \param thePosition position
756 void PyEditor_Editor::createParenthesisSelection( int thePosition )
758 QList<QTextEdit::ExtraSelection> selections = extraSelections();
760 QTextEdit::ExtraSelection selection;
762 QTextCharFormat format = selection.format;
763 format.setForeground( Qt::red );
764 format.setBackground( Qt::white );
765 selection.format = format;
767 QTextCursor cursor = textCursor();
768 cursor.setPosition( thePosition );
769 cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor );
770 selection.cursor = cursor;
772 selections.append( selection );
773 setExtraSelections( selections );
777 return true whether the left bracket
779 bool PyEditor_Editor::isLeftBrackets( QChar theSymbol )
781 return theSymbol == '(' || theSymbol == '{' || theSymbol == '[';
785 Appends a new paragraph with text to the end of the text edit.
787 void PyEditor_Editor::append( const QString & text ) {
788 QPlainTextEdit::appendPlainText(text);
792 Sets the text edit's text.
794 void PyEditor_Editor::setText( const QString & text ) {
795 QPlainTextEdit::appendPlainText(text);
799 return true whether the right bracket
801 bool PyEditor_Editor::isRightBrackets( QChar theSymbol )
803 return theSymbol == ')' || theSymbol == '}' || theSymbol == ']';