![]() |
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 "gcmainwindow.h" 00030 #include "ui_gcmainwindow.h" 00031 #include "db/gcdatabaseinterface.h" 00032 #include "db/gcdbsessionmanager.h" 00033 #include "forms/gcadditemsform.h" 00034 #include "forms/gcremoveitemsform.h" 00035 #include "forms/gchelpdialog.h" 00036 #include "forms/gcsearchform.h" 00037 #include "forms/gcaddsnippetsform.h" 00038 #include "forms/gcrestorefilesform.h" 00039 #include "utils/gctreewidgetitem.h" 00040 #include "utils/gccombobox.h" 00041 #include "utils/gcmessagespace.h" 00042 #include "utils/gcglobalspace.h" 00043 00044 #include <QDesktopServices> 00045 #include <QSignalMapper> 00046 #include <QDomDocument> 00047 #include <QTextBlock> 00048 #include <QMessageBox> 00049 #include <QFileDialog> 00050 #include <QTextStream> 00051 #include <QTextCursor> 00052 #include <QComboBox> 00053 #include <QTimer> 00054 #include <QLabel> 00055 #include <QUrl> 00056 #include <QCloseEvent> 00057 #include <QFont> 00058 #include <QScrollBar> 00059 #include <QMovie> 00060 #include <QSettings> 00061 00062 /*--------------------------------------------------------------------------------------*/ 00063 00064 const QString EMPTY( "---" ); 00065 const QString LEFTRIGHTBRACKETS( "\\[|\\]" ); 00066 00067 const qint64 DOMWARNING( 262144 ); // 0.25MB or ~7 500 lines 00068 const qint64 DOMLIMIT ( 524288 ); // 0.5MB or ~15 000 lines 00069 00070 const int ATTRIBUTECOLUMN = 0; 00071 const int VALUESCOLUMN = 1; 00072 00073 /*--------------------------------- MEMBER FUNCTIONS ----------------------------------*/ 00074 00075 GCMainWindow::GCMainWindow( QWidget* parent ) 00076 : QMainWindow ( parent ), 00077 ui ( new Ui::GCMainWindow ), 00078 m_signalMapper ( new QSignalMapper( this ) ), 00079 m_activeAttribute ( NULL ), 00080 m_currentCombo ( NULL ), 00081 m_saveTimer ( NULL ), 00082 m_activeProfileLabel ( NULL ), 00083 m_progressLabel ( NULL ), 00084 m_spinner ( NULL ), 00085 m_currentXMLFileName ( "" ), 00086 m_activeAttributeName ( "" ), 00087 m_wasTreeItemActivated ( false ), 00088 m_newAttributeAdded ( false ), 00089 m_busyImporting ( false ), 00090 m_fileContentsChanged ( false ), 00091 m_comboBoxes () 00092 { 00093 ui->setupUi( this ); 00094 ui->showEmptyProfileHelpButton->setVisible( false ); 00095 ui->tableWidget->setFont( QFont( GCGlobalSpace::FONT, GCGlobalSpace::FONTSIZE ) ); 00096 ui->tableWidget->horizontalHeader()->setFont( QFont( GCGlobalSpace::FONT, GCGlobalSpace::FONTSIZE ) ); 00097 00098 /* XML File related. */ 00099 connect( ui->actionNew, SIGNAL( triggered() ), this, SLOT( newXMLFile() ) ); 00100 connect( ui->actionOpen, SIGNAL( triggered() ), this, SLOT( openXMLFile() ) ); 00101 connect( ui->actionSave, SIGNAL( triggered() ), this, SLOT( saveXMLFile() ) ); 00102 connect( ui->actionSaveAs, SIGNAL( triggered() ), this, SLOT( saveXMLFileAs() ) ); 00103 connect( ui->actionCloseFile, SIGNAL( triggered() ), this, SLOT( closeXMLFile() ) ); 00104 00105 /* Build/Edit XML. */ 00106 connect( ui->addChildElementButton, SIGNAL( clicked() ), this, SLOT( addElementToDocument() ) ); 00107 connect( ui->addSnippetButton, SIGNAL( clicked() ), this, SLOT( addSnippetToDocument() ) ); 00108 00109 /* Various other actions. */ 00110 connect( ui->actionExit, SIGNAL( triggered() ), this, SLOT( close() ) ); 00111 connect( ui->actionFind, SIGNAL( triggered() ), this, SLOT( searchDocument() ) ); 00112 connect( ui->actionForgetPreferences, SIGNAL( triggered() ), this, SLOT( forgetMessagePreferences() ) ); 00113 connect( ui->actionHelpContents, SIGNAL( triggered() ), this, SLOT( showMainHelp() ) ); 00114 connect( ui->actionVisitOfficialSite, SIGNAL( triggered() ), this, SLOT( goToSite() ) ); 00115 connect( ui->expandAllCheckBox, SIGNAL( clicked( bool ) ), this, SLOT( collapseOrExpandTreeWidget( bool ) ) ); 00116 connect( ui->commentLineEdit, SIGNAL( textEdited( QString ) ), this, SLOT( updateComment( QString ) ) ); 00117 connect( ui->actionUseDarkTheme, SIGNAL( triggered( bool ) ), this, SLOT( useDarkTheme( bool ) ) ); 00118 00119 connect( ui->wrapTextCheckBox, SIGNAL( clicked( bool ) ), ui->dockWidgetTextEdit, SLOT( wrapText( bool ) ) ); 00120 connect( ui->dockWidgetTextEdit, SIGNAL( selectedIndex( int ) ), ui->treeWidget, SLOT( setCurrentItemFromIndex( int ) ) ); 00121 connect( ui->dockWidgetTextEdit, SIGNAL( commentOut( const QList< int >&, const QString& ) ), this, SLOT( commentOut( const QList< int >&, const QString& ) ) ); 00122 connect( ui->dockWidgetTextEdit, SIGNAL( manualEditAccepted() ), this, SLOT( rebuild() ) ); 00123 00124 /* Help related. */ 00125 connect( ui->actionShowHelpButtons, SIGNAL( triggered( bool ) ), this, SLOT( setShowHelpButtons( bool ) ) ); 00126 connect( ui->showEmptyProfileHelpButton, SIGNAL( clicked() ), this, SLOT( showEmptyProfileHelp() ) ); 00127 connect( ui->showAddElementHelpButton, SIGNAL( clicked() ), this, SLOT( showElementHelp() ) ); 00128 00129 /* Everything tree widget related. */ 00130 connect( ui->treeWidget, SIGNAL( gcCurrentItemSelected( GCTreeWidgetItem*, int ) ), this, SLOT( elementSelected( GCTreeWidgetItem*, int ) ) ); 00131 connect( ui->treeWidget, SIGNAL( gcCurrentItemChanged( GCTreeWidgetItem*, int ) ), this, SLOT( elementChanged( GCTreeWidgetItem*, int ) ) ); 00132 connect( ui->treeWidget, SIGNAL( collapsed( QModelIndex ) ), this, SLOT( uncheckExpandAll() ) ); 00133 connect( ui->actionShowTreeElementsVerbose, SIGNAL( triggered( bool ) ), this, SLOT( setShowTreeItemsVerbose( bool ) ) ); 00134 00135 /* Everything table widget related. */ 00136 connect( ui->tableWidget, SIGNAL( itemClicked( QTableWidgetItem* ) ), this, SLOT( attributeSelected( QTableWidgetItem* ) ) ); 00137 connect( ui->tableWidget, SIGNAL( itemChanged( QTableWidgetItem* ) ), this, SLOT( attributeChanged( QTableWidgetItem* ) ) ); 00138 00139 /* Database related. */ 00140 connect( ui->actionAddItems, SIGNAL( triggered() ), this, SLOT( addItemsToDB() ) ); 00141 connect( ui->actionRemoveItems, SIGNAL( triggered() ), this, SLOT( removeItemsFromDB() ) ); 00142 connect( ui->actionImportXMLToDatabase, SIGNAL( triggered() ), this, SLOT( importXMLFromFile() ) ); 00143 connect( ui->actionSwitchSessionDatabase, SIGNAL( triggered() ), this, SLOT( switchActiveDatabase() ) ); 00144 connect( ui->actionAddNewDatabase, SIGNAL( triggered() ), this, SLOT( addNewDatabase() ) ); 00145 connect( ui->actionAddExistingDatabase, SIGNAL( triggered() ), this, SLOT( addExistingDatabase() ) ); 00146 connect( ui->actionRemoveDatabase, SIGNAL( triggered() ), this, SLOT( removeDatabase() ) ); 00147 00148 connect( m_signalMapper, SIGNAL( mapped( QWidget* ) ), this, SLOT( setCurrentComboBox( QWidget* ) ) ); 00149 00150 readSettings(); 00151 00152 /* Wait for the event loop to be initialised before calling this function. */ 00153 QTimer::singleShot( 0, this, SLOT( initialise() ) ); 00154 } 00155 00156 /*--------------------------------------------------------------------------------------*/ 00157 00158 GCMainWindow::~GCMainWindow() 00159 { 00160 delete ui; 00161 } 00162 00163 /*--------------------------------------------------------------------------------------*/ 00164 00165 void GCMainWindow::closeEvent( QCloseEvent* event ) 00166 { 00167 if( m_fileContentsChanged ) 00168 { 00169 QMessageBox::StandardButtons accept = QMessageBox::question( this, 00170 "Save File?", 00171 "Save changes before closing?", 00172 QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, 00173 QMessageBox::Yes ); 00174 00175 if( accept == QMessageBox::Yes ) 00176 { 00177 saveXMLFile(); 00178 } 00179 else if( accept == QMessageBox::Cancel ) 00180 { 00181 event->ignore(); 00182 return; 00183 } 00184 } 00185 00186 deleteTempFile(); 00187 saveSettings(); 00188 QMainWindow::closeEvent( event ); 00189 } 00190 00191 /*--------------------------------------------------------------------------------------*/ 00192 00193 void GCMainWindow::initialise() 00194 { 00195 /* Initialise the database interface and retrieve the list of database names (this will 00196 include the path references to the ".db" files). */ 00197 if( !GCDataBaseInterface::instance()->isInitialised() ) 00198 { 00199 GCMessageSpace::showErrorMessageBox( this, GCDataBaseInterface::instance()->lastError() ); 00200 this->close(); 00201 } 00202 00203 /* If the interface was successfully initialised, prompt the user to choose a database 00204 connection for this session. */ 00205 GCDBSessionManager* manager = createDBSessionManager(); 00206 manager->selectActiveDatabase(); 00207 QDialog::DialogCode result = static_cast< QDialog::DialogCode >( manager->result() ); 00208 00209 if( result == QDialog::Rejected ) 00210 { 00211 this->close(); // also deletes manager ("this" is parent) 00212 } 00213 00214 queryRestoreFiles(); 00215 } 00216 00217 /*--------------------------------------------------------------------------------------*/ 00218 00219 void GCMainWindow::elementChanged( GCTreeWidgetItem* item, int column ) 00220 { 00221 if( !ui->treeWidget->empty() ) 00222 { 00223 elementSelected( item, column ); 00224 setTextEditContent( item ); 00225 } 00226 else 00227 { 00228 resetDOM(); 00229 } 00230 } 00231 00232 /*--------------------------------------------------------------------------------------*/ 00233 00234 void GCMainWindow::elementSelected( GCTreeWidgetItem* item, int column ) 00235 { 00236 Q_UNUSED( column ); 00237 00238 if( item ) 00239 { 00240 /* This flag is set to prevent the functionality in "attributeChanged" (which is triggered 00241 by the population of the table widget) from being executed until this function exits. */ 00242 m_wasTreeItemActivated = true; 00243 00244 resetTableWidget(); 00245 00246 QDomElement element = item->element(); // shallow copy 00247 QString elementName = item->name(); 00248 QStringList attributeNames = GCDataBaseInterface::instance()->attributes( elementName ); 00249 00250 /* Add all the associated attribute names to the first column of the table widget, 00251 create and populate combo boxes with the attributes' known values and insert the 00252 combo boxes into the second column of the table widget. Finally, insert an "empty" 00253 row so that the user may add additional attributes and values to the current element. */ 00254 for( int i = 0; i < attributeNames.count(); ++i ) 00255 { 00256 GCComboBox* attributeCombo = new GCComboBox; 00257 QTableWidgetItem* label = new QTableWidgetItem( attributeNames.at( i ) ); 00258 label->setFlags( label->flags() | Qt::ItemIsUserCheckable ); 00259 00260 if( item->attributeIncluded( attributeNames.at( i ) ) ) 00261 { 00262 label->setCheckState( Qt::Checked ); 00263 attributeCombo->setEnabled( true ); 00264 } 00265 else 00266 { 00267 label->setCheckState( Qt::Unchecked ); 00268 attributeCombo->setEnabled( false ); 00269 } 00270 00271 ui->tableWidget->setRowCount( i + 1 ); 00272 ui->tableWidget->setItem( i, 0, label ); 00273 00274 attributeCombo->addItems( GCDataBaseInterface::instance()->attributeValues( elementName, attributeNames.at( i ) ) ); 00275 attributeCombo->setEditable( true ); 00276 00277 /* If we are still in the process of building the document, the attribute value will 00278 be empty since it has never been set before. For this particular case, calling 00279 "findText" will result in a null pointer exception being thrown so we need to 00280 cater for this possibility here. */ 00281 QString attributeValue = element.attribute( attributeNames.at( i ) ); 00282 00283 if( !attributeValue.isEmpty() ) 00284 { 00285 attributeCombo->setCurrentIndex( attributeCombo->findText( attributeValue ) ); 00286 } 00287 else 00288 { 00289 attributeCombo->setCurrentIndex( -1 ); 00290 } 00291 00292 /* Attempting the connection before we've set the current index above causes the 00293 "attributeValueChanged" slot to be called prematurely, resulting in a segmentation 00294 fault due to value conflicts/missing values (in short, we can't do the connect 00295 before we set the current index above). */ 00296 connect( attributeCombo, SIGNAL( currentIndexChanged( QString ) ), this, SLOT( attributeValueChanged( QString ) ) ); 00297 00298 ui->tableWidget->setCellWidget( i, 1, attributeCombo ); 00299 m_comboBoxes.insert( attributeCombo, i ); 00300 00301 /* This will point the current combo box member to the combo that's been activated 00302 in the table widget (used in "attributeValueChanged" to obtain the row number the 00303 combo box appears in in the table widget, etc, etc). */ 00304 connect( attributeCombo, SIGNAL( activated( int ) ), m_signalMapper, SLOT( map() ) ); 00305 m_signalMapper->setMapping( attributeCombo, attributeCombo ); 00306 } 00307 00308 /* Add the "empty" row as described above. */ 00309 insertEmptyTableRow(); 00310 00311 /* Populate the "add child element" combo box with the known first level children of the 00312 current highlighted element (highlighted in the tree widget, of course). */ 00313 ui->addElementComboBox->clear(); 00314 ui->addElementComboBox->addItems( GCDataBaseInterface::instance()->children( elementName ) ); 00315 00316 /* The following will be used to allow the user to add an element of the current type to 00317 its parent (this should improve the user experience as they do not have to explicitly 00318 navigate back up to a parent when adding multiple items of the same type). We also don't 00319 want the user to add a document root element to itself by accident. */ 00320 if( !ui->treeWidget->matchesRootName( elementName ) ) 00321 { 00322 ui->addElementComboBox->addItem( QString( "[%1]" ).arg( elementName ) ); 00323 } 00324 00325 toggleAddElementWidgets(); 00326 highlightTextElement( item ); 00327 00328 /* The user must not be allowed to add an entire document as a "snippet". */ 00329 if( ui->treeWidget->matchesRootName( ui->addElementComboBox->currentText() ) ) 00330 { 00331 ui->addSnippetButton->setEnabled( false ); 00332 ui->addChildElementButton->setText( "Add Root" ); 00333 } 00334 else 00335 { 00336 ui->addSnippetButton->setEnabled( true ); 00337 ui->addChildElementButton->setText( "Add Child" ); 00338 } 00339 00340 ui->commentLineEdit->setText( ui->treeWidget->activeCommentValue() ); 00341 00342 /* Unset flag. */ 00343 m_wasTreeItemActivated = false; 00344 } 00345 else if( ui->treeWidget->empty() ) 00346 { 00347 resetDOM(); 00348 } 00349 } 00350 00351 /*--------------------------------------------------------------------------------------*/ 00352 00353 void GCMainWindow::attributeChanged( QTableWidgetItem* tableItem ) 00354 { 00355 /* Don't execute the logic if a tree widget item's activation is triggering 00356 a re-population of the table widget (which results in this slot being called). */ 00357 if( !m_wasTreeItemActivated && !m_newAttributeAdded ) 00358 { 00359 /* Also don't allow for empty attribute names. */ 00360 if( tableItem->text().isEmpty() ) 00361 { 00362 tableItem->setText( m_activeAttributeName ); 00363 return; 00364 } 00365 00366 /* When the check state of a table widget item changes, the "itemChanged" signal is emitted 00367 before the "itemClicked" one and this messes up the logic that follows completely. Since 00368 I depend on knowing which attribute is currently active, I needed to insert the following 00369 odd little check (the other alternative is probably to subclass QTableWidgetItem but I'm 00370 worried about the added complications). */ 00371 if( m_activeAttribute != tableItem ) 00372 { 00373 m_activeAttributeName = tableItem->text(); 00374 } 00375 00376 /* All attribute name changes will be assumed to be additions, removing an attribute 00377 with a specific name has to be done explicitly. */ 00378 GCTreeWidgetItem* treeItem = ui->treeWidget->gcCurrentItem(); 00379 00380 /* See if an existing attribute's name changed or if a new attribute was added. */ 00381 if( tableItem->text() != m_activeAttributeName ) 00382 { 00383 /* Add the new attribute's name to the current element's list of associated attributes. */ 00384 if( GCDataBaseInterface::instance()->updateElementAttributes( treeItem->name(), QStringList( tableItem->text() ) ) ) 00385 { 00386 /* Is this a name change? */ 00387 if( m_activeAttributeName != EMPTY ) 00388 { 00389 treeItem->excludeAttribute( m_activeAttributeName ); 00390 00391 /* Retrieve the list of values associated with the previous name and insert it against the new name. */ 00392 QStringList attributeValues = GCDataBaseInterface::instance()->attributeValues( treeItem->name(), 00393 m_activeAttributeName ); 00394 00395 if( !GCDataBaseInterface::instance()->updateAttributeValues( treeItem->name(), tableItem->text(), attributeValues ) ) 00396 { 00397 GCMessageSpace::showErrorMessageBox( this, GCDataBaseInterface::instance()->lastError() ); 00398 } 00399 } 00400 else 00401 { 00402 /* If this is an entirely new attribute, insert an "empty" row so that the user may add 00403 even more attributes if he/she wishes to do so. */ 00404 m_newAttributeAdded = true; 00405 tableItem->setFlags( tableItem->flags() | Qt::ItemIsUserCheckable ); 00406 tableItem->setCheckState( Qt::Checked ); 00407 insertEmptyTableRow(); 00408 m_newAttributeAdded = false; 00409 } 00410 } 00411 else 00412 { 00413 GCMessageSpace::showErrorMessageBox( this, GCDataBaseInterface::instance()->lastError() ); 00414 } 00415 } 00416 00417 m_activeAttributeName = tableItem->text(); 00418 00419 /* Is this attribute included or excluded? */ 00420 GCComboBox* attributeValueCombo = dynamic_cast< GCComboBox* >( ui->tableWidget->cellWidget( tableItem->row(), VALUESCOLUMN ) ); 00421 00422 if( tableItem->checkState() == Qt::Checked ) 00423 { 00424 attributeValueCombo->setEnabled( true ); 00425 treeItem->includeAttribute( m_activeAttributeName, attributeValueCombo->currentText() ); 00426 } 00427 else 00428 { 00429 attributeValueCombo->setEnabled( false ); 00430 treeItem->excludeAttribute( m_activeAttributeName ); 00431 } 00432 00433 setTextEditContent( treeItem ); 00434 } 00435 } 00436 00437 /*--------------------------------------------------------------------------------------*/ 00438 00439 void GCMainWindow::attributeSelected( QTableWidgetItem* tableItem ) 00440 { 00441 m_activeAttribute = tableItem; 00442 m_activeAttributeName = tableItem->text(); 00443 } 00444 00445 /*--------------------------------------------------------------------------------------*/ 00446 00447 void GCMainWindow::attributeValueChanged( const QString& value ) 00448 { 00449 /* Don't execute the logic if a tree widget item's activation is triggering 00450 a re-population of the table widget (which results in this slot being called). */ 00451 if( !m_wasTreeItemActivated && !value.isEmpty() ) 00452 { 00453 GCTreeWidgetItem* treeItem = ui->treeWidget->gcCurrentItem(); 00454 QString currentAttributeName = ui->tableWidget->item( m_comboBoxes.value( m_currentCombo ), ATTRIBUTECOLUMN )->text(); 00455 00456 /* If we don't know about this value, we need to add it to the DB. */ 00457 QStringList attributeValues = GCDataBaseInterface::instance()->attributeValues( treeItem->name(), currentAttributeName ); 00458 00459 if( !attributeValues.contains( value ) ) 00460 { 00461 if( !GCDataBaseInterface::instance()->updateAttributeValues( treeItem->name(), 00462 currentAttributeName, 00463 QStringList( value ) ) ) 00464 { 00465 GCMessageSpace::showErrorMessageBox( this, GCDataBaseInterface::instance()->lastError() ); 00466 } 00467 } 00468 00469 treeItem->includeAttribute( currentAttributeName, value ); 00470 setTextEditContent( treeItem ); 00471 } 00472 } 00473 00474 /*--------------------------------------------------------------------------------------*/ 00475 00476 void GCMainWindow::setCurrentComboBox( QWidget* combo ) 00477 { 00478 m_currentCombo = combo; 00479 } 00480 00481 /*--------------------------------------------------------------------------------------*/ 00482 00483 bool GCMainWindow::openXMLFile() 00484 { 00485 /* Can't open a file if there is no DB profile to describe it. */ 00486 querySetActiveSession( QString( "No active profile set, please set one for this session." ) ); 00487 00488 if( !queryResetDOM( "Save document before continuing?" ) ) 00489 { 00490 return false; 00491 } 00492 00493 /* Start off where the user finished last. */ 00494 QString fileName = QFileDialog::getOpenFileName( this, "Open File", GCGlobalSpace::lastUserSelectedDirectory(), "XML Files (*.*)" ); 00495 00496 /* If the user cancelled, we don't want to continue. */ 00497 if( fileName.isEmpty() ) 00498 { 00499 return false; 00500 } 00501 00502 /* Note to future self: although the user would have explicitly saved (or not saved) the file 00503 by the time this functionality is encountered, we only reset the document once we have a new, 00504 active file to work with since users are fickle and may still change their minds. In other 00505 words, do NOT move this code to before "getOpenFileName" as previously considered! */ 00506 resetDOM(); 00507 m_currentXMLFileName = ""; 00508 00509 QFile file( fileName ); 00510 00511 if( !file.open( QIODevice::ReadOnly | QIODevice::Text ) ) 00512 { 00513 QString errorMsg = QString( "Failed to open file \"%1\": [%2]" ) 00514 .arg( fileName ) 00515 .arg( file.errorString() ); 00516 GCMessageSpace::showErrorMessageBox( this, errorMsg ); 00517 return false; 00518 } 00519 00520 QTextStream inStream( &file ); 00521 QString fileContent( inStream.readAll() ); 00522 qint64 fileSize = file.size(); 00523 file.close(); 00524 00525 /* This application isn't optimised for dealing with very large XML files (the entire point is that 00526 this suite should provide the functionality necessary for the manual manipulation of, e.g. XML config 00527 files normally set up by hand via copy and paste exercises), if this file is too large to be handled 00528 comfortably, we need to let the user know and also make sure that we don't try to set the DOM content 00529 as text in the QTextEdit (QTextEdit is optimised for paragraphs). */ 00530 if( fileSize > DOMWARNING && 00531 fileSize < DOMLIMIT ) 00532 { 00533 QMessageBox::warning( this, 00534 "Large file!", 00535 "The file you just opened is pretty large. Response times may be slow." ); 00536 } 00537 else if( fileSize > DOMLIMIT ) 00538 { 00539 QMessageBox::critical( this, 00540 "Very large file!", 00541 "This file is too large to edit manually!" ); 00542 00543 return false; 00544 } 00545 00546 QString xmlErr( "" ); 00547 int line ( -1 ); 00548 int col ( -1 ); 00549 00550 if( !ui->treeWidget->setContent( fileContent, &xmlErr, &line, &col ) ) 00551 { 00552 QString errorMsg = QString( "XML is broken - Error [%1], line [%2], column [%3]" ) 00553 .arg( xmlErr ) 00554 .arg( line ) 00555 .arg( col ); 00556 GCMessageSpace::showErrorMessageBox( this, errorMsg ); 00557 resetDOM(); 00558 return false; 00559 } 00560 00561 m_currentXMLFileName = fileName; 00562 m_fileContentsChanged = false; // at first load, nothing has changed 00563 00564 if( !m_busyImporting ) 00565 { 00566 /* If the user is opening an XML file of a kind that isn't supported by the current active DB, 00567 we need to warn him/her of this fact and provide them with a couple of options. */ 00568 if( !ui->treeWidget->documentCompatible() ) 00569 { 00570 bool accepted = GCMessageSpace::userAccepted( "QueryImportXML", 00571 "Import document?", 00572 "Encountered unknown relationships - import differences to active profile?", 00573 GCMessageSpace::YesNo, 00574 GCMessageSpace::No, 00575 GCMessageSpace::Question ); 00576 00577 if( accepted ) 00578 { 00579 QTimer timer; 00580 timer.singleShot( 1000, this, SLOT( createSpinner() ) ); 00581 qApp->processEvents( QEventLoop::ExcludeUserInputEvents ); 00582 00583 if( !ui->treeWidget->batchProcessSuccess() ) 00584 { 00585 GCMessageSpace::showErrorMessageBox( this, GCDataBaseInterface::instance()->lastError() ); 00586 } 00587 else 00588 { 00589 qApp->processEvents( QEventLoop::ExcludeUserInputEvents ); 00590 processDOMDoc(); 00591 } 00592 00593 deleteSpinner(); 00594 } 00595 else 00596 { 00597 /* If the user decided to rather abort the import, reset everything. */ 00598 resetDOM(); 00599 m_currentXMLFileName = ""; 00600 } 00601 } 00602 else 00603 { 00604 /* If the user selected a database that knows of this particular XML profile, 00605 simply process the document. */ 00606 processDOMDoc(); 00607 } 00608 } 00609 00610 /* Save whatever directory the user ended up in. */ 00611 QFileInfo fileInfo( fileName ); 00612 QString finalDirectory = fileInfo.dir().path(); 00613 GCGlobalSpace::setLastUserSelectedDirectory( finalDirectory ); 00614 return true; 00615 } 00616 00617 /*--------------------------------------------------------------------------------------*/ 00618 00619 void GCMainWindow::newXMLFile() 00620 { 00621 if( queryResetDOM( "Save document before continuing?" ) ) 00622 { 00623 resetDOM(); 00624 m_currentXMLFileName = ""; 00625 ui->actionCloseFile->setEnabled( true ); 00626 ui->actionSaveAs->setEnabled( true ); 00627 ui->actionSave->setEnabled( true ); 00628 } 00629 } 00630 00631 /*--------------------------------------------------------------------------------------*/ 00632 00633 bool GCMainWindow::saveXMLFile() 00634 { 00635 if( m_currentXMLFileName.isEmpty() ) 00636 { 00637 return saveXMLFileAs(); 00638 } 00639 else 00640 { 00641 QFile file( m_currentXMLFileName ); 00642 00643 if( !file.open( QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text ) ) 00644 { 00645 QString errMsg = QString( "Failed to save file \"%1\": [%2]." ) 00646 .arg( m_currentXMLFileName ) 00647 .arg( file.errorString() ); 00648 GCMessageSpace::showErrorMessageBox( this, errMsg ); 00649 return false; 00650 } 00651 else 00652 { 00653 QTextStream outStream( &file ); 00654 outStream << ui->treeWidget->toString(); 00655 file.close(); 00656 00657 m_fileContentsChanged = false; 00658 deleteTempFile(); 00659 startSaveTimer(); 00660 } 00661 } 00662 00663 return true; 00664 } 00665 00666 /*--------------------------------------------------------------------------------------*/ 00667 00668 bool GCMainWindow::saveXMLFileAs() 00669 { 00670 QString file = QFileDialog::getSaveFileName( this, "Save As", GCGlobalSpace::lastUserSelectedDirectory(), "XML Files (*.*)" ); 00671 00672 /* If the user clicked "OK". */ 00673 if( !file.isEmpty() ) 00674 { 00675 m_currentXMLFileName = file; 00676 00677 /* Save the last visited directory. */ 00678 QFileInfo fileInfo( m_currentXMLFileName ); 00679 QString finalDirectory = fileInfo.dir().path(); 00680 GCGlobalSpace::setLastUserSelectedDirectory( finalDirectory ); 00681 00682 return saveXMLFile(); 00683 } 00684 else 00685 { 00686 /* Return false when the file save operation is cancelled so that "queryResetDom" does 00687 not inadvertently caused a file to be reset by accident when the user changes his/her mind. */ 00688 return false; 00689 } 00690 } 00691 00692 /*--------------------------------------------------------------------------------------*/ 00693 00694 void GCMainWindow::closeXMLFile() 00695 { 00696 if( queryResetDOM( "Save document before continuing?" ) ) 00697 { 00698 resetDOM(); 00699 } 00700 } 00701 00702 /*--------------------------------------------------------------------------------------*/ 00703 00704 void GCMainWindow::importXMLFromFile() 00705 { 00706 /* This flag is used in "openXMLFile" to distinguish between an explicit import 00707 and a simple file opening operation. */ 00708 m_busyImporting = true; 00709 ui->treeWidget->setVisible( false ); 00710 00711 if( openXMLFile() ) 00712 { 00713 if( importXMLToDatabase() ) 00714 { 00715 if( m_progressLabel ) 00716 { 00717 m_progressLabel->hide(); 00718 } 00719 00720 QMessageBox::StandardButtons accept = QMessageBox::question( this, 00721 "Edit file", 00722 "Also open file for editing?", 00723 QMessageBox::Yes | QMessageBox::No, 00724 QMessageBox::Yes ); 00725 00726 if( accept == QMessageBox::Yes ) 00727 { 00728 QTimer timer; 00729 00730 if( m_progressLabel ) 00731 { 00732 timer.singleShot( 1000, m_progressLabel, SLOT( show() ) ); 00733 } 00734 00735 qApp->processEvents( QEventLoop::ExcludeUserInputEvents ); 00736 processDOMDoc(); 00737 } 00738 else 00739 { 00740 /* DOM was set in the process of opening the XML file and loading its content. If the user 00741 doesn't want to work with the file that was imported, we need to reset it here. */ 00742 resetDOM(); 00743 m_currentXMLFileName = ""; 00744 } 00745 } 00746 } 00747 00748 ui->treeWidget->setVisible( true ); 00749 m_busyImporting = false; 00750 } 00751 00752 /*--------------------------------------------------------------------------------------*/ 00753 00754 bool GCMainWindow::importXMLToDatabase() 00755 { 00756 createSpinner(); 00757 qApp->processEvents( QEventLoop::ExcludeUserInputEvents ); 00758 00759 if( !ui->treeWidget->batchProcessSuccess() ) 00760 { 00761 GCMessageSpace::showErrorMessageBox( this, GCDataBaseInterface::instance()->lastError() ); 00762 deleteSpinner(); 00763 return false; 00764 } 00765 00766 deleteSpinner(); 00767 return true; 00768 } 00769 00770 /*--------------------------------------------------------------------------------------*/ 00771 00772 void GCMainWindow::addNewDatabase() 00773 { 00774 GCDBSessionManager* manager = createDBSessionManager(); 00775 00776 /* If we have an active DOM document, we need to pass the name of the root 00777 element through to the DB session manager which uses it to determine whether 00778 or not a user is on the verge of messing something up... */ 00779 if( ui->treeWidget->empty() ) 00780 { 00781 manager->addNewDatabase(); 00782 } 00783 else 00784 { 00785 manager->addNewDatabase( ui->treeWidget->rootName() ); 00786 } 00787 00788 delete manager; 00789 00790 /* Ensure that the user sets a database as active for this session. */ 00791 querySetActiveSession( QString( "No active profile set, please set one for this session." ) ); 00792 } 00793 00794 /*--------------------------------------------------------------------------------------*/ 00795 00796 void GCMainWindow::addExistingDatabase() 00797 { 00798 GCDBSessionManager* manager = createDBSessionManager(); 00799 00800 /* If we have an active DOM document, we need to pass the name of the root 00801 element through to the DB session manager which uses it to determine whether 00802 or not a user is on the verge of messing something up... */ 00803 if( ui->treeWidget->empty() ) 00804 { 00805 manager->addExistingDatabase(); 00806 } 00807 else 00808 { 00809 manager->addExistingDatabase( ui->treeWidget->rootName() ); 00810 } 00811 00812 delete manager; 00813 00814 /* Ensure that the user sets a database as active for this session. */ 00815 querySetActiveSession( QString( "No active profile set, please set one for this session." ) ); 00816 } 00817 00818 /*--------------------------------------------------------------------------------------*/ 00819 00820 void GCMainWindow::removeDatabase() 00821 { 00822 GCDBSessionManager* manager = createDBSessionManager(); 00823 00824 /* If we have an active DOM document, we need to pass the name of the root 00825 element through to the DB session manager which uses it to determine whether 00826 or not a user is on the verge of messing something up... */ 00827 if( ui->treeWidget->empty() ) 00828 { 00829 manager->removeDatabase(); 00830 } 00831 else 00832 { 00833 manager->removeDatabase( ui->treeWidget->rootName() ); 00834 } 00835 00836 delete manager; 00837 00838 /* If the user removed the active DB for this session, we need to know 00839 what he/she intends to replace it with. */ 00840 querySetActiveSession( QString( "The active profile has been removed, please select another." ) ); 00841 } 00842 00843 /*--------------------------------------------------------------------------------------*/ 00844 00845 void GCMainWindow::switchActiveDatabase() 00846 { 00847 GCDBSessionManager* manager = createDBSessionManager(); 00848 00849 /* If we have an active DOM document, we need to pass the name of the root 00850 element through to the DB session manager which uses it to determine whether 00851 or not a user is on the verge of messing something up... */ 00852 if( ui->treeWidget->empty() ) 00853 { 00854 manager->selectActiveDatabase(); 00855 } 00856 else 00857 { 00858 manager->selectActiveDatabase( ui->treeWidget->rootName() ); 00859 } 00860 00861 delete manager; 00862 } 00863 00864 /*--------------------------------------------------------------------------------------*/ 00865 00866 void GCMainWindow::activeDatabaseChanged( QString dbName ) 00867 { 00868 if( ui->treeWidget->empty() ) 00869 { 00870 resetDOM(); 00871 } 00872 00873 /* If the user set an empty database, prompt to populate it. This message must 00874 always be shown (i.e. we don't have to show the custom dialog box that provides 00875 the \"Don't show this again\" option). */ 00876 if( GCDataBaseInterface::instance()->isProfileEmpty() ) 00877 { 00878 QMessageBox::StandardButton accepted = QMessageBox::warning( this, 00879 "Empty Profile", 00880 "Empty profile selected. Import XML from file?", 00881 QMessageBox::Yes | QMessageBox::No, 00882 QMessageBox::Yes ); 00883 00884 if( accepted == QMessageBox::Yes ) 00885 { 00886 importXMLFromFile(); 00887 } 00888 else 00889 { 00890 addItemsToDB(); 00891 } 00892 } 00893 00894 if( !m_activeProfileLabel ) 00895 { 00896 m_activeProfileLabel = new QLabel( QString( "Active Profile: %1" ).arg( dbName ) ); 00897 statusBar()->addWidget( m_activeProfileLabel ); 00898 } 00899 else 00900 { 00901 m_activeProfileLabel->setText( QString( "Active Profile: %1" ).arg( dbName ) ); 00902 } 00903 } 00904 00905 /*--------------------------------------------------------------------------------------*/ 00906 00907 void GCMainWindow::addElementToDocument() 00908 { 00909 QString elementName = ui->addElementComboBox->currentText(); 00910 bool treeWasEmpty = ui->treeWidget->empty(); 00911 bool addToParent = false; 00912 00913 /* If the user selected the <element> option, we add a new sibling element 00914 of the same name as the current element to the current element's parent. */ 00915 if( elementName.contains( QRegExp( LEFTRIGHTBRACKETS ) ) ) 00916 { 00917 elementName = elementName.remove( QRegExp( LEFTRIGHTBRACKETS ) ); 00918 addToParent = true; 00919 } 00920 00921 /* There is probably no chance of this ever happening, but defensive programming FTW! */ 00922 if( !elementName.isEmpty() ) 00923 { 00924 /* Update the tree widget. */ 00925 ui->treeWidget->addItem( elementName, addToParent ); 00926 00927 GCTreeWidgetItem* treeItem = ui->treeWidget->gcCurrentItem(); 00928 treeItem->setFlags( treeItem->flags() | Qt::ItemIsEditable ); 00929 00930 /* If the user starts creating a DOM document without having explicitly asked for 00931 a new file to be created, do it automatically (we can't call "newXMLFile here" since 00932 it resets the DOM document). */ 00933 if( treeWasEmpty ) 00934 { 00935 m_currentXMLFileName = ""; 00936 ui->actionCloseFile->setEnabled( true ); 00937 ui->actionSave->setEnabled( true ); 00938 ui->actionSaveAs->setEnabled( true ); 00939 ui->addSnippetButton->setEnabled( true ); 00940 ui->addChildElementButton->setText( "Add Child" ); 00941 } 00942 00943 /* Add all the known attributes associated with this element name to the new element. */ 00944 QStringList attributes = GCDataBaseInterface::instance()->attributes( elementName ); 00945 00946 for( int i = 0; i < attributes.size(); ++i ) 00947 { 00948 treeItem->element().setAttribute( attributes.at( i ), QString( "" ) ); 00949 } 00950 00951 setTextEditContent( treeItem ); 00952 elementSelected( treeItem, 0 ); 00953 } 00954 } 00955 00956 /*--------------------------------------------------------------------------------------*/ 00957 00958 void GCMainWindow::addSnippetToDocument() 00959 { 00960 QString elementName = ui->addElementComboBox->currentText(); 00961 00962 /* Check if we're inserting snippets as children, or as siblings. */ 00963 if( elementName.contains( QRegExp( LEFTRIGHTBRACKETS ) ) ) 00964 { 00965 /* Qt::WA_DeleteOnClose flag set. */ 00966 GCAddSnippetsForm* dialog = new GCAddSnippetsForm( elementName.remove( QRegExp( LEFTRIGHTBRACKETS ) ), 00967 ui->treeWidget->gcCurrentItem()->gcParent(), 00968 this ); 00969 connect( dialog, SIGNAL( snippetAdded( GCTreeWidgetItem*, QDomElement ) ), this, SLOT( insertSnippet( GCTreeWidgetItem*, QDomElement ) ) ); 00970 dialog->exec(); 00971 } 00972 else 00973 { 00974 /* Qt::WA_DeleteOnClose flag set. */ 00975 GCAddSnippetsForm* dialog = new GCAddSnippetsForm( elementName, 00976 ui->treeWidget->gcCurrentItem(), 00977 this ); 00978 connect( dialog, SIGNAL( snippetAdded( GCTreeWidgetItem*, QDomElement ) ), this, SLOT( insertSnippet( GCTreeWidgetItem*, QDomElement ) ) ); 00979 dialog->exec(); 00980 } 00981 } 00982 00983 /*--------------------------------------------------------------------------------------*/ 00984 00985 void GCMainWindow::insertSnippet( GCTreeWidgetItem* treeItem, QDomElement element ) 00986 { 00987 ui->treeWidget->appendSnippet( treeItem, element ); 00988 ui->treeWidget->expandAll(); 00989 setTextEditContent(); 00990 itemFound( treeItem->gcChild( treeItem->childCount() - 1 ) ); 00991 m_fileContentsChanged = true; 00992 } 00993 00994 /*--------------------------------------------------------------------------------------*/ 00995 00996 void GCMainWindow::removeItemsFromDB() 00997 { 00998 if( !GCDataBaseInterface::instance()->hasActiveSession() ) 00999 { 01000 GCMessageSpace::showErrorMessageBox( this, "No active profile." ); 01001 return; 01002 } 01003 01004 if( GCDataBaseInterface::instance()->isProfileEmpty() ) 01005 { 01006 QMessageBox::warning( this, 01007 "Profile Empty", 01008 "Active profile empty, nothing to remove." ); 01009 return; 01010 } 01011 01012 if( !ui->treeWidget->empty() ) 01013 { 01014 GCMessageSpace::showErrorMessageBox( this, "For practical reasons, items cannot be removed when documents are open.\n" 01015 "Please \"Save\" and/or \"Close\" the current document before returning back here." ); 01016 return; 01017 } 01018 01019 /* Delete on close flag set (no clean-up needed). */ 01020 GCRemoveItemsForm* dialog = new GCRemoveItemsForm( this ); 01021 dialog->exec(); 01022 } 01023 01024 /*--------------------------------------------------------------------------------------*/ 01025 01026 void GCMainWindow::addItemsToDB() 01027 { 01028 if( !GCDataBaseInterface::instance()->hasActiveSession() ) 01029 { 01030 GCMessageSpace::showErrorMessageBox( this, "No active profile, please set one (Edit->Switch Profile)." ); 01031 return; 01032 } 01033 01034 bool profileWasEmpty = GCDataBaseInterface::instance()->isProfileEmpty(); 01035 01036 /* Delete on close flag set (no clean-up needed). */ 01037 GCAddItemsForm* form = new GCAddItemsForm( this ); 01038 form->exec(); 01039 01040 /* If the active profile has just been populated with elements for the first time, 01041 make sure that we set the newly added root elements to the dropdown. */ 01042 if( profileWasEmpty ) 01043 { 01044 ui->addElementComboBox->addItems( GCDataBaseInterface::instance()->knownRootElements() ); 01045 toggleAddElementWidgets(); 01046 } 01047 } 01048 01049 /*--------------------------------------------------------------------------------------*/ 01050 01051 void GCMainWindow::searchDocument() 01052 { 01053 /* Delete on close flag set (no clean-up needed). */ 01054 GCSearchForm* form = new GCSearchForm( ui->treeWidget->allTreeWidgetItems(), ui->dockWidgetTextEdit, this ); 01055 connect( form, SIGNAL( foundItem( GCTreeWidgetItem* ) ), this, SLOT( itemFound( GCTreeWidgetItem* ) ) ); 01056 form->exec(); 01057 } 01058 01059 /*--------------------------------------------------------------------------------------*/ 01060 01061 void GCMainWindow::itemFound( GCTreeWidgetItem* item ) 01062 { 01063 ui->treeWidget->expandAll(); 01064 ui->treeWidget->setCurrentItem( item ); 01065 elementSelected( item, 0 ); 01066 } 01067 01068 /*--------------------------------------------------------------------------------------*/ 01069 01070 void GCMainWindow::commentOut( const QList< int >& indices, const QString& comment ) 01071 { 01072 m_fileContentsChanged = true; 01073 ui->treeWidget->replaceItemsWithComment( indices, comment ); 01074 } 01075 01076 /*--------------------------------------------------------------------------------------*/ 01077 01078 void GCMainWindow::rebuild() 01079 { 01080 /* No need to check if setContent is a success. If this function gets called, the document 01081 content is already valid XML. The reason I reset the text edit's content is due to the Qt 01082 XML parser adding attributes in alphabetical order. What this means is that the elements 01083 in the tree widget have all their attributes aligned alphabetically, and this may or may 01084 not add up with what is in the text edit, hence the reset (this way we ensure that the order 01085 of the attributes in the text edit matches exactly that of the tree widget's elements). */ 01086 ui->treeWidget->setContent( ui->dockWidgetTextEdit->toPlainText() ); 01087 ui->dockWidgetTextEdit->setContent( ui->treeWidget->toString() ); 01088 01089 ui->treeWidget->expandAll(); 01090 m_fileContentsChanged = true; 01091 } 01092 01093 /*--------------------------------------------------------------------------------------*/ 01094 01095 void GCMainWindow::updateComment( const QString& comment ) 01096 { 01097 ui->treeWidget->setActiveCommentValue( comment ); 01098 } 01099 01100 /*--------------------------------------------------------------------------------------*/ 01101 01102 void GCMainWindow::collapseOrExpandTreeWidget( bool checked ) 01103 { 01104 if( checked ) 01105 { 01106 ui->treeWidget->expandAll(); 01107 } 01108 else 01109 { 01110 ui->treeWidget->collapseAll(); 01111 } 01112 } 01113 01114 /*--------------------------------------------------------------------------------------*/ 01115 01116 void GCMainWindow::uncheckExpandAll() 01117 { 01118 ui->expandAllCheckBox->setChecked( false ); 01119 } 01120 01121 /*--------------------------------------------------------------------------------------*/ 01122 01123 void GCMainWindow::forgetMessagePreferences() 01124 { 01125 GCMessageSpace::forgetAllPreferences(); 01126 } 01127 01128 /*--------------------------------------------------------------------------------------*/ 01129 01130 void GCMainWindow::createSpinner() 01131 { 01132 /* Clean-up must be handled in the calling function, but just in case. */ 01133 if( m_spinner || m_progressLabel ) 01134 { 01135 deleteSpinner(); 01136 } 01137 01138 m_progressLabel = new QLabel( this, Qt::Popup ); 01139 m_progressLabel->move( window()->frameGeometry().topLeft() + window()->rect().center() - m_progressLabel->rect().center() ); 01140 01141 m_spinner = new QMovie( ":/resources/spinner.gif" ); 01142 m_spinner->start(); 01143 01144 m_progressLabel->setMovie( m_spinner ); 01145 m_progressLabel->show(); 01146 } 01147 01148 /*--------------------------------------------------------------------------------------*/ 01149 01150 void GCMainWindow::deleteSpinner() 01151 { 01152 if( m_spinner ) 01153 { 01154 delete m_spinner; 01155 m_spinner = NULL; 01156 } 01157 01158 if( m_progressLabel ) 01159 { 01160 delete m_progressLabel; 01161 m_progressLabel = NULL; 01162 } 01163 } 01164 01165 /*--------------------------------------------------------------------------------------*/ 01166 01167 void GCMainWindow::resetDOM() 01168 { 01169 ui->treeWidget->clearAndReset(); 01170 ui->dockWidgetTextEdit->clearAndReset(); 01171 01172 resetTableWidget(); 01173 01174 ui->addElementComboBox->clear(); 01175 ui->addElementComboBox->addItems( GCDataBaseInterface::instance()->knownRootElements() ); 01176 toggleAddElementWidgets(); 01177 01178 ui->addSnippetButton->setEnabled( false ); 01179 ui->addChildElementButton->setText( "Add Root" ); 01180 01181 m_currentCombo = NULL; 01182 m_activeAttributeName = ""; 01183 m_fileContentsChanged = false; 01184 01185 /* The timer will be reactivated as soon as work starts again on a legitimate 01186 document and the user saves it for the first time. */ 01187 if( m_saveTimer ) 01188 { 01189 m_saveTimer->stop(); 01190 } 01191 } 01192 01193 /*--------------------------------------------------------------------------------------*/ 01194 01195 bool GCMainWindow::queryResetDOM( const QString& resetReason ) 01196 { 01197 /* There are a number of places and opportunities for "resetDOM" to be called, 01198 if there is an active document, check if it's content has changed since the 01199 last time it had changed and make sure we don't accidentally delete anything. */ 01200 if( m_fileContentsChanged ) 01201 { 01202 QMessageBox::StandardButtons accept = QMessageBox::question( this, 01203 "Save file?", 01204 resetReason, 01205 QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, 01206 QMessageBox::Yes ); 01207 01208 if( accept == QMessageBox::Yes ) 01209 { 01210 return saveXMLFile(); 01211 } 01212 else if( accept == QMessageBox::No ) 01213 { 01214 m_fileContentsChanged = false; 01215 } 01216 else 01217 { 01218 return false; 01219 } 01220 } 01221 01222 return true; 01223 } 01224 01225 /*--------------------------------------------------------------------------------------*/ 01226 01227 void GCMainWindow::showEmptyProfileHelp() 01228 { 01229 QMessageBox::information( this, 01230 "Empty Profile", 01231 "The active profile is empty. You can either import XML from file " 01232 "via \"Edit -> Import XML to Profile\" or you can populate the " 01233 "profile from scratch via \"Edit -> Edit Profile -> Add Items\"." ); 01234 } 01235 01236 /*--------------------------------------------------------------------------------------*/ 01237 01238 void GCMainWindow::showElementHelp() 01239 { 01240 QMessageBox::information( this, 01241 "Adding Elements and Snippets", 01242 "If the document is still empty, your first element will be the root element.\n\n" 01243 "New elements are added as children of the element selected in the element tree.\n\n" 01244 "\"Empty\" duplicate siblings are added by selecting the element name in the drop down " 01245 "that matches that of the element selected in the element tree (these names will be " 01246 "bracketed by \"[]\").\n\n" 01247 "\"Snippets\" are fully formed XML segments consisting of the entire element hierarchy " 01248 "with the element selected in the drop down combo box as base. Selecting this option will " 01249 "provide you with the opportunity to provide default values for the snippet's attributes." ); 01250 } 01251 01252 /*--------------------------------------------------------------------------------------*/ 01253 01254 void GCMainWindow::showMainHelp() 01255 { 01256 QFile file( ":/resources/help/Help.txt" ); 01257 01258 if( !file.open( QIODevice::ReadOnly | QIODevice::Text ) ) 01259 { 01260 GCMessageSpace::showErrorMessageBox( this, QString( "Failed to open \"Help\" file: [%1]" ).arg( file.errorString() ) ); 01261 } 01262 else 01263 { 01264 QTextStream stream( &file ); 01265 QString fileContent = stream.readAll(); 01266 file.close(); 01267 01268 /* Qt::WA_DeleteOnClose flag set...no cleanup required. */ 01269 GCHelpDialog* dialog = new GCHelpDialog( fileContent, this ); 01270 dialog->show(); 01271 } 01272 } 01273 01274 /*--------------------------------------------------------------------------------------*/ 01275 01276 void GCMainWindow::goToSite() 01277 { 01278 QDesktopServices::openUrl( QUrl( "http://goblincoding.com" ) ); 01279 } 01280 01281 /*--------------------------------------------------------------------------------------*/ 01282 01283 void GCMainWindow::useDarkTheme( bool dark ) 01284 { 01285 if( dark ) 01286 { 01287 QFile file( ":resources/StyleSheet.txt" ); 01288 file.open( QIODevice::ReadOnly | QIODevice::Text ); 01289 QTextStream stream( &file ); 01290 01291 qApp->setStyleSheet( stream.readAll() ); 01292 file.close(); 01293 } 01294 else 01295 { 01296 qApp->setStyleSheet( QString() ); 01297 } 01298 } 01299 01300 /*--------------------------------------------------------------------------------------*/ 01301 01302 void GCMainWindow::setShowHelpButtons( bool show ) 01303 { 01304 GCGlobalSpace::setShowHelpButtons( show ); 01305 ui->showAddElementHelpButton->setVisible( show ); 01306 } 01307 01308 /*--------------------------------------------------------------------------------------*/ 01309 01310 void GCMainWindow::setShowTreeItemsVerbose( bool verbose ) 01311 { 01312 GCGlobalSpace::setShowTreeItemsVerbose( verbose ); 01313 ui->treeWidget->setShowTreeItemsVerbose( verbose ); 01314 } 01315 01316 /*--------------------------------------------------------------------------------------*/ 01317 01318 GCDBSessionManager* GCMainWindow::createDBSessionManager() 01319 { 01320 /* Clean-up is the responsibility of the calling function. */ 01321 GCDBSessionManager* manager = new GCDBSessionManager( this ); 01322 connect( manager, SIGNAL( reset() ), this, SLOT( resetDOM() ) ); 01323 connect( manager, SIGNAL( activeDatabaseChanged( QString ) ), this, SLOT( activeDatabaseChanged( QString ) ) ); 01324 return manager; 01325 } 01326 01327 /*--------------------------------------------------------------------------------------*/ 01328 01329 void GCMainWindow::processDOMDoc() 01330 { 01331 ui->treeWidget->rebuildTreeWidget(); // also deletes current items 01332 resetTableWidget(); 01333 01334 ui->addSnippetButton->setEnabled( true ); 01335 ui->addChildElementButton->setText( "Add Child" ); 01336 01337 /* Enable file save options. */ 01338 ui->actionCloseFile->setEnabled( true ); 01339 ui->actionSave->setEnabled( true ); 01340 ui->actionSaveAs->setEnabled( true ); 01341 01342 /* Display the DOM content in the text edit. */ 01343 setTextEditContent(); 01344 01345 /* Generally-speaking, we want the file contents changed flag to be set whenever 01346 the text edit content is set (this is done, not surprisingly, in "setTextEditContent" 01347 above). However, whenever a DOM document is processed for the first time, nothing 01348 is changed in it, so to avoid the annoying "Save File" queries when nothing has 01349 been done yet, we unset the flag here. */ 01350 m_fileContentsChanged = false; 01351 01352 collapseOrExpandTreeWidget( ui->expandAllCheckBox->isChecked() ); 01353 } 01354 01355 /*--------------------------------------------------------------------------------------*/ 01356 01357 void GCMainWindow::setStatusBarMessage( const QString& message ) 01358 { 01359 Q_UNUSED( message ); 01360 // TODO. 01361 } 01362 01363 /*--------------------------------------------------------------------------------------*/ 01364 01365 void GCMainWindow::setTextEditContent( GCTreeWidgetItem* item ) 01366 { 01367 m_fileContentsChanged = true; 01368 ui->dockWidgetTextEdit->setContent( ui->treeWidget->toString() ); 01369 highlightTextElement( item ); 01370 } 01371 01372 /*--------------------------------------------------------------------------------------*/ 01373 01374 void GCMainWindow::highlightTextElement( GCTreeWidgetItem* item ) 01375 { 01376 if( item ) 01377 { 01378 QString stringToMatch = item->toString(); 01379 int pos = ui->treeWidget->itemPositionRelativeToIdenticalSiblings( stringToMatch, item->index() ); 01380 ui->dockWidgetTextEdit->findTextRelativeToDuplicates( stringToMatch, pos ); 01381 } 01382 } 01383 01384 /*--------------------------------------------------------------------------------------*/ 01385 01386 void GCMainWindow::insertEmptyTableRow() 01387 { 01388 QTableWidgetItem* label = new QTableWidgetItem( EMPTY ); 01389 01390 int lastRow = ui->tableWidget->rowCount(); 01391 ui->tableWidget->setRowCount( lastRow + 1 ); 01392 ui->tableWidget->setItem( lastRow, 0, label ); 01393 01394 /* Create the combo box, but deactivate it until we have an associated attribute name. */ 01395 GCComboBox* attributeCombo = new GCComboBox; 01396 attributeCombo->setEditable( true ); 01397 attributeCombo->setEnabled( false ); 01398 01399 /* Only connect after inserting items or bad things will happen! */ 01400 connect( attributeCombo, SIGNAL( currentIndexChanged( QString ) ), this, SLOT( attributeValueChanged( QString ) ) ); 01401 01402 ui->tableWidget->setCellWidget( lastRow, 1, attributeCombo ); 01403 m_comboBoxes.insert( attributeCombo, lastRow ); 01404 01405 /* This will point the current combo box member to the combo that's been activated 01406 in the table widget (used in "attributeValueChanged" to obtain the row number the 01407 combo box appears in in the table widget, etc, etc). */ 01408 connect( attributeCombo, SIGNAL( activated( int ) ), m_signalMapper, SLOT( map() ) ); 01409 m_signalMapper->setMapping( attributeCombo, attributeCombo ); 01410 } 01411 01412 /*--------------------------------------------------------------------------------------*/ 01413 01414 void GCMainWindow::resetTableWidget() 01415 { 01416 /* Remove the currently visible/live combo boxes from the signal mapper's 01417 mappings and the combo box map before we whack them all. */ 01418 for( int i = 0; i < m_comboBoxes.keys().size(); ++i ) 01419 { 01420 m_signalMapper->removeMappings( m_comboBoxes.keys().at( i ) ); 01421 } 01422 01423 m_comboBoxes.clear(); 01424 01425 ui->tableWidget->clearContents(); // also deletes current items 01426 ui->tableWidget->setRowCount( 0 ); 01427 } 01428 01429 /*--------------------------------------------------------------------------------------*/ 01430 01431 void GCMainWindow::startSaveTimer() 01432 { 01433 /* Automatically save the file at five minute intervals. */ 01434 if( !m_saveTimer ) 01435 { 01436 m_saveTimer = new QTimer( this ); 01437 connect( m_saveTimer, SIGNAL( timeout() ), this, SLOT( saveTempFile() ) ); 01438 m_saveTimer->start( 300000 ); 01439 } 01440 else 01441 { 01442 /* If the timer was stopped due to a DOM reset, start it again. */ 01443 m_saveTimer->start( 300000 ); 01444 } 01445 } 01446 01447 /*--------------------------------------------------------------------------------------*/ 01448 01449 void GCMainWindow::toggleAddElementWidgets() 01450 { 01451 /* Make sure we don't inadvertently create "empty" elements. */ 01452 if( ui->addElementComboBox->count() < 1 ) 01453 { 01454 ui->addElementComboBox->setEnabled( false ); 01455 ui->addChildElementButton->setEnabled( false ); 01456 ui->commentLineEdit->setEnabled( false ); 01457 01458 /* Check if the element combo box is empty due to an empty profile 01459 being active. */ 01460 if( GCDataBaseInterface::instance()->isProfileEmpty() ) 01461 { 01462 ui->showEmptyProfileHelpButton->setVisible( true ); 01463 } 01464 else 01465 { 01466 ui->showEmptyProfileHelpButton->setVisible( false ); 01467 } 01468 } 01469 else 01470 { 01471 ui->addElementComboBox->setEnabled( true ); 01472 ui->addChildElementButton->setEnabled( true ); 01473 ui->commentLineEdit->setEnabled( true ); 01474 ui->showEmptyProfileHelpButton->setVisible( false ); 01475 } 01476 } 01477 01478 /*--------------------------------------------------------------------------------------*/ 01479 01480 void GCMainWindow::querySetActiveSession( QString reason ) 01481 { 01482 while( !GCDataBaseInterface::instance()->hasActiveSession() ) 01483 { 01484 GCMessageSpace::showErrorMessageBox( this, reason ); 01485 GCDBSessionManager* manager = createDBSessionManager(); 01486 manager->selectActiveDatabase(); 01487 delete manager; 01488 } 01489 } 01490 01491 /*--------------------------------------------------------------------------------------*/ 01492 01493 void GCMainWindow::readSettings() 01494 { 01495 restoreGeometry( GCGlobalSpace::windowGeometry() ); 01496 restoreState( GCGlobalSpace::windowState() ); 01497 01498 setShowHelpButtons( GCGlobalSpace::showHelpButtons() ); 01499 setShowTreeItemsVerbose( GCGlobalSpace::showTreeItemsVerbose() ); 01500 01501 ui->actionRememberWindowGeometry->setChecked( GCGlobalSpace::useWindowSettings() ); 01502 ui->actionUseDarkTheme->setChecked( GCGlobalSpace::useDarkTheme() ); 01503 ui->actionShowHelpButtons->setChecked( GCGlobalSpace::showHelpButtons() ); 01504 } 01505 01506 /*--------------------------------------------------------------------------------------*/ 01507 01508 void GCMainWindow::saveSettings() 01509 { 01510 if( ui->actionRememberWindowGeometry->isChecked() ) 01511 { 01512 GCGlobalSpace::setWindowGeometry( saveGeometry() ); 01513 GCGlobalSpace::setWindowState( saveState() ); 01514 } 01515 else 01516 { 01517 GCGlobalSpace::removeWindowInfo(); 01518 } 01519 01520 GCGlobalSpace::setUseDarkTheme( ui->actionUseDarkTheme->isChecked() ); 01521 GCGlobalSpace::setUseWindowSettings( ui->actionRememberWindowGeometry->isChecked() ); 01522 } 01523 01524 /*--------------------------------------------------------------------------------------*/ 01525 01526 void GCMainWindow::queryRestoreFiles() 01527 { 01528 QDir dir = QDir::current(); 01529 QStringList tempFiles = QDir::current() 01530 .entryList( QDir::Files ) 01531 .filter( QString( "%1_temp" ) 01532 .arg( GCDataBaseInterface::instance()->activeSessionName().remove( ".db" ) ) ); 01533 01534 if( !tempFiles.empty() ) 01535 { 01536 QMessageBox::StandardButton accept = QMessageBox::information( this, 01537 "Found recovery files", 01538 "Found auto-recover files related to this profile. Would you like to view and save?", 01539 QMessageBox::Ok | QMessageBox::Cancel, 01540 QMessageBox::Ok ); 01541 01542 if( accept == QMessageBox::Ok ) 01543 { 01544 /* "Delete on close" flag set. */ 01545 GCRestoreFilesForm* restore = new GCRestoreFilesForm( tempFiles, this ); 01546 restore->exec(); 01547 } 01548 } 01549 } 01550 01551 /*--------------------------------------------------------------------------------------*/ 01552 01553 void GCMainWindow::saveTempFile() 01554 { 01555 QFile file( QDir::currentPath() + 01556 QString( "/%1_%2_temp" ) 01557 .arg( m_currentXMLFileName.split( "/" ).last() ) 01558 .arg( GCDataBaseInterface::instance()->activeSessionName().remove( ".db" ) ) ); 01559 01560 /* Since this is an attempt at auto-saving, we aim for a "best case" scenario and don't display 01561 error messages if encountered. */ 01562 if( file.open( QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text ) ) 01563 { 01564 QTextStream outStream( &file ); 01565 outStream << ui->treeWidget->toString(); 01566 file.close(); 01567 startSaveTimer(); 01568 } 01569 } 01570 01571 /*--------------------------------------------------------------------------------------*/ 01572 01573 void GCMainWindow::deleteTempFile() 01574 { 01575 QFile file( QDir::currentPath() + 01576 QString( "/%1_%2_temp" ) 01577 .arg( m_currentXMLFileName.split( "/" ).last() ) 01578 .arg( GCDataBaseInterface::instance()->activeSessionName().remove( ".db" ) ) ); 01579 01580 file.remove(); 01581 } 01582 01583 /*--------------------------------------------------------------------------------------*/