XML Mill  1.0.0
A GUI based XML editor with a memory.
gcplaintextedit.cpp
00001 /* Copyright (c) 2012 - 2013 by William Hallatt.
00002  *
00003  * This file forms part of "XML Mill".
00004  *
00005  * The official website for this project is <http://www.goblincoding.com> and,
00006  * although not compulsory, it would be appreciated if all works of whatever
00007  * nature using this source code (in whole or in part) include a reference to
00008  * this site.
00009  *
00010  * Should you wish to contact me for whatever reason, please do so via:
00011  *
00012  *                 <http://www.goblincoding.com/contact>
00013  *
00014  * This program is free software: you can redistribute it and/or modify it under
00015  * the terms of the GNU General Public License as published by the Free Software
00016  * Foundation, either version 3 of the License, or (at your option) any later
00017  * version.
00018  *
00019  * This program is distributed in the hope that it will be useful, but WITHOUT
00020  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
00021  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
00022  *
00023  * You should have received a copy of the GNU General Public License along with
00024  * this program (GNUGPL.txt).  If not, see
00025  *
00026  *                    <http://www.gnu.org/licenses/>
00027  */
00028 
00029 #include "gcplaintextedit.h"
00030 #include "xml/xmlsyntaxhighlighter.h"
00031 #include "utils/gcglobalspace.h"
00032 #include "utils/gcmessagespace.h"
00033 
00034 #include <QMenu>
00035 #include <QAction>
00036 #include <QDomDocument>
00037 #include <QApplication>
00038 
00039 /*--------------------------------------------------------------------------------------*/
00040 
00041 const QString OPENCOMMENT( "<!--" );
00042 const QString CLOSECOMMENT( "-->" );
00043 
00044 /*-------------------------------- NON MEMBER FUNCTIONS --------------------------------*/
00045 
00046 void removeDuplicates( QList< int >& indices )
00047 {
00048   for( int i = 0; i < indices.size(); ++i )
00049   {
00050     if( indices.count( indices.at( i ) ) > 1 )
00051     {
00052       int backup = indices.at( i );
00053 
00054       /* Remove all duplicates. */
00055       indices.removeAll( backup );
00056 
00057       /* Add one occurrence back. */
00058       indices.append( backup );
00059     }
00060   }
00061 }
00062 
00063 /*---------------------------------- MEMBER FUNCTIONS ----------------------------------*/
00064 
00065 GCPlainTextEdit::GCPlainTextEdit( QWidget* parent )
00066 : QPlainTextEdit   ( parent ),
00067   m_savedBackground(),
00068   m_savedForeground(),
00069   m_comment        ( NULL ),
00070   m_uncomment      ( NULL ),
00071   m_deleteSelection( NULL ),
00072   m_deleteEmptyRow ( NULL ),
00073   m_insertEmptyRow ( NULL ),
00074   m_cursorPositionChanged ( false ),
00075   m_cursorPositionChanging( false ),
00076   m_mouseDragEntered      ( false ),
00077   m_textEditClicked       ( false )
00078 {
00079   setAcceptDrops( false );
00080   setFont( QFont( GCGlobalSpace::FONT, GCGlobalSpace::FONTSIZE ) );
00081   setCenterOnScroll( true );
00082   setTextInteractionFlags( Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard );
00083   setContextMenuPolicy( Qt::CustomContextMenu );
00084 
00085   m_comment = new QAction( "Comment Out Selection", this );
00086   m_uncomment = new QAction( "Uncomment Selection", this );
00087   m_deleteSelection = new QAction( "Delete Selection", this );
00088   m_deleteEmptyRow = new QAction( "Delete Empty Line", this );
00089   m_insertEmptyRow = new QAction( "Insert Empty Line", this );
00090 
00091   m_deleteEmptyRow->setShortcut( Qt::Key_Delete );
00092   m_insertEmptyRow->setShortcut( Qt::Key_Return );
00093 
00094   connect( m_comment, SIGNAL( triggered() ), this, SLOT( commentOutSelection() ) );
00095   connect( m_uncomment, SIGNAL( triggered() ), this, SLOT( uncommentSelection() ) );
00096   connect( m_deleteSelection, SIGNAL( triggered() ), this, SLOT( deleteSelection() ) );
00097   connect( m_deleteEmptyRow, SIGNAL( triggered() ), this, SLOT( deleteEmptyRow() ) );
00098   connect( m_insertEmptyRow, SIGNAL( triggered() ), this, SLOT( insertEmptyRow() ) );
00099 
00100   connect( this, SIGNAL( customContextMenuRequested( const QPoint& ) ), this, SLOT( showContextMenu( const QPoint& ) ) );
00101   connect( this, SIGNAL( cursorPositionChanged() ), this, SLOT( setCursorPositionChanged() ) );
00102 
00103   /* Everything happens automagically and the text edit takes ownership. */
00104   XmlSyntaxHighlighter* highLighter = new XmlSyntaxHighlighter( document() );
00105   Q_UNUSED( highLighter )
00106   ;
00107 }
00108 
00109 /*--------------------------------------------------------------------------------------*/
00110 
00111 void GCPlainTextEdit::setContent( const QString& text )
00112 {
00113   m_cursorPositionChanging = true;
00114 
00115   /* Squeezing every ounce of performance out of the text edit...this significantly speeds
00116     up the loading of large files. */
00117   setUpdatesEnabled( false );
00118   setPlainText( text );
00119   setUpdatesEnabled( true );
00120 
00121   m_cursorPositionChanging = false;
00122 }
00123 
00124 /*--------------------------------------------------------------------------------------*/
00125 
00126 void GCPlainTextEdit::findTextRelativeToDuplicates( const QString& text, int relativePos )
00127 {
00128   /* If the user clicked on any element's representation in the text edit, then there is
00129      no need to find the text (this method is called after the tree is updated with the
00130      selected element) since we already know where it is and moving the cursor around
00131      will only make the text edit "jump" positions.  Rather highlight the text ourselves.
00132     */
00133   if( m_textEditClicked )
00134   {
00135     m_savedBackground = textCursor().blockCharFormat().background();
00136     m_savedForeground = textCursor().blockCharFormat().foreground();
00137 
00138     QTextEdit::ExtraSelection extra;
00139     extra.cursor = textCursor();
00140     extra.format.setProperty( QTextFormat::FullWidthSelection, true );
00141     extra.format.setBackground( QApplication::palette().highlight() );
00142     extra.format.setForeground( QApplication::palette().highlightedText() );
00143 
00144     QList< QTextEdit::ExtraSelection > extras;
00145     extras << extra;
00146     setExtraSelections( extras );
00147     m_textEditClicked = false;
00148   }
00149   else
00150   {
00151     /* Unset any previously set selections. */
00152     QList< QTextEdit::ExtraSelection > extras = extraSelections();
00153 
00154     for( int i = 0; i < extras.size(); ++i )
00155     {
00156       extras[ i ].format.setProperty( QTextFormat::FullWidthSelection, true );
00157       extras[ i ].format.setBackground( m_savedBackground );
00158       extras[ i ].format.setForeground( m_savedForeground );
00159     }
00160 
00161     setExtraSelections( extras );
00162 
00163     m_cursorPositionChanging = true;
00164 
00165     moveCursor( QTextCursor::Start );
00166 
00167     for( int i = 0; i <= relativePos; ++i )
00168     {
00169       find( text );
00170     }
00171 
00172     m_cursorPositionChanging = false;
00173   }
00174 }
00175 
00176 /*--------------------------------------------------------------------------------------*/
00177 
00178 void GCPlainTextEdit::clearAndReset()
00179 {
00180   m_cursorPositionChanging = true;
00181   clear();
00182   m_cursorPositionChanging = false;
00183 }
00184 
00185 /*--------------------------------------------------------------------------------------*/
00186 
00187 void GCPlainTextEdit::emitSelectedIndex()
00188 {
00189   if( !m_cursorPositionChanging )
00190   {
00191     emit selectedIndex( findIndexMatchingBlockNumber( textCursor().block() ) );
00192   }
00193 }
00194 
00195 /*--------------------------------------------------------------------------------------*/
00196 
00197 void GCPlainTextEdit::setCursorPositionChanged()
00198 {
00199   m_cursorPositionChanged = true;
00200 }
00201 
00202 /*--------------------------------------------------------------------------------------*/
00203 
00204 void GCPlainTextEdit::showContextMenu( const QPoint& point )
00205 {
00206   m_comment->setEnabled( textCursor().hasSelection() );
00207   m_uncomment->setEnabled( textCursor().hasSelection() );
00208   m_deleteSelection->setEnabled( textCursor().hasSelection() );
00209 
00210   QMenu* menu = createStandardContextMenu();
00211   menu->addSeparator();
00212   menu->addAction( m_comment );
00213   menu->addAction( m_uncomment );
00214   menu->addSeparator();
00215   menu->addAction( m_deleteSelection );
00216   menu->addAction( m_deleteEmptyRow );
00217   menu->addAction( m_insertEmptyRow );
00218   menu->exec( mapToGlobal( point ) );
00219   delete menu;
00220 }
00221 
00222 /*--------------------------------------------------------------------------------------*/
00223 
00224 void GCPlainTextEdit::commentOutSelection()
00225 {
00226   m_cursorPositionChanging = true;
00227 
00228   /* Capture the text before we make any changes. */
00229   QString comment = textCursor().selectedText();
00230 
00231   int selectionStart = textCursor().selectionStart();
00232   int selectionEnd = textCursor().selectionEnd();
00233 
00234   QTextCursor cursor = textCursor();
00235   cursor.setPosition( selectionEnd );
00236   cursor.movePosition( QTextCursor::EndOfBlock );
00237 
00238   int finalBlockNumber = cursor.blockNumber();
00239 
00240   cursor.setPosition( selectionStart );
00241   cursor.movePosition( QTextCursor::StartOfBlock );
00242 
00243   QList< int > indices;
00244   QTextBlock block = cursor.block();
00245 
00246   while( block.isValid() &&
00247          block.blockNumber() <= finalBlockNumber )
00248   {
00249     indices.append( findIndexMatchingBlockNumber( block ) );
00250     block = block.next();
00251   }
00252 
00253   cursor.setPosition( selectionStart );
00254   cursor.beginEditBlock();
00255   cursor.insertText( OPENCOMMENT );
00256   cursor.endEditBlock();
00257 
00258   cursor.setPosition( selectionEnd );
00259   cursor.movePosition( QTextCursor::EndOfBlock );
00260   cursor.beginEditBlock();
00261   cursor.insertText( CLOSECOMMENT );
00262   cursor.endEditBlock();
00263 
00264   setTextCursor( cursor );
00265 
00266   if( confirmDomNotBroken( 2 ) )
00267   {
00268     comment = comment.replace( QChar( 0x2029 ), '\n' );    // replace Unicode end of line character
00269     comment = comment.trimmed();
00270     removeDuplicates( indices );
00271     emit commentOut( indices, comment );
00272   }
00273 
00274   m_cursorPositionChanging = false;
00275 }
00276 
00277 /*--------------------------------------------------------------------------------------*/
00278 
00279 void GCPlainTextEdit::uncommentSelection()
00280 {
00281   m_cursorPositionChanging = true;
00282 
00283   int selectionStart = textCursor().selectionStart();
00284   int selectionEnd = textCursor().selectionEnd();
00285 
00286   QTextCursor cursor = textCursor();
00287   cursor.setPosition( selectionStart );
00288   cursor.movePosition( QTextCursor::StartOfBlock );
00289 
00290   cursor.setPosition( selectionEnd, QTextCursor::KeepAnchor );
00291   cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
00292 
00293   /* We need to capture this text way in the beginning before we start
00294     messing with cursor positions, etc. */
00295   QString selectedText = cursor.selectedText();
00296 
00297   cursor.beginEditBlock();
00298   cursor.removeSelectedText();
00299   selectedText.remove( OPENCOMMENT );
00300   selectedText.remove( CLOSECOMMENT );
00301   cursor.insertText( selectedText );
00302   cursor.endEditBlock();
00303 
00304   setTextCursor( cursor );
00305 
00306   m_cursorPositionChanging = false;
00307 
00308   if( confirmDomNotBroken( 2 ) )
00309   {
00310     emit manualEditAccepted();
00311 
00312     QTextCursor reselectCursor = textCursor();
00313     reselectCursor.setPosition( selectionStart );
00314     setTextCursor( reselectCursor );
00315     emitSelectedIndex();
00316   }
00317 }
00318 
00319 /*--------------------------------------------------------------------------------------*/
00320 
00321 void GCPlainTextEdit::deleteSelection()
00322 {
00323   m_cursorPositionChanging = true;
00324 
00325   textCursor().removeSelectedText();
00326 
00327   if( confirmDomNotBroken( 1 ) )
00328   {
00329     emit manualEditAccepted();
00330     emitSelectedIndex();
00331   }
00332 
00333   m_cursorPositionChanging = false;
00334 }
00335 
00336 /*--------------------------------------------------------------------------------------*/
00337 
00338 void GCPlainTextEdit::insertEmptyRow()
00339 {
00340   QTextCursor cursor = textCursor();
00341   cursor.movePosition( QTextCursor::EndOfBlock );
00342   cursor.insertBlock();
00343   setTextCursor( cursor );
00344 }
00345 
00346 /*--------------------------------------------------------------------------------------*/
00347 
00348 void GCPlainTextEdit::deleteEmptyRow()
00349 {
00350   QTextCursor cursor = textCursor();
00351   QTextBlock block = cursor.block();
00352 
00353   /* Check if the user is deleting an empty line (the only kind of deletion
00354     that is allowed). */
00355   if( block.text().remove( " " ).isEmpty() )
00356   {
00357     cursor.movePosition( QTextCursor::PreviousBlock );
00358     cursor.movePosition( QTextCursor::EndOfBlock );
00359     cursor.movePosition( QTextCursor::NextBlock, QTextCursor::KeepAnchor );
00360     cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
00361     cursor.removeSelectedText();
00362     setTextCursor( cursor );
00363   }
00364 }
00365 
00366 /*--------------------------------------------------------------------------------------*/
00367 
00368 bool GCPlainTextEdit::confirmDomNotBroken( int undoCount )
00369 {
00370   QString xmlErr( "" );
00371   int line  ( -1 );
00372   int col   ( -1 );
00373 
00374   /* Create a temporary document so that we do not mess with the contents
00375     of the tree item node map and current DOM if the new XML is broken. */
00376   QDomDocument doc;
00377 
00378   if( !doc.setContent( toPlainText(), &xmlErr, &line, &col ) )
00379   {
00380     /* Unfortunately the line number returned by the DOM doc doesn't match up with what's
00381       visible in the QTextEdit.  It seems as if it's mostly off by one line.  For now it's a
00382       fix, but will have to figure out how to make sure that we highlight the correct lines.
00383       Ultimately this finds the broken XML and highlights it in red...what a mission... */
00384     QTextBlock textBlock = document()->findBlockByLineNumber( line - 1 );
00385     QTextCursor cursor( textBlock );
00386     cursor.movePosition( QTextCursor::NextWord );
00387     cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
00388 
00389     m_savedBackground = cursor.blockCharFormat().background();
00390 
00391     QTextEdit::ExtraSelection highlight;
00392     highlight.cursor = cursor;
00393     highlight.format.setBackground( QColor( 220, 150, 220 ) );
00394     highlight.format.setProperty( QTextFormat::FullWidthSelection, true );
00395 
00396     QList< QTextEdit::ExtraSelection > extras;
00397     extras << highlight;
00398     setExtraSelections( extras );
00399     ensureCursorVisible();
00400 
00401     QString errorMsg = QString( "XML is broken - Error [%1], line [%2], column [%3].\n\n"
00402                                 "Your action will be reverted." )
00403                                 .arg( xmlErr )
00404                                 .arg( line )
00405                                 .arg( col );
00406 
00407     GCMessageSpace::showErrorMessageBox( this, errorMsg );
00408 
00409     for( int i = 0; i < undoCount; ++i )
00410     {
00411       undo();
00412     }
00413 
00414     highlight.cursor = textCursor();
00415     highlight.format.setBackground( m_savedBackground );
00416     highlight.format.setProperty( QTextFormat::FullWidthSelection, true );
00417 
00418     extras.clear();
00419     extras << highlight;
00420     setExtraSelections( extras );
00421     return false;
00422   }
00423 
00424   return true;
00425 }
00426 
00427 /*--------------------------------------------------------------------------------------*/
00428 
00429 int GCPlainTextEdit::findIndexMatchingBlockNumber( QTextBlock block )
00430 {
00431   int itemNumber = block.blockNumber();
00432   int errorCounter = 0;
00433   bool insideComment = false;
00434 
00435   while( block.isValid() &&
00436          block.blockNumber() >= 0 )
00437   {
00438     /* Check if we just entered a comment block (this is NOT wrong, remember
00439       that we are working our way back up the document, not down). */
00440     if( block.text().contains( CLOSECOMMENT ) )
00441     {
00442       errorCounter = 0;
00443       insideComment = true;
00444     }
00445 
00446     if( insideComment ||
00447         block.text().contains( "</" ) ||          // element close
00448         block.text().remove( " " ).isEmpty() ||   // empty lines
00449         ( block.text().contains( "<?" ) &&
00450           block.text().contains( "?>" ) ) )       // xml version specification
00451     {
00452       itemNumber--;
00453     }
00454 
00455     /* Check if we are about to exit a comment block. */
00456     if( block.text().contains( OPENCOMMENT ) )
00457     {
00458       /* If we are exiting but we never entered, then we need to compensate for the
00459         subtractions we've done erroneously. */
00460       if( !insideComment )
00461       {
00462         itemNumber -= errorCounter;
00463       }
00464 
00465       insideComment = false;
00466     }
00467 
00468     errorCounter++;
00469     block = block.previous();
00470   }
00471 
00472   return itemNumber;
00473 }
00474 
00475 /*--------------------------------------------------------------------------------------*/
00476 
00477 void GCPlainTextEdit::wrapText( bool wrap )
00478 {
00479   if( wrap )
00480   {
00481     setLineWrapMode( QPlainTextEdit::WidgetWidth );
00482   }
00483   else
00484   {
00485     setLineWrapMode( QPlainTextEdit::NoWrap );
00486   }
00487 }
00488 
00489 /*--------------------------------------------------------------------------------------*/
00490 
00491 void GCPlainTextEdit::keyPressEvent( QKeyEvent* e )
00492 {
00493   switch( e->key() )
00494   {
00495     case Qt::Key_Return:
00496       insertEmptyRow();
00497       break;
00498     case Qt::Key_Delete:
00499       deleteEmptyRow();
00500       break;
00501     default:
00502       QPlainTextEdit::keyPressEvent( e );
00503   }
00504 }
00505 
00506 /*--------------------------------------------------------------------------------------*/
00507 
00508 void GCPlainTextEdit::mouseMoveEvent( QMouseEvent* e )
00509 {
00510   m_mouseDragEntered = true;
00511   QPlainTextEdit::mouseMoveEvent( e );
00512 }
00513 
00514 /*--------------------------------------------------------------------------------------*/
00515 
00516 void GCPlainTextEdit::mouseReleaseEvent( QMouseEvent* e )
00517 {
00518   if( !m_mouseDragEntered &&
00519        m_cursorPositionChanged )
00520   {
00521     m_textEditClicked = true;
00522     emitSelectedIndex();
00523   }
00524 
00525   m_mouseDragEntered = false;
00526   m_cursorPositionChanged = false;
00527   QPlainTextEdit::mouseReleaseEvent( e );
00528 }
00529 
00530 /*--------------------------------------------------------------------------------------*/