![]() |
XML Mill
1.0.0
A GUI based XML editor with a memory.
|
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 /*--------------------------------------------------------------------------------------*/