Salome HOME
Merge branch 'pre/V8_3_BR'
[modules/gui.git] / tools / PyEditor / src / PyEditor_Editor.cxx
1 // Copyright (C) 2015-2016  OPEN CASCADE
2 //
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.
7 //
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.
12 //
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
16 //
17 // See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
18 //
19 // File   : PyEditor_Editor.cxx
20 // Author : Maxim GLIBIN, Open CASCADE S.A.S. (maxim.glibin@opencascade.com)
21 //
22
23 #include "PyEditor_Editor.h"
24 #include "PyEditor_LineNumberArea.h"
25 #include "PyEditor_PyHighlighter.h"
26 #include "PyEditor_Completer.h"
27 #include "PyEditor_Settings.h"
28 #include "PyEditor_Keywords.h"
29
30 #include <QMenu>
31 #include <QPainter>
32 #include <QTextBlock>
33
34 #include <iostream>
35
36 /*!
37   \class PyEditor_Editor
38   \brief Widget to show / edit Python scripts.
39 */
40
41 /*!
42   \brief Constructor.
43   \param parent parent widget
44 */
45 PyEditor_Editor::PyEditor_Editor( QWidget* parent )
46   : QPlainTextEdit( parent ),
47     myCompletionPolicy( Always )
48 {
49   myStdKeywords = new PyEditor_StandardKeywords( this );
50   myUserKeywords = new PyEditor_Keywords( this );
51   myUserKeywords->append( "print", 0, Qt::red );
52
53   // Set up line number area
54   myLineNumberArea = new PyEditor_LineNumberArea( this );
55   myLineNumberArea->setMouseTracking( true );
56
57   // Set up syntax highighter
58   mySyntaxHighlighter = new PyEditor_PyHighlighter( this->document(),
59                                                     myStdKeywords, myUserKeywords );
60
61   // Set-up settings
62   PyEditor_Settings* settings = PyEditor_Settings::settings();
63   if ( settings )
64     setSettings( *settings );
65
66   myCompleter = new PyEditor_Completer( this, myStdKeywords, myUserKeywords );
67
68   // Connect signals
69   connect( this, SIGNAL( blockCountChanged( int ) ), this, SLOT( updateLineNumberAreaWidth( int ) ) );
70   connect( this, SIGNAL( updateRequest( QRect, int ) ), this, SLOT( updateLineNumberArea( QRect, int ) ) );
71   connect( this, SIGNAL( cursorPositionChanged() ), this, SLOT( updateHighlightCurrentLine() ) );
72   connect( this, SIGNAL( cursorPositionChanged() ), this, SLOT( matchParentheses() ) );
73 }
74
75 /*!
76   \brief Destructor.
77 */
78 PyEditor_Editor::~PyEditor_Editor()
79 {
80 }
81
82 /*!
83   \brief Get editor settings.
84   \return settings object
85 */
86 const PyEditor_Settings& PyEditor_Editor::settings() const
87 {
88   return mySettings;
89 }
90
91 /*!
92   \brief Set editor settings.
93   \param settings new settings
94 */
95 void PyEditor_Editor::setSettings( const PyEditor_Settings& settings )
96 {
97   mySettings.copyFrom( settings );
98
99   // Set font size
100   QFont aFont = font();
101   aFont.setFamily( mySettings.font().family() );
102   aFont.setPointSize( mySettings.font().pointSize() );
103   setFont( aFont );
104
105   // Set line wrap mode
106   setLineWrapMode( mySettings.textWrapping() ? QPlainTextEdit::WidgetWidth : QPlainTextEdit::NoWrap );
107
108   // Center the cursor on screen
109   setCenterOnScroll( mySettings.centerCursorOnScroll() );
110
111   // Set size white spaces
112   setTabStopWidth( mySettings.tabSize() * 10 );
113
114   // Set completion policy
115   setCompletionPolicy( (CompletionPolicy)mySettings.completionPolicy() );
116
117   // Update current line highlight
118   updateHighlightCurrentLine();
119
120   // Update line numbers area
121   updateLineNumberAreaWidth( 0 );
122
123   mySyntaxHighlighter->rehighlight();
124   viewport()->update();
125 }
126
127 /*!
128   \brief Gets the current completion policy
129   \return completion policy
130 */
131 PyEditor_Editor::CompletionPolicy PyEditor_Editor::completionPolicy() const
132 {
133   return myCompletionPolicy;
134 }
135
136 /*!
137   \brief Sets the current completion policy
138   \param policy completion policy
139 */
140 void PyEditor_Editor::setCompletionPolicy( const CompletionPolicy& policy )
141 {
142   myCompletionPolicy = policy;
143 }
144
145 /*!
146   \brief Gets the all user keywords.
147   \param event key press event
148   \return keyword string list
149 */
150 QStringList PyEditor_Editor::keywords() const
151 {
152   return myUserKeywords->keywords();
153 }
154
155 /*!
156   \brief Add the user keywords.
157   \param kws keywords string list
158   \param type keywords type
159   \param color keywords color
160 */
161 void PyEditor_Editor::appendKeywords( const QStringList& kws, int type, const QColor& color )
162 {
163   myUserKeywords->append( kws, type, color );
164 }
165
166 /*!
167   \brief Remove the user keywords.
168   \param kws keywords string list
169 */
170 void PyEditor_Editor::removeKeywords( const QStringList& kws )
171 {
172   myUserKeywords->remove( kws );
173 }
174
175 /*!
176   Delete current selection contents.
177 */
178 void PyEditor_Editor::deleteSelected()
179 {
180   QTextCursor aCursor = textCursor();
181   if ( aCursor.hasSelection() )
182     aCursor.removeSelectedText();
183   setTextCursor( aCursor );
184 }
185
186 /*!
187   \brief Process key press event.
188   Reimplemented from QPlainTextEdit.
189   \param event key press event
190 */
191 void PyEditor_Editor::keyPressEvent( QKeyEvent* event )
192 {
193   if ( event->type() == QEvent::KeyPress )
194   {
195     int aKey = event->key();
196     Qt::KeyboardModifiers aCtrl = event->modifiers() & Qt::ControlModifier;
197     Qt::KeyboardModifiers aShift = event->modifiers() & Qt::ShiftModifier;
198     
199     if ( aKey == Qt::Key_Tab || ( aKey == Qt::Key_Backtab || ( aKey == Qt::Key_Tab && aShift ) ) )
200     {
201       QTextCursor aCursor = textCursor();
202       aCursor.beginEditBlock();
203       tabIndentation( aKey == Qt::Key_Backtab );
204       aCursor.endEditBlock();
205       event->accept();
206     }
207     else if ( aKey == Qt::Key_Enter || aKey == Qt::Key_Return )
208     {
209       QTextCursor aCursor = textCursor();
210       aCursor.beginEditBlock();
211       if ( lineIndent() == 0 )
212       {
213         QPlainTextEdit::keyPressEvent( event );
214       }
215       aCursor.endEditBlock();
216       event->accept();
217     }
218     else if ( aKey == Qt::Key_Space && aCtrl && !aShift &&
219               ( completionPolicy() == Manual || completionPolicy() == Always ) )
220     {
221       myCompleter->perform();
222       event->accept();
223     }
224     else if ( event == QKeySequence::MoveToStartOfLine || event == QKeySequence::SelectStartOfLine )
225     {
226       QTextCursor aCursor = this->textCursor();
227       if ( QTextLayout* aLayout = aCursor.block().layout() )
228       {
229         if ( aLayout->lineForTextPosition( aCursor.position() - aCursor.block().position() ).lineNumber() == 0 )
230         {
231           handleHome( event == QKeySequence::SelectStartOfLine );
232         }
233       }
234     }
235     else if ( ( aKey == Qt::Key_Colon || ( aKey == Qt::Key_Space && !aCtrl && !aShift ) ) &&
236               !textCursor().hasSelection() )
237     {
238       QTextCursor aCursor = textCursor();
239       aCursor.movePosition( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
240       
241       QString aSelectedText = aCursor.selectedText();
242       int numSpaces = findFirstNonSpace( aSelectedText );
243       int amountChars = aSelectedText.size() - findFirstNonSpace( aSelectedText );
244       QString aLeadingText = aSelectedText.right( amountChars );
245       
246       QStringList aReservedWords;
247       aReservedWords.append( "except" );
248       if ( aKey == Qt::Key_Colon )
249       {
250         aReservedWords.append( "else" );
251         aReservedWords.append( "finally" );
252       }
253       else if ( aKey == Qt::Key_Space )
254       {
255         aReservedWords.append( "elif" );
256       }
257       
258       if ( aReservedWords.contains( aLeadingText ) )
259       {
260         QString aPreviousText = aCursor.block().previous().text();
261         int numSpacesPrevious = findFirstNonSpace( aPreviousText );
262         if ( numSpaces == numSpacesPrevious )
263         {
264           tabIndentation( true );
265           aCursor.movePosition( QTextCursor::EndOfBlock );
266           setTextCursor( aCursor );
267         }
268       }
269       QPlainTextEdit::keyPressEvent( event );
270     }
271     else
272     {
273       QPlainTextEdit::keyPressEvent( event );
274     }
275   }
276 }
277
278 /*!
279   \brief Handle resize event.
280   Reimplemented from QPlainTextEdit.
281   \param event resize event
282 */
283 void PyEditor_Editor::resizeEvent( QResizeEvent* event )
284 {
285   QPlainTextEdit::resizeEvent( event );
286
287   // Change size geometry of line number area
288   QRect aContentsRect = contentsRect();
289   myLineNumberArea->setGeometry( QRect( aContentsRect.left(),
290                                         aContentsRect.top(),
291                                         lineNumberAreaWidth(),
292                                         aContentsRect.height() ) );
293 }
294
295 /*!
296   \brief Paint event.
297   Reimplemented from QPlainTextEdit.
298   \param event paint event
299 */
300 void PyEditor_Editor::paintEvent( QPaintEvent* event )
301 {
302   QPlainTextEdit::paintEvent( event );
303
304   QTextBlock aBlock( firstVisibleBlock() );
305   QPointF anOffset( contentOffset() );
306   QPainter aPainter( this->viewport() );
307
308   int aTabSpaces = this->tabStopWidth() / 10;
309
310   // Visualization tab spaces
311   if ( mySettings.tabSpaceVisible() )
312   {
313     qreal aTop = blockBoundingGeometry( aBlock ).translated( anOffset ).top();
314     while ( aBlock.isValid() && aTop <= event->rect().bottom() )
315     {
316       if ( aBlock.isVisible() && blockBoundingGeometry( aBlock ).translated( anOffset ).toRect().intersects( event->rect() ) )
317       {
318         QString aText = aBlock.text();
319         if ( aText.contains( QRegExp( "\\w+" ) ) )
320           aText.remove( QRegExp( "(?!\\w+)\\s+$" ) );
321         
322         int aColumn = 0;
323         int anIndex = 0;
324         while ( anIndex != -1 )
325         {
326           anIndex = aText.indexOf( QRegExp( QString( "^\\s{%1}" ).arg( aTabSpaces ) ), 0 );
327           if ( anIndex != -1 )
328           {
329             aColumn = aColumn + aTabSpaces;
330             aText = aText.mid( aTabSpaces );
331     
332             if ( aText.startsWith( ' ' ) )
333             {
334               QTextCursor aCursor( aBlock );
335               aCursor.setPosition( aBlock.position() + aColumn );
336       
337               QRect aRect = cursorRect( aCursor );
338               aPainter.setPen( QPen( Qt::darkGray, 1, Qt::DotLine ) );
339               aPainter.drawLine( aRect.x() + 1, aRect.top(), aRect.x() + 1, aRect.bottom() );
340             }
341           }
342         }
343       }
344       aBlock = aBlock.next();
345     }
346   }
347   
348   // Vertical edge line
349   if ( mySettings.verticalEdge() )
350   {
351     const QRect aRect = event->rect();
352     const QFont aFont = currentCharFormat().font();
353     int aNumberColumn =  QFontMetrics( aFont ).averageCharWidth() * mySettings.numberColumns() + anOffset.x() + document()->documentMargin();
354     aPainter.setPen( QPen( Qt::lightGray, 1, Qt::SolidLine ) );
355     aPainter.drawLine( aNumberColumn, aRect.top(), aNumberColumn, aRect.bottom() );
356   }
357 }
358
359 void PyEditor_Editor::contextMenuEvent( QContextMenuEvent* event )
360 {
361   QMenu* menu = createStandardContextMenu();
362   emit customizeMenu( menu );
363   menu->exec( event->globalPos() );
364   delete menu;
365 }
366
367 /*!
368   \brief Indent and tab text.
369   \param isShift flag defines reverse tab direction
370 */
371 void PyEditor_Editor::tabIndentation( bool isShift )
372 {
373   QTextCursor aCursor = textCursor();
374   int aTabSpaces = this->tabStopWidth()/10;
375
376   if ( !aCursor.hasSelection() )
377   {
378     if ( !isShift )
379     {
380       int N = aCursor.columnNumber() % aTabSpaces;
381       aCursor.insertText( QString( aTabSpaces - N, QLatin1Char( ' ' ) ) );
382     }
383     else
384     {
385       QTextBlock aCurrentBlock = document()->findBlock( aCursor.position() );
386       int anIndentPos = findFirstNonSpace( aCurrentBlock.text() );
387       aCursor.setPosition( aCurrentBlock.position() + anIndentPos );
388       setTextCursor( aCursor );
389       
390       //if ( aCurrCursorColumnPos <= anIndentPos )
391       //{
392       int aColumnPos = aCursor.columnNumber();
393       if ( aColumnPos != 0 )
394       {
395         int N = aCursor.columnNumber() % aTabSpaces;
396         if ( N == 0 ) N = aTabSpaces;
397         aCursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, N );
398         aCursor.removeSelectedText();
399       }
400       setTextCursor( aCursor );
401       //}
402     }
403   }
404   else
405   {
406     indentSelection( isShift );
407   }
408 }
409
410 /*!
411   \brief Indent and tab selected text.
412   \param isShift flag defines reverse tab direction
413 */
414 void PyEditor_Editor::indentSelection( bool isShift )
415 {
416   QTextCursor aCursor = this->textCursor();
417
418   int aCursorStart = aCursor.selectionStart();
419   int aCursorEnd = aCursor.selectionEnd();
420
421   QTextBlock aStartBlock = document()->findBlock( aCursorStart );
422   QTextBlock anEndBlock = document()->findBlock( aCursorEnd - 1 ).next();
423
424   int aTabSpaces = this->tabStopWidth()/10;
425
426   for ( QTextBlock aBlock = aStartBlock; aBlock.isValid() && aBlock != anEndBlock; aBlock = aBlock.next() )
427   {
428     QString aText = aBlock.text();
429     int anIndentPos = findFirstNonSpace( aText );
430     int N = ( anIndentPos % aTabSpaces );
431     
432     aCursor.setPosition( aBlock.position() + anIndentPos );
433     if ( !isShift )
434     {
435       aCursor.insertText( QString( aTabSpaces - N, QLatin1Char( ' ' ) ) );
436       setTextCursor( aCursor );
437     }
438     else
439     {
440       int aColumnPos = aCursor.columnNumber();
441       if ( aColumnPos != 0 )
442       {
443         int blockN = aColumnPos % aTabSpaces;
444         if ( blockN == 0 ) blockN = aTabSpaces;
445         aCursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, blockN );
446         aCursor.removeSelectedText();
447         setTextCursor( aCursor );
448       }
449     }
450   }
451
452   // Reselect the selected lines
453   aCursor.setPosition( aStartBlock.position() );
454   aCursor.setPosition( anEndBlock.previous().position(), QTextCursor::KeepAnchor );
455   aCursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
456   setTextCursor( aCursor );
457 }
458
459 /*!
460   \brief Find first non white-space symbol in text.
461   \param text input text
462   \return index of first non white-space symbol
463 */
464 int PyEditor_Editor::findFirstNonSpace( const QString& text )
465 {
466   int i = 0;
467   while ( i < text.size() )
468   {
469     if ( !text.at(i).isSpace() )
470       return i;
471     ++i;
472   }
473   return i;
474 }
475
476 /*!
477   \brief Indent line.
478   \return error code
479 */
480 int PyEditor_Editor::lineIndent()
481 {
482   int aTabSpaces = this->tabStopWidth() / 10;
483
484   QTextCursor aCursor = textCursor();
485   aCursor.insertBlock();
486   setTextCursor( aCursor );
487
488   QTextBlock aCurrentBlock = aCursor.block();
489   if ( aCurrentBlock == document()->begin() )
490     return 0;
491
492   QTextBlock aPreviousBlock = aCurrentBlock.previous();
493
494   QString aPreviousText;
495   forever
496   {
497     if ( aPreviousBlock == document()->begin() )
498     {
499       aPreviousText = aPreviousBlock.text();
500       if ( aPreviousText.isEmpty() && aPreviousText.trimmed().isEmpty() )
501         return -1;
502       break;
503     }
504     
505     // If the text of this block is not empty then break the loop.
506     aPreviousText = aPreviousBlock.text();
507     if ( !aPreviousText.isEmpty() && !aPreviousText.trimmed().isEmpty() )
508       break;
509     
510     aPreviousBlock = aPreviousBlock.previous();
511   }
512   
513   int aTabIndentation = 0;
514   int anAmountIndentation = -1;
515   int i = 0;
516   while ( i < aPreviousText.size() )
517   {
518     if ( !aPreviousText.at(i).isSpace() )
519     {
520       anAmountIndentation = findFirstNonSpace( aPreviousText );
521       break;
522     }
523     else
524     {
525       ++aTabIndentation;
526     }
527     ++i;
528   }
529   
530   if ( anAmountIndentation == -1 )
531   {
532     if ( aTabIndentation > 0 )
533       anAmountIndentation = aTabIndentation;
534     else
535       return 0;
536   }
537   
538   const QString aPreviousTrimmed = aPreviousText.trimmed();
539   if ( aPreviousTrimmed.endsWith( ":" ) )
540   {
541     anAmountIndentation += aTabSpaces;
542   }
543   else
544   {
545     if ( aPreviousTrimmed == "continue"
546       || aPreviousTrimmed == "break"
547       || aPreviousTrimmed == "pass"
548       || aPreviousTrimmed == "return"
549       || aPreviousTrimmed == "raise"
550       || aPreviousTrimmed.startsWith( "raise " )
551       || aPreviousTrimmed.startsWith( "return " ) )
552       anAmountIndentation -= aTabSpaces;
553   }
554   
555   aCursor.insertText( QString( anAmountIndentation, QLatin1Char(' ') ) );
556   setTextCursor( aCursor );
557   
558   return 1;
559 }
560
561 /*!
562   \brief Set text cursor on home position.
563   \param isExtendLine \c true to keep current anchor position
564 */
565 void PyEditor_Editor::handleHome( bool isExtendLine )
566 {
567   QTextCursor aCursor = textCursor();
568   QTextCursor::MoveMode aMode = QTextCursor::MoveAnchor;
569
570   if ( isExtendLine )
571     aMode = QTextCursor::KeepAnchor;
572
573   int anInitPos = aCursor.position();
574   int aBlockPos = aCursor.block().position();
575
576   QChar aCharacter = document()->characterAt( aBlockPos );
577   while ( aCharacter.category() == QChar::Separator_Space )
578   {
579     ++aBlockPos;
580     if ( aBlockPos == anInitPos )
581       break;
582     aCharacter = document()->characterAt( aBlockPos );
583   }
584   
585   if ( aBlockPos == anInitPos )
586     aBlockPos = aCursor.block().position();
587   
588   aCursor.setPosition( aBlockPos, aMode );
589   setTextCursor( aCursor );
590 }
591
592 /*!
593   \brief Update current line highlighting.
594 */
595 void PyEditor_Editor::updateHighlightCurrentLine()
596 {
597   QList<QTextEdit::ExtraSelection> anExtraSelections;
598   if ( !isReadOnly() && mySettings.highlightCurrentLine() )
599   {
600     QTextEdit::ExtraSelection selection;
601     
602     QColor lineColor = QColor( Qt::gray ).lighter( 155 );
603     
604     selection.format.setBackground( lineColor );
605     selection.format.setProperty( QTextFormat::FullWidthSelection, QVariant( true ) );
606     selection.cursor = textCursor();
607     selection.cursor.clearSelection();
608     anExtraSelections.append( selection );
609   }
610   setExtraSelections( anExtraSelections );
611 }
612
613 /*!
614   \brief Draw linne number area.
615   \param event paint event
616 */
617 void PyEditor_Editor::lineNumberAreaPaintEvent( QPaintEvent* event )
618 {
619   QPainter aPainter( myLineNumberArea );
620   aPainter.fillRect( event->rect(), QColor( Qt::lightGray ).lighter( 125 ) );
621
622   QTextBlock aBlock = firstVisibleBlock();
623   int aBlockNumber = aBlock.blockNumber();
624   int aTop = (int)blockBoundingGeometry( aBlock ).translated( contentOffset() ).top();
625   int aBottom = aTop + (int)blockBoundingRect( aBlock ).height();
626   int aCurrentLine = document()->findBlock( textCursor().position() ).blockNumber();
627
628   QFont aFont = aPainter.font();
629   aPainter.setPen( this->palette().color( QPalette::Text ) );
630
631   while ( aBlock.isValid() && aTop <= event->rect().bottom() )
632   {
633     if ( aBlock.isVisible() && aBottom >= event->rect().top() )
634     {
635       if ( aBlockNumber == aCurrentLine )
636       {
637         aPainter.setPen( Qt::darkGray );
638         aFont.setBold( true );
639         aPainter.setFont( aFont );
640       }
641       else
642       {
643         aPainter.setPen( Qt::gray ) ;
644         aFont.setBold( false );
645         aPainter.setFont( aFont );
646       }
647       QString aNumber = QString::number( aBlockNumber + 1 );
648       aPainter.drawText( 0, aTop, myLineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, aNumber );
649     }
650
651     aBlock = aBlock.next();
652     aTop = aBottom;
653     aBottom = aTop + (int)blockBoundingRect( aBlock ).height();
654     ++aBlockNumber;
655   }
656 }
657
658 /*!
659   \brief Get with of line number area.
660   \return width of line number area
661 */
662 int PyEditor_Editor::lineNumberAreaWidth()
663 {
664   int aSpace = 0;
665
666   int aDigits = 1;
667   int aMaximum = qMax( 1, blockCount() );
668   while ( aMaximum >= 10 )
669   {
670     aMaximum /= 10;
671     ++aDigits;
672   }
673
674   if ( mySettings.lineNumberArea() )
675     aSpace += 5 + fontMetrics().width( QLatin1Char( '9' ) ) * aDigits;
676   
677   return aSpace;
678 }
679
680 /*!
681   \brief Update width of the line number area.
682   \param newBlockCount (not used)
683 */
684 void PyEditor_Editor::updateLineNumberAreaWidth( int /*newBlockCount*/ )
685 {
686   setViewportMargins( lineNumberAreaWidth(), 0, 0, 0 );
687 }
688
689 /*!
690   \brief Update line number area (when editor viewport is scrolled).
691   \param rect area being updated
692   \param dy scroll factor
693 */
694 void PyEditor_Editor::updateLineNumberArea( const QRect& rect, int dy )
695 {
696   if ( dy )
697     myLineNumberArea->scroll( 0, dy );
698   else
699     myLineNumberArea->update( 0, rect.y(), myLineNumberArea->width(), rect.height() );
700
701   if ( rect.contains( viewport()->rect() ) )
702     updateLineNumberAreaWidth( 0 );
703 }
704
705 /*!
706   \brief Manage parentheses.
707 */
708 void PyEditor_Editor::matchParentheses()
709 {
710   PyEditor_PyHighlighter::TextBlockData* data =
711     static_cast<PyEditor_PyHighlighter::TextBlockData*>( textCursor().block().userData() );
712
713   if ( data )
714   {
715     QVector<PyEditor_PyHighlighter::ParenthesisInfo*> infoEntries = data->parentheses();
716     
717     int aPos = textCursor().block().position();
718     bool ignore = false;
719     for ( int i = 0; i < infoEntries.size(); ++i )
720     {
721       PyEditor_PyHighlighter::ParenthesisInfo* info = infoEntries.at(i);
722       
723       int currentColumnPosition = textCursor().columnNumber();
724       if ( info->position == currentColumnPosition - 1 && isLeftBrackets( info->character ) )
725       {
726         if ( matchLeftParenthesis( textCursor().block(), i + 1, 0 ) )
727           createParenthesisSelection( aPos + info->position );
728       }
729       else if ( info->position == currentColumnPosition && isLeftBrackets( info->character ) )
730       {
731         if ( !ignore )
732         {
733           if ( matchLeftParenthesis( textCursor().block(), i + 1, 0 ) )
734             createParenthesisSelection( aPos + info->position );
735         }
736       }
737       else if ( info->position == currentColumnPosition - 1 && isRightBrackets( info->character ) )
738       {
739         if ( matchRightParenthesis( textCursor().block(), i - 1, 0 ) )
740           createParenthesisSelection( aPos + info->position );
741         ignore = true;
742       }
743       else if ( info->position == currentColumnPosition && isRightBrackets( info->character ) )
744       {
745         if ( matchRightParenthesis( textCursor().block(), i - 1, 0 ) )
746           createParenthesisSelection( aPos + info->position );
747       }
748     }
749   }
750 }
751
752 /*!
753   \brief Match left brackets.
754   \param currentBlock text block
755   \param idx index
756   \param numLeftParentheses number of left parentheses
757   \return \c true if the left match
758 */
759 bool PyEditor_Editor::matchLeftParenthesis( const QTextBlock& currentBlock,
760                                             int idx, int numLeftParentheses )
761 {
762   PyEditor_PyHighlighter::TextBlockData* data =
763     static_cast<PyEditor_PyHighlighter::TextBlockData*>( currentBlock.userData() );
764   QVector<PyEditor_PyHighlighter::ParenthesisInfo*> infos = data->parentheses();
765
766   int docPos = currentBlock.position();
767   for ( ; idx < infos.size(); ++idx )
768   {
769     PyEditor_PyHighlighter::ParenthesisInfo* info = infos.at( idx );
770
771     if ( isLeftBrackets( info->character ) )
772     {
773       ++numLeftParentheses;
774       continue;
775     }
776
777     if ( isRightBrackets( info->character ) && numLeftParentheses == 0 )
778     {
779       createParenthesisSelection( docPos + info->position );
780       return true;
781     }
782     else
783       --numLeftParentheses;
784   }
785
786   QTextBlock nextBlock = currentBlock.next();
787   if ( nextBlock.isValid() )
788     return matchLeftParenthesis( nextBlock, 0, numLeftParentheses );
789
790   return false;
791 }
792
793 /*!
794   \brief Match right brackets.
795   \param currentBlock text block
796   \param idx index
797   \param numRightParentheses number of right parentheses
798   \return \c true if the right match
799 */
800 bool PyEditor_Editor::matchRightParenthesis( const QTextBlock& currentBlock,
801                                              int idx, int numRightParentheses )
802 {
803   PyEditor_PyHighlighter::TextBlockData* data = static_cast<PyEditor_PyHighlighter::TextBlockData*>( currentBlock.userData() );
804   QVector<PyEditor_PyHighlighter::ParenthesisInfo*> parentheses = data->parentheses();
805
806   int docPos = currentBlock.position();
807   for ( ; idx > -1 && parentheses.size() > 0; --idx )
808   {
809     PyEditor_PyHighlighter::ParenthesisInfo* info = parentheses.at( idx );
810     if ( isRightBrackets( info->character ) )
811     {
812       ++numRightParentheses;
813       continue;
814     }
815     if ( isLeftBrackets( info->character ) && numRightParentheses == 0 )
816     {
817       createParenthesisSelection( docPos + info->position );
818       return true;
819     }
820     else
821       --numRightParentheses;
822   }
823
824   QTextBlock prevBlock = currentBlock.previous();
825   if ( prevBlock.isValid() )
826   {
827     PyEditor_PyHighlighter::TextBlockData* data = static_cast<PyEditor_PyHighlighter::TextBlockData*>( prevBlock.userData() );
828     QVector<PyEditor_PyHighlighter::ParenthesisInfo*> parentheses = data->parentheses();
829     return matchRightParenthesis( prevBlock, parentheses.size() - 1, numRightParentheses );
830   }
831
832   return false;
833 }
834
835 /*!
836   \brief Create brackets selection.
837   \param position cursor position
838 */
839 void PyEditor_Editor::createParenthesisSelection( int position )
840 {
841   QList<QTextEdit::ExtraSelection> selections = extraSelections();
842
843   QTextEdit::ExtraSelection selection;
844
845   QTextCharFormat format = selection.format;
846   format.setForeground( Qt::red );
847   format.setBackground( Qt::white );
848   selection.format = format;
849
850   QTextCursor cursor = textCursor();
851   cursor.setPosition( position );
852   cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor );
853   selection.cursor = cursor;
854
855   selections.append( selection );
856   setExtraSelections( selections );
857 }
858
859 /*!
860   \brief Check if symbol is a left bracket.
861   \param symbol text symbol
862   \return \c true if symbol is any left bracket
863 */
864 bool PyEditor_Editor::isLeftBrackets( QChar symbol )
865 {
866   return symbol == '(' || symbol == '{' || symbol == '[';
867 }
868
869 /*!
870   \brief Check if symbol is a right bracket.
871   \param symbol text symbol
872   \return \c true if symbol is any right bracket
873 */
874 bool PyEditor_Editor::isRightBrackets( QChar symbol )
875 {
876   return symbol == ')' || symbol == '}' || symbol == ']';
877 }
878
879 /*!
880   \brief Append new paragraph to the end of the editor's text.
881   \param text paragraph text
882 */
883 void PyEditor_Editor::append( const QString& text )
884 {
885   appendPlainText( text );
886 }
887
888 /*!
889   \brief Set text to the editor.
890   \param text new text
891 */
892 void PyEditor_Editor::setText( const QString& text )
893 {
894   setPlainText( text );
895 }
896
897 /*!
898   \brief Get current editor's content.
899   \return current text
900 */
901 QString PyEditor_Editor::text() const
902 {
903   return toPlainText();
904 }
905
906 /*!
907   \brief Get user keywords dictionary.
908   \return keywords dictionary
909 */
910 PyEditor_Keywords* PyEditor_Editor::userKeywords() const
911 {
912   return myUserKeywords;
913 }
914
915 /*!
916   \brief Get standard keywords dictionary.
917   \return keywords dictionary
918 */
919 PyEditor_Keywords* PyEditor_Editor::standardKeywords() const
920 {
921   return myStdKeywords;
922 }