1 // Copyright (C) 2015-2016 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)
23 #include "PyEditor_Editor.h"
24 #include "PyEditor_LineNumberArea.h"
25 #include "PyEditor_PyHighlighter.h"
26 #include "PyEditor_Settings.h"
32 \class PyEditor_Editor
33 \brief Widget to show / edit Python scripts.
38 \param parent parent widget
40 PyEditor_Editor::PyEditor_Editor( QWidget* parent )
41 : QPlainTextEdit( parent )
43 // Set up line number area
44 myLineNumberArea = new PyEditor_LineNumberArea( this );
45 myLineNumberArea->setMouseTracking( true );
47 // Set up syntax highighter
48 mySyntaxHighlighter = new PyEditor_PyHighlighter( this->document() );
51 PyEditor_Settings* settings = PyEditor_Settings::settings();
53 setSettings( *settings );
56 connect( this, SIGNAL( blockCountChanged( int ) ), this, SLOT( updateLineNumberAreaWidth( int ) ) );
57 connect( this, SIGNAL( updateRequest( QRect, int ) ), this, SLOT( updateLineNumberArea( QRect, int ) ) );
58 connect( this, SIGNAL( cursorPositionChanged() ), this, SLOT( updateHighlightCurrentLine() ) );
59 connect( this, SIGNAL( cursorPositionChanged() ), this, SLOT( matchParentheses() ) );
65 PyEditor_Editor::~PyEditor_Editor()
70 \brief Get editor settings.
71 \return settings object
73 const PyEditor_Settings& PyEditor_Editor::settings() const
79 \brief Set editor settings.
80 \param settings new settings
82 void PyEditor_Editor::setSettings( const PyEditor_Settings& settings )
84 mySettings.copyFrom( settings );
88 aFont.setFamily( mySettings.font().family() );
89 aFont.setPointSize( mySettings.font().pointSize() );
93 setLineWrapMode( mySettings.textWrapping() ? QPlainTextEdit::WidgetWidth : QPlainTextEdit::NoWrap );
95 // Center the cursor on screen
96 setCenterOnScroll( mySettings.centerCursorOnScroll() );
98 // Set size white spaces
99 setTabStopWidth( mySettings.tabSize() * 10 );
101 // Update current line highlight
102 updateHighlightCurrentLine();
104 // Update line numbers area
105 updateLineNumberAreaWidth( 0 );
107 mySyntaxHighlighter->rehighlight();
108 viewport()->update();
112 Delete current selection contents.
114 void PyEditor_Editor::deleteSelected()
116 QTextCursor aCursor = textCursor();
117 if ( aCursor.hasSelection() )
118 aCursor.removeSelectedText();
119 setTextCursor( aCursor );
123 \brief Process key press event.
124 Reimplemented from QPlainTextEdit.
125 \param event key press event
127 void PyEditor_Editor::keyPressEvent( QKeyEvent* event )
129 if ( event->type() == QEvent::KeyPress )
131 int aKey = event->key();
132 Qt::KeyboardModifiers aCtrl = event->modifiers() & Qt::ControlModifier;
133 Qt::KeyboardModifiers aShift = event->modifiers() & Qt::ShiftModifier;
135 if ( aKey == Qt::Key_Tab || ( aKey == Qt::Key_Backtab || ( aKey == Qt::Key_Tab && aShift ) ) )
137 QTextCursor aCursor = textCursor();
138 aCursor.beginEditBlock();
139 tabIndentation( aKey == Qt::Key_Backtab );
140 aCursor.endEditBlock();
143 else if ( aKey == Qt::Key_Enter || aKey == Qt::Key_Return )
145 QTextCursor aCursor = textCursor();
146 aCursor.beginEditBlock();
147 if ( lineIndent() == 0 )
149 QPlainTextEdit::keyPressEvent( event );
151 aCursor.endEditBlock();
154 else if ( event == QKeySequence::MoveToStartOfLine || event == QKeySequence::SelectStartOfLine )
156 QTextCursor aCursor = this->textCursor();
157 if ( QTextLayout* aLayout = aCursor.block().layout() )
159 if ( aLayout->lineForTextPosition( aCursor.position() - aCursor.block().position() ).lineNumber() == 0 )
161 handleHome( event == QKeySequence::SelectStartOfLine );
165 else if ( ( aKey == Qt::Key_Colon || ( aKey == Qt::Key_Space && !aCtrl && !aShift ) ) &&
166 !textCursor().hasSelection() )
168 QTextCursor aCursor = textCursor();
169 aCursor.movePosition( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
171 QString aSelectedText = aCursor.selectedText();
172 int numSpaces = findFirstNonSpace( aSelectedText );
173 int amountChars = aSelectedText.size() - findFirstNonSpace( aSelectedText );
174 QString aLeadingText = aSelectedText.right( amountChars );
176 QStringList aReservedWords;
177 aReservedWords.append( "except" );
178 if ( aKey == Qt::Key_Colon )
180 aReservedWords.append( "else" );
181 aReservedWords.append( "finally" );
183 else if ( aKey == Qt::Key_Space )
185 aReservedWords.append( "elif" );
188 if ( aReservedWords.contains( aLeadingText ) )
190 QString aPreviousText = aCursor.block().previous().text();
191 int numSpacesPrevious = findFirstNonSpace( aPreviousText );
192 if ( numSpaces == numSpacesPrevious )
194 tabIndentation( true );
195 aCursor.movePosition( QTextCursor::EndOfBlock );
196 setTextCursor( aCursor );
199 QPlainTextEdit::keyPressEvent( event );
203 QPlainTextEdit::keyPressEvent( event );
209 \brief Handle resize event.
210 Reimplemented from QPlainTextEdit.
211 \param event resize event
213 void PyEditor_Editor::resizeEvent( QResizeEvent* event )
215 QPlainTextEdit::resizeEvent( event );
217 // Change size geometry of line number area
218 QRect aContentsRect = contentsRect();
219 myLineNumberArea->setGeometry( QRect( aContentsRect.left(),
221 lineNumberAreaWidth(),
222 aContentsRect.height() ) );
227 Reimplemented from QPlainTextEdit.
228 \param event paint event
230 void PyEditor_Editor::paintEvent( QPaintEvent* event )
232 QPlainTextEdit::paintEvent( event );
234 QTextBlock aBlock( firstVisibleBlock() );
235 QPointF anOffset( contentOffset() );
236 QPainter aPainter( this->viewport() );
238 int aTabSpaces = this->tabStopWidth() / 10;
240 // Visualization tab spaces
241 if ( mySettings.tabSpaceVisible() )
243 qreal aTop = blockBoundingGeometry( aBlock ).translated( anOffset ).top();
244 while ( aBlock.isValid() && aTop <= event->rect().bottom() )
246 if ( aBlock.isVisible() && blockBoundingGeometry( aBlock ).translated( anOffset ).toRect().intersects( event->rect() ) )
248 QString aText = aBlock.text();
249 if ( aText.contains( QRegExp( "\\w+" ) ) )
250 aText.remove( QRegExp( "(?!\\w+)\\s+$" ) );
254 while ( anIndex != -1 )
256 anIndex = aText.indexOf( QRegExp( QString( "^\\s{%1}" ).arg( aTabSpaces ) ), 0 );
259 aColumn = aColumn + aTabSpaces;
260 aText = aText.mid( aTabSpaces );
262 if ( aText.startsWith( ' ' ) )
264 QTextCursor aCursor( aBlock );
265 aCursor.setPosition( aBlock.position() + aColumn );
267 QRect aRect = cursorRect( aCursor );
268 aPainter.setPen( QPen( Qt::darkGray, 1, Qt::DotLine ) );
269 aPainter.drawLine( aRect.x() + 1, aRect.top(), aRect.x() + 1, aRect.bottom() );
274 aBlock = aBlock.next();
278 // Vertical edge line
279 if ( mySettings.verticalEdge() )
281 const QRect aRect = event->rect();
282 const QFont aFont = currentCharFormat().font();
283 int aNumberColumn = QFontMetrics( aFont ).averageCharWidth() * mySettings.numberColumns() + anOffset.x() + document()->documentMargin();
284 aPainter.setPen( QPen( Qt::lightGray, 1, Qt::SolidLine ) );
285 aPainter.drawLine( aNumberColumn, aRect.top(), aNumberColumn, aRect.bottom() );
290 \brief Indent and tab text.
291 \param isShift flag defines reverse tab direction
293 void PyEditor_Editor::tabIndentation( bool isShift )
295 QTextCursor aCursor = textCursor();
296 int aTabSpaces = this->tabStopWidth()/10;
298 if ( !aCursor.hasSelection() )
302 int N = aCursor.columnNumber() % aTabSpaces;
303 aCursor.insertText( QString( aTabSpaces - N, QLatin1Char( ' ' ) ) );
307 QTextBlock aCurrentBlock = document()->findBlock( aCursor.position() );
308 int anIndentPos = findFirstNonSpace( aCurrentBlock.text() );
309 aCursor.setPosition( aCurrentBlock.position() + anIndentPos );
310 setTextCursor( aCursor );
312 //if ( aCurrCursorColumnPos <= anIndentPos )
314 int aColumnPos = aCursor.columnNumber();
315 if ( aColumnPos != 0 )
317 int N = aCursor.columnNumber() % aTabSpaces;
318 if ( N == 0 ) N = aTabSpaces;
319 aCursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, N );
320 aCursor.removeSelectedText();
322 setTextCursor( aCursor );
328 indentSelection( isShift );
333 \brief Indent and tab selected text.
334 \param isShift flag defines reverse tab direction
336 void PyEditor_Editor::indentSelection( bool isShift )
338 QTextCursor aCursor = this->textCursor();
340 int aCursorStart = aCursor.selectionStart();
341 int aCursorEnd = aCursor.selectionEnd();
343 QTextBlock aStartBlock = document()->findBlock( aCursorStart );
344 QTextBlock anEndBlock = document()->findBlock( aCursorEnd - 1 ).next();
346 int aTabSpaces = this->tabStopWidth()/10;
348 for ( QTextBlock aBlock = aStartBlock; aBlock.isValid() && aBlock != anEndBlock; aBlock = aBlock.next() )
350 QString aText = aBlock.text();
351 int anIndentPos = findFirstNonSpace( aText );
352 int N = ( anIndentPos % aTabSpaces );
354 aCursor.setPosition( aBlock.position() + anIndentPos );
357 aCursor.insertText( QString( aTabSpaces - N, QLatin1Char( ' ' ) ) );
358 setTextCursor( aCursor );
362 int aColumnPos = aCursor.columnNumber();
363 if ( aColumnPos != 0 )
365 int blockN = aColumnPos % aTabSpaces;
366 if ( blockN == 0 ) blockN = aTabSpaces;
367 aCursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, blockN );
368 aCursor.removeSelectedText();
369 setTextCursor( aCursor );
374 // Reselect the selected lines
375 aCursor.setPosition( aStartBlock.position() );
376 aCursor.setPosition( anEndBlock.previous().position(), QTextCursor::KeepAnchor );
377 aCursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
378 setTextCursor( aCursor );
382 \brief Find first non white-space symbol in text.
383 \param text input text
384 \return index of first non white-space symbol
386 int PyEditor_Editor::findFirstNonSpace( const QString& text )
389 while ( i < text.size() )
391 if ( !text.at(i).isSpace() )
402 int PyEditor_Editor::lineIndent()
404 int aTabSpaces = this->tabStopWidth() / 10;
406 QTextCursor aCursor = textCursor();
407 aCursor.insertBlock();
408 setTextCursor( aCursor );
410 QTextBlock aCurrentBlock = aCursor.block();
411 if ( aCurrentBlock == document()->begin() )
414 QTextBlock aPreviousBlock = aCurrentBlock.previous();
416 QString aPreviousText;
419 if ( aPreviousBlock == document()->begin() )
421 aPreviousText = aPreviousBlock.text();
422 if ( aPreviousText.isEmpty() && aPreviousText.trimmed().isEmpty() )
427 // If the text of this block is not empty then break the loop.
428 aPreviousText = aPreviousBlock.text();
429 if ( !aPreviousText.isEmpty() && !aPreviousText.trimmed().isEmpty() )
432 aPreviousBlock = aPreviousBlock.previous();
435 int aTabIndentation = 0;
436 int anAmountIndentation = -1;
438 while ( i < aPreviousText.size() )
440 if ( !aPreviousText.at(i).isSpace() )
442 anAmountIndentation = findFirstNonSpace( aPreviousText );
452 if ( anAmountIndentation == -1 )
454 if ( aTabIndentation > 0 )
455 anAmountIndentation = aTabIndentation;
460 const QString aPreviousTrimmed = aPreviousText.trimmed();
461 if ( aPreviousTrimmed.endsWith( ":" ) )
463 anAmountIndentation += aTabSpaces;
467 if ( aPreviousTrimmed == "continue"
468 || aPreviousTrimmed == "break"
469 || aPreviousTrimmed == "pass"
470 || aPreviousTrimmed == "return"
471 || aPreviousTrimmed == "raise"
472 || aPreviousTrimmed.startsWith( "raise " )
473 || aPreviousTrimmed.startsWith( "return " ) )
474 anAmountIndentation -= aTabSpaces;
477 aCursor.insertText( QString( anAmountIndentation, QLatin1Char(' ') ) );
478 setTextCursor( aCursor );
484 \brief Set text cursor on home position.
485 \param isExtendLine \c true to keep current anchor position
487 void PyEditor_Editor::handleHome( bool isExtendLine )
489 QTextCursor aCursor = textCursor();
490 QTextCursor::MoveMode aMode = QTextCursor::MoveAnchor;
493 aMode = QTextCursor::KeepAnchor;
495 int anInitPos = aCursor.position();
496 int aBlockPos = aCursor.block().position();
498 QChar aCharacter = document()->characterAt( aBlockPos );
499 while ( aCharacter.category() == QChar::Separator_Space )
502 if ( aBlockPos == anInitPos )
504 aCharacter = document()->characterAt( aBlockPos );
507 if ( aBlockPos == anInitPos )
508 aBlockPos = aCursor.block().position();
510 aCursor.setPosition( aBlockPos, aMode );
511 setTextCursor( aCursor );
515 \brief Update current line highlighting.
517 void PyEditor_Editor::updateHighlightCurrentLine()
519 QList<QTextEdit::ExtraSelection> anExtraSelections;
520 if ( !isReadOnly() && mySettings.highlightCurrentLine() )
522 QTextEdit::ExtraSelection selection;
524 QColor lineColor = QColor( Qt::gray ).lighter( 155 );
526 selection.format.setBackground( lineColor );
527 selection.format.setProperty( QTextFormat::FullWidthSelection, QVariant( true ) );
528 selection.cursor = textCursor();
529 selection.cursor.clearSelection();
530 anExtraSelections.append( selection );
532 setExtraSelections( anExtraSelections );
536 \brief Draw linne number area.
537 \param event paint event
539 void PyEditor_Editor::lineNumberAreaPaintEvent( QPaintEvent* event )
541 QPainter aPainter( myLineNumberArea );
542 aPainter.fillRect( event->rect(), QColor( Qt::lightGray ).lighter( 125 ) );
544 QTextBlock aBlock = firstVisibleBlock();
545 int aBlockNumber = aBlock.blockNumber();
546 int aTop = (int)blockBoundingGeometry( aBlock ).translated( contentOffset() ).top();
547 int aBottom = aTop + (int)blockBoundingRect( aBlock ).height();
548 int aCurrentLine = document()->findBlock( textCursor().position() ).blockNumber();
550 QFont aFont = aPainter.font();
551 aPainter.setPen( this->palette().color( QPalette::Text ) );
553 while ( aBlock.isValid() && aTop <= event->rect().bottom() )
555 if ( aBlock.isVisible() && aBottom >= event->rect().top() )
557 if ( aBlockNumber == aCurrentLine )
559 aPainter.setPen( Qt::darkGray );
560 aFont.setBold( true );
561 aPainter.setFont( aFont );
565 aPainter.setPen( Qt::gray ) ;
566 aFont.setBold( false );
567 aPainter.setFont( aFont );
569 QString aNumber = QString::number( aBlockNumber + 1 );
570 aPainter.drawText( 0, aTop, myLineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, aNumber );
573 aBlock = aBlock.next();
575 aBottom = aTop + (int)blockBoundingRect( aBlock ).height();
581 \brief Get with of line number area.
582 \return width of line number area
584 int PyEditor_Editor::lineNumberAreaWidth()
589 int aMaximum = qMax( 1, blockCount() );
590 while ( aMaximum >= 10 )
596 if ( mySettings.lineNumberArea() )
597 aSpace += 5 + fontMetrics().width( QLatin1Char( '9' ) ) * aDigits;
603 \brief Update width of the line number area.
604 \param newBlockCount (not used)
606 void PyEditor_Editor::updateLineNumberAreaWidth( int /*newBlockCount*/ )
608 setViewportMargins( lineNumberAreaWidth(), 0, 0, 0 );
612 \brief Update line number area (when editor viewport is scrolled).
613 \param rect area being updated
614 \param dy scroll factor
616 void PyEditor_Editor::updateLineNumberArea( const QRect& rect, int dy )
619 myLineNumberArea->scroll( 0, dy );
621 myLineNumberArea->update( 0, rect.y(), myLineNumberArea->width(), rect.height() );
623 if ( rect.contains( viewport()->rect() ) )
624 updateLineNumberAreaWidth( 0 );
628 \brief Manage parentheses.
630 void PyEditor_Editor::matchParentheses()
632 PyEditor_PyHighlighter::TextBlockData* data =
633 static_cast<PyEditor_PyHighlighter::TextBlockData*>( textCursor().block().userData() );
637 QVector<PyEditor_PyHighlighter::ParenthesisInfo*> infoEntries = data->parentheses();
639 int aPos = textCursor().block().position();
641 for ( int i = 0; i < infoEntries.size(); ++i )
643 PyEditor_PyHighlighter::ParenthesisInfo* info = infoEntries.at(i);
645 int currentColumnPosition = textCursor().columnNumber();
646 if ( info->position == currentColumnPosition - 1 && isLeftBrackets( info->character ) )
648 if ( matchLeftParenthesis( textCursor().block(), i + 1, 0 ) )
649 createParenthesisSelection( aPos + info->position );
651 else if ( info->position == currentColumnPosition && isLeftBrackets( info->character ) )
655 if ( matchLeftParenthesis( textCursor().block(), i + 1, 0 ) )
656 createParenthesisSelection( aPos + info->position );
659 else if ( info->position == currentColumnPosition - 1 && isRightBrackets( info->character ) )
661 if ( matchRightParenthesis( textCursor().block(), i - 1, 0 ) )
662 createParenthesisSelection( aPos + info->position );
665 else if ( info->position == currentColumnPosition && isRightBrackets( info->character ) )
667 if ( matchRightParenthesis( textCursor().block(), i - 1, 0 ) )
668 createParenthesisSelection( aPos + info->position );
675 \brief Match left brackets.
676 \param currentBlock text block
678 \param numLeftParentheses number of left parentheses
679 \return \c true if the left match
681 bool PyEditor_Editor::matchLeftParenthesis( const QTextBlock& currentBlock,
682 int idx, int numLeftParentheses )
684 PyEditor_PyHighlighter::TextBlockData* data =
685 static_cast<PyEditor_PyHighlighter::TextBlockData*>( currentBlock.userData() );
686 QVector<PyEditor_PyHighlighter::ParenthesisInfo*> infos = data->parentheses();
688 int docPos = currentBlock.position();
689 for ( ; idx < infos.size(); ++idx )
691 PyEditor_PyHighlighter::ParenthesisInfo* info = infos.at( idx );
693 if ( isLeftBrackets( info->character ) )
695 ++numLeftParentheses;
699 if ( isRightBrackets( info->character ) && numLeftParentheses == 0 )
701 createParenthesisSelection( docPos + info->position );
705 --numLeftParentheses;
708 QTextBlock nextBlock = currentBlock.next();
709 if ( nextBlock.isValid() )
710 return matchLeftParenthesis( nextBlock, 0, numLeftParentheses );
716 \brief Match right brackets.
717 \param currentBlock text block
719 \param numRightParentheses number of right parentheses
720 \return \c true if the right match
722 bool PyEditor_Editor::matchRightParenthesis( const QTextBlock& currentBlock,
723 int idx, int numRightParentheses )
725 PyEditor_PyHighlighter::TextBlockData* data = static_cast<PyEditor_PyHighlighter::TextBlockData*>( currentBlock.userData() );
726 QVector<PyEditor_PyHighlighter::ParenthesisInfo*> parentheses = data->parentheses();
728 int docPos = currentBlock.position();
729 for ( ; idx > -1 && parentheses.size() > 0; --idx )
731 PyEditor_PyHighlighter::ParenthesisInfo* info = parentheses.at( idx );
732 if ( isRightBrackets( info->character ) )
734 ++numRightParentheses;
737 if ( isLeftBrackets( info->character ) && numRightParentheses == 0 )
739 createParenthesisSelection( docPos + info->position );
743 --numRightParentheses;
746 QTextBlock prevBlock = currentBlock.previous();
747 if ( prevBlock.isValid() )
749 PyEditor_PyHighlighter::TextBlockData* data = static_cast<PyEditor_PyHighlighter::TextBlockData*>( prevBlock.userData() );
750 QVector<PyEditor_PyHighlighter::ParenthesisInfo*> parentheses = data->parentheses();
751 return matchRightParenthesis( prevBlock, parentheses.size() - 1, numRightParentheses );
758 \brief Create brackets selection.
759 \param position cursor position
761 void PyEditor_Editor::createParenthesisSelection( int position )
763 QList<QTextEdit::ExtraSelection> selections = extraSelections();
765 QTextEdit::ExtraSelection selection;
767 QTextCharFormat format = selection.format;
768 format.setForeground( Qt::red );
769 format.setBackground( Qt::white );
770 selection.format = format;
772 QTextCursor cursor = textCursor();
773 cursor.setPosition( position );
774 cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor );
775 selection.cursor = cursor;
777 selections.append( selection );
778 setExtraSelections( selections );
782 \brief Check if symbol is a left bracket.
783 \param symbol text symbol
784 \return \c true if symbol is any left bracket
786 bool PyEditor_Editor::isLeftBrackets( QChar symbol )
788 return symbol == '(' || symbol == '{' || symbol == '[';
792 \brief Check if symbol is a right bracket.
793 \param symbol text symbol
794 \return \c true if symbol is any right bracket
796 bool PyEditor_Editor::isRightBrackets( QChar symbol )
798 return symbol == ')' || symbol == '}' || symbol == ']';
802 \brief Append new paragraph to the end of the editor's text.
803 \param text paragraph text
805 void PyEditor_Editor::append( const QString& text )
807 appendPlainText( text );
811 \brief Set text to the editor.
814 void PyEditor_Editor::setText( const QString& text )
816 setPlainText( text );
820 \brief Get current editor's content.
823 QString PyEditor_Editor::text() const
825 return toPlainText();