/*************************************************************************** qgsprocessingalgorithmdialogbase.cpp ------------------------------------ Date : November 2017 Copyright : (C) 2017 Nyall Dawson Email : nyall dot dawson at gmail dot com *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "qgsprocessingalgorithmdialogbase.h" #include "qgssettings.h" #include "qgshelp.h" #include "qgsmessagebar.h" #include "qgsgui.h" #include "processing/qgsprocessingalgorithm.h" #include "processing/qgsprocessingprovider.h" #include "qgstaskmanager.h" #include "processing/qgsprocessingalgrunnertask.h" #include "qgsstringutils.h" #include "qgsapplication.h" #include "qgspanelwidget.h" #include "qgsjsonutils.h" #include "qgsunittypes.h" #include "qgsnative.h" #include #include #include #include #include #include #include #include #include ///@cond NOT_STABLE QgsProcessingAlgorithmDialogFeedback::QgsProcessingAlgorithmDialogFeedback() : QgsProcessingFeedback( false ) {} void QgsProcessingAlgorithmDialogFeedback::setProgressText( const QString &text ) { QgsProcessingFeedback::setProgressText( text ); emit progressTextChanged( text ); } void QgsProcessingAlgorithmDialogFeedback::reportError( const QString &error, bool fatalError ) { QgsProcessingFeedback::reportError( error, fatalError ); emit errorReported( error, fatalError ); } void QgsProcessingAlgorithmDialogFeedback::pushWarning( const QString &warning ) { QgsProcessingFeedback::pushWarning( warning ); emit warningPushed( warning ); } void QgsProcessingAlgorithmDialogFeedback::pushInfo( const QString &info ) { QgsProcessingFeedback::pushInfo( info ); emit infoPushed( info ); } void QgsProcessingAlgorithmDialogFeedback::pushCommandInfo( const QString &info ) { QgsProcessingFeedback::pushCommandInfo( info ); emit commandInfoPushed( info ); } void QgsProcessingAlgorithmDialogFeedback::pushDebugInfo( const QString &info ) { QgsProcessingFeedback::pushDebugInfo( info ); emit debugInfoPushed( info ); } void QgsProcessingAlgorithmDialogFeedback::pushConsoleInfo( const QString &info ) { QgsProcessingFeedback::pushConsoleInfo( info ); emit consoleInfoPushed( info ); } void QgsProcessingAlgorithmDialogFeedback::pushFormattedMessage( const QString &html, const QString &text ) { QgsProcessingFeedback::pushFormattedMessage( html, text ); emit formattedMessagePushed( html ); } // // QgsProcessingAlgorithmDialogBase // QgsProcessingAlgorithmDialogBase::QgsProcessingAlgorithmDialogBase( QWidget *parent, Qt::WindowFlags flags, DialogMode mode ) : QDialog( parent, flags ) , mMode( mode ) { setupUi( this ); //don't collapse parameters panel splitter->setCollapsible( 0, false ); // add collapse button to splitter QSplitterHandle *splitterHandle = splitter->handle( 1 ); QVBoxLayout *handleLayout = new QVBoxLayout(); handleLayout->setContentsMargins( 0, 0, 0, 0 ); mButtonCollapse = new QToolButton( splitterHandle ); mButtonCollapse->setAutoRaise( true ); mButtonCollapse->setFixedSize( 12, 12 ); mButtonCollapse->setCursor( Qt::ArrowCursor ); handleLayout->addWidget( mButtonCollapse ); handleLayout->addStretch(); splitterHandle->setLayout( handleLayout ); QgsGui::enableAutoGeometryRestore( this ); txtLog->setOpenLinks( false ); connect( txtLog, &QTextBrowser::anchorClicked, this, &QgsProcessingAlgorithmDialogBase::urlClicked ); const QgsSettings settings; splitter->restoreState( settings.value( QStringLiteral( "/Processing/dialogBaseSplitter" ), QByteArray() ).toByteArray() ); mSplitterState = splitter->saveState(); splitterChanged( 0, 0 ); // Rename OK button to Run mButtonRun = mButtonBox->button( QDialogButtonBox::Ok ); mButtonRun->setText( tr( "Run" ) ); // Rename Yes button. Yes is used to ensure same position of Run and Change Parameters with respect to Close button. mButtonChangeParameters = mButtonBox->button( QDialogButtonBox::Yes ); mButtonChangeParameters->setText( tr( "Change Parameters" ) ); buttonCancel->setEnabled( false ); mButtonClose = mButtonBox->button( QDialogButtonBox::Close ); switch ( mMode ) { case DialogMode::Single: { mAdvancedButton = new QPushButton( tr( "Advanced" ) ); mAdvancedMenu = new QMenu( this ); mAdvancedButton->setMenu( mAdvancedMenu ); mContextSettingsAction = new QAction( tr( "Algorithm Settings…" ), mAdvancedMenu ); mContextSettingsAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/propertyicons/settings.svg" ) ) ); mAdvancedMenu->addAction( mContextSettingsAction ); connect( mContextSettingsAction, &QAction::triggered, this, [this] { if ( QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( mMainWidget ) ) { mTabWidget->setCurrentIndex( 0 ); if ( !mContextOptionsWidget ) { mContextOptionsWidget = new QgsProcessingContextOptionsWidget(); mContextOptionsWidget->setFromContext( processingContext() ); mContextOptionsWidget->setLogLevel( mLogLevel ); panel->openPanel( mContextOptionsWidget ); connect( mContextOptionsWidget, &QgsPanelWidget::widgetChanged, this, [ = ] { mOverrideDefaultContextSettings = true; mGeometryCheck = mContextOptionsWidget->invalidGeometryCheck(); mDistanceUnits = mContextOptionsWidget->distanceUnit(); mAreaUnits = mContextOptionsWidget->areaUnit(); mTemporaryFolderOverride = mContextOptionsWidget->temporaryFolder(); mMaximumThreads = mContextOptionsWidget->maximumThreads(); mLogLevel = mContextOptionsWidget->logLevel(); } ); } } } ); mAdvancedMenu->addSeparator(); QAction *copyAsPythonCommand = new QAction( tr( "Copy as Python Command" ), mAdvancedMenu ); copyAsPythonCommand->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconPythonFile.svg" ) ) ); mAdvancedMenu->addAction( copyAsPythonCommand ); connect( copyAsPythonCommand, &QAction::triggered, this, [this] { if ( const QgsProcessingAlgorithm *alg = algorithm() ) { QgsProcessingContext *context = processingContext(); if ( !context ) return; const QString command = alg->asPythonCommand( createProcessingParameters(), *context ); QMimeData *m = new QMimeData(); m->setText( command ); QClipboard *cb = QApplication::clipboard(); #ifdef Q_OS_LINUX cb->setMimeData( m, QClipboard::Selection ); #endif cb->setMimeData( m, QClipboard::Clipboard ); } } ); mCopyAsQgisProcessCommand = new QAction( tr( "Copy as qgis_process Command" ), mAdvancedMenu ); mCopyAsQgisProcessCommand->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionTerminal.svg" ) ) ); mAdvancedMenu->addAction( mCopyAsQgisProcessCommand ); connect( mCopyAsQgisProcessCommand, &QAction::triggered, this, [this] { if ( const QgsProcessingAlgorithm *alg = algorithm() ) { QgsProcessingContext *context = processingContext(); if ( !context ) return; bool ok = false; const QString command = alg->asQgisProcessCommand( createProcessingParameters(), *context, ok ); if ( ! ok ) { mMessageBar->pushMessage( tr( "Current settings cannot be specified as arguments to qgis_process (Pipe parameters as JSON to qgis_process instead)" ), Qgis::MessageLevel::Warning ); } else { QMimeData *m = new QMimeData(); m->setText( command ); QClipboard *cb = QApplication::clipboard(); #ifdef Q_OS_LINUX cb->setMimeData( m, QClipboard::Selection ); #endif cb->setMimeData( m, QClipboard::Clipboard ); } } } ); mAdvancedMenu->addSeparator(); QAction *copyAsJson = new QAction( tr( "Copy as JSON" ), mAdvancedMenu ); copyAsJson->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionEditCopy.svg" ) ) ); mAdvancedMenu->addAction( copyAsJson ); connect( copyAsJson, &QAction::triggered, this, [this] { if ( const QgsProcessingAlgorithm *alg = algorithm() ) { QgsProcessingContext *context = processingContext(); if ( !context ) return; const QVariantMap properties = alg->asMap( createProcessingParameters(), *context ); const QString json = QString::fromStdString( QgsJsonUtils::jsonFromVariant( properties ).dump( 2 ) ); QMimeData *m = new QMimeData(); m->setText( json ); QClipboard *cb = QApplication::clipboard(); #ifdef Q_OS_LINUX cb->setMimeData( m, QClipboard::Selection ); #endif cb->setMimeData( m, QClipboard::Clipboard ); } } ); mPasteJsonAction = new QAction( tr( "Paste Settings" ), mAdvancedMenu ); mPasteJsonAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionEditPaste.svg" ) ) ); mAdvancedMenu->addAction( mPasteJsonAction ); connect( mPasteJsonAction, &QAction::triggered, this, [this] { const QString text = QApplication::clipboard()->text(); if ( text.isEmpty() ) return; const QVariantMap parameterValues = QgsJsonUtils::parseJson( text ).toMap().value( QStringLiteral( "inputs" ) ).toMap(); if ( parameterValues.isEmpty() ) return; bool ok = false; QString error; const QVariantMap preparedValues = QgsProcessingUtils::preprocessQgisProcessParameters( parameterValues, ok, error ); setParameters( preparedValues ); } ); mButtonBox->addButton( mAdvancedButton, QDialogButtonBox::ResetRole ); break; } case DialogMode::Batch: break; } if ( mAdvancedMenu ) { connect( mAdvancedMenu, &QMenu::aboutToShow, this, [ = ] { mCopyAsQgisProcessCommand->setEnabled( algorithm() && !( algorithm()->flags() & Qgis::ProcessingAlgorithmFlag::NotAvailableInStandaloneTool ) ); mPasteJsonAction->setEnabled( !QApplication::clipboard()->text().isEmpty() ); } ); } connect( mButtonRun, &QPushButton::clicked, this, &QgsProcessingAlgorithmDialogBase::runAlgorithm ); connect( mButtonChangeParameters, &QPushButton::clicked, this, &QgsProcessingAlgorithmDialogBase::showParameters ); connect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsProcessingAlgorithmDialogBase::closeClicked ); connect( mButtonBox, &QDialogButtonBox::helpRequested, this, &QgsProcessingAlgorithmDialogBase::openHelp ); connect( mButtonCollapse, &QToolButton::clicked, this, &QgsProcessingAlgorithmDialogBase::toggleCollapsed ); connect( splitter, &QSplitter::splitterMoved, this, &QgsProcessingAlgorithmDialogBase::splitterChanged ); connect( mButtonSaveLog, &QToolButton::clicked, this, &QgsProcessingAlgorithmDialogBase::saveLog ); connect( mButtonCopyLog, &QToolButton::clicked, this, &QgsProcessingAlgorithmDialogBase::copyLogToClipboard ); connect( mButtonClearLog, &QToolButton::clicked, this, &QgsProcessingAlgorithmDialogBase::clearLog ); connect( mTabWidget, &QTabWidget::currentChanged, this, &QgsProcessingAlgorithmDialogBase::mTabWidget_currentChanged ); mMessageBar = new QgsMessageBar(); mMessageBar->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed ); verticalLayout->insertWidget( 0, mMessageBar ); connect( QgsApplication::taskManager(), &QgsTaskManager::taskTriggered, this, &QgsProcessingAlgorithmDialogBase::taskTriggered ); } QgsProcessingAlgorithmDialogBase::~QgsProcessingAlgorithmDialogBase() = default; void QgsProcessingAlgorithmDialogBase::setParameters( const QVariantMap & ) {} void QgsProcessingAlgorithmDialogBase::setAlgorithm( QgsProcessingAlgorithm *algorithm ) { mAlgorithm.reset( algorithm ); QString title; if ( ( QgsGui::higFlags() & QgsGui::HigDialogTitleIsTitleCase ) && !( algorithm->flags() & Qgis::ProcessingAlgorithmFlag::DisplayNameIsLiteral ) ) { title = QStringLiteral( "%1 - %2" ).arg( QgsStringUtils::capitalize( mAlgorithm->group(), Qgis::Capitalization::TitleCase ), QgsStringUtils::capitalize( mAlgorithm->displayName(), Qgis::Capitalization::TitleCase ) ); } else { title = QStringLiteral( "%1 - %2" ).arg( mAlgorithm->group(), mAlgorithm->displayName() ); } setWindowTitle( title ); const QString algHelp = formatHelp( algorithm ); if ( algHelp.isEmpty() ) textShortHelp->hide(); else { textShortHelp->document()->setDefaultStyleSheet( QStringLiteral( ".summary { margin-left: 10px; margin-right: 10px; }\n" "h2 { color: #555555; padding-bottom: 15px; }\n" "a { text - decoration: none; color: #3498db; font-weight: bold; }\n" "p { color: #666666; }\n" "b { color: #333333; }\n" "dl dd { margin - bottom: 5px; }" ) ); textShortHelp->setHtml( algHelp ); connect( textShortHelp, &QTextBrowser::anchorClicked, this, &QgsProcessingAlgorithmDialogBase::linkClicked ); textShortHelp->show(); } if ( algorithm->helpUrl().isEmpty() && ( !algorithm->provider() || algorithm->provider()->helpId().isEmpty() ) ) { mButtonBox->removeButton( mButtonBox->button( QDialogButtonBox::Help ) ); } const QString warning = algorithm->provider() ? algorithm->provider()->warningMessage() : QString(); if ( !warning.isEmpty() ) { mMessageBar->pushMessage( warning, Qgis::MessageLevel::Warning ); } } QgsProcessingAlgorithm *QgsProcessingAlgorithmDialogBase::algorithm() { return mAlgorithm.get(); } void QgsProcessingAlgorithmDialogBase::setMainWidget( QgsPanelWidget *widget ) { if ( mMainWidget ) { mMainWidget->deleteLater(); } mPanelStack->setMainPanel( widget ); widget->setDockMode( true ); mMainWidget = widget; connect( mMainWidget, &QgsPanelWidget::panelAccepted, this, &QDialog::reject ); } QgsPanelWidget *QgsProcessingAlgorithmDialogBase::mainWidget() { return mMainWidget; } void QgsProcessingAlgorithmDialogBase::saveLogToFile( const QString &path, const LogFormat format ) { QFile logFile( path ); if ( !logFile.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate ) ) { return; } QTextStream fout( &logFile ); switch ( format ) { case FormatPlainText: fout << txtLog->toPlainText(); break; case FormatHtml: fout << txtLog->toHtml(); break; } } QgsProcessingFeedback *QgsProcessingAlgorithmDialogBase::createFeedback() { auto feedback = std::make_unique< QgsProcessingAlgorithmDialogFeedback >(); connect( feedback.get(), &QgsProcessingFeedback::progressChanged, this, &QgsProcessingAlgorithmDialogBase::setPercentage ); connect( feedback.get(), &QgsProcessingAlgorithmDialogFeedback::commandInfoPushed, this, &QgsProcessingAlgorithmDialogBase::pushCommandInfo ); connect( feedback.get(), &QgsProcessingAlgorithmDialogFeedback::consoleInfoPushed, this, &QgsProcessingAlgorithmDialogBase::pushConsoleInfo ); connect( feedback.get(), &QgsProcessingAlgorithmDialogFeedback::debugInfoPushed, this, &QgsProcessingAlgorithmDialogBase::pushDebugInfo ); connect( feedback.get(), &QgsProcessingAlgorithmDialogFeedback::errorReported, this, &QgsProcessingAlgorithmDialogBase::reportError ); connect( feedback.get(), &QgsProcessingAlgorithmDialogFeedback::warningPushed, this, &QgsProcessingAlgorithmDialogBase::pushWarning ); connect( feedback.get(), &QgsProcessingAlgorithmDialogFeedback::infoPushed, this, &QgsProcessingAlgorithmDialogBase::pushInfo ); connect( feedback.get(), &QgsProcessingAlgorithmDialogFeedback::formattedMessagePushed, this, &QgsProcessingAlgorithmDialogBase::pushFormattedMessage ); connect( feedback.get(), &QgsProcessingAlgorithmDialogFeedback::progressTextChanged, this, &QgsProcessingAlgorithmDialogBase::setProgressText ); connect( buttonCancel, &QPushButton::clicked, feedback.get(), &QgsProcessingFeedback::cancel ); return feedback.release(); } QDialogButtonBox *QgsProcessingAlgorithmDialogBase::buttonBox() { return mButtonBox; } QTabWidget *QgsProcessingAlgorithmDialogBase::tabWidget() { return mTabWidget; } void QgsProcessingAlgorithmDialogBase::showLog() { mTabWidget->setCurrentIndex( 1 ); } void QgsProcessingAlgorithmDialogBase::showParameters() { mTabWidget->setCurrentIndex( 0 ); } QPushButton *QgsProcessingAlgorithmDialogBase::runButton() { return mButtonRun; } QPushButton *QgsProcessingAlgorithmDialogBase::cancelButton() { return buttonCancel; } QPushButton *QgsProcessingAlgorithmDialogBase::changeParametersButton() { return mButtonChangeParameters; } void QgsProcessingAlgorithmDialogBase::clearProgress() { progressBar->setMaximum( 0 ); } void QgsProcessingAlgorithmDialogBase::setExecuted( bool executed ) { mExecuted = executed; } void QgsProcessingAlgorithmDialogBase::setExecutedAnyResult( bool executedAnyResult ) { mExecutedAnyResult = executedAnyResult; } void QgsProcessingAlgorithmDialogBase::setResults( const QVariantMap &results ) { mResults = results; } void QgsProcessingAlgorithmDialogBase::finished( bool, const QVariantMap &, QgsProcessingContext &, QgsProcessingFeedback * ) { } void QgsProcessingAlgorithmDialogBase::openHelp() { QUrl algHelp = mAlgorithm->helpUrl(); if ( algHelp.isEmpty() && mAlgorithm->provider() ) { algHelp = QgsHelp::helpUrl( QStringLiteral( "processing_algs/%1/%2.html#%3" ).arg( mAlgorithm->provider()->helpId(), mAlgorithm->groupId(), QStringLiteral( "%1%2" ).arg( mAlgorithm->provider()->helpId() ).arg( mAlgorithm->name() ) ) ); } if ( !algHelp.isEmpty() ) QDesktopServices::openUrl( algHelp ); } void QgsProcessingAlgorithmDialogBase::toggleCollapsed() { if ( mHelpCollapsed ) { splitter->restoreState( mSplitterState ); mButtonCollapse->setArrowType( Qt::RightArrow ); } else { mSplitterState = splitter->saveState(); splitter->setSizes( QList() << 1 << 0 ); mButtonCollapse->setArrowType( Qt::LeftArrow ); } mHelpCollapsed = !mHelpCollapsed; } void QgsProcessingAlgorithmDialogBase::splitterChanged( int, int ) { if ( splitter->sizes().at( 1 ) == 0 ) { mHelpCollapsed = true; mButtonCollapse->setArrowType( Qt::LeftArrow ); } else { mHelpCollapsed = false; mButtonCollapse->setArrowType( Qt::RightArrow ); } } void QgsProcessingAlgorithmDialogBase::mTabWidget_currentChanged( int ) { updateRunButtonVisibility(); } void QgsProcessingAlgorithmDialogBase::linkClicked( const QUrl &url ) { QDesktopServices::openUrl( url.toString() ); } void QgsProcessingAlgorithmDialogBase::algExecuted( bool successful, const QVariantMap & ) { mAlgorithmTask = nullptr; if ( !successful ) { // show dialog to display errors show(); raise(); setWindowState( ( windowState() & ~Qt::WindowMinimized ) | Qt::WindowActive ); activateWindow(); showLog(); } else { if ( isFinalized() && successful ) { progressBar->setFormat( tr( "Complete" ) ); } // delete dialog if closed if ( isFinalized() && !isVisible() ) { deleteLater(); } } } void QgsProcessingAlgorithmDialogBase::taskTriggered( QgsTask *task ) { if ( task == mAlgorithmTask ) { show(); raise(); setWindowState( ( windowState() & ~Qt::WindowMinimized ) | Qt::WindowActive ); activateWindow(); showLog(); } } void QgsProcessingAlgorithmDialogBase::closeClicked() { reject(); close(); } void QgsProcessingAlgorithmDialogBase::urlClicked( const QUrl &url ) { const QFileInfo file( url.toLocalFile() ); if ( file.exists() && !file.isDir() ) QgsGui::nativePlatformInterface()->openFileExplorerAndSelectFile( url.toLocalFile() ); else QDesktopServices::openUrl( url ); } Qgis::ProcessingLogLevel QgsProcessingAlgorithmDialogBase::logLevel() const { return mLogLevel; } void QgsProcessingAlgorithmDialogBase::setLogLevel( Qgis::ProcessingLogLevel level ) { mLogLevel = level; } void QgsProcessingAlgorithmDialogBase::reportError( const QString &error, bool fatalError ) { setInfo( error, true ); if ( fatalError ) resetGui(); showLog(); processEvents(); } void QgsProcessingAlgorithmDialogBase::pushWarning( const QString &warning ) { setInfo( warning, false, true, true ); processEvents(); } void QgsProcessingAlgorithmDialogBase::pushInfo( const QString &info ) { setInfo( info ); processEvents(); } void QgsProcessingAlgorithmDialogBase::pushFormattedMessage( const QString &html ) { setInfo( html, false, false ); processEvents(); } void QgsProcessingAlgorithmDialogBase::pushCommandInfo( const QString &command ) { txtLog->append( QStringLiteral( "%1" ).arg( formatStringForLog( command.toHtmlEscaped() ) ) ); scrollToBottomOfLog(); processEvents(); } void QgsProcessingAlgorithmDialogBase::pushDebugInfo( const QString &message ) { txtLog->append( QStringLiteral( "%1" ).arg( formatStringForLog( message.toHtmlEscaped() ) ) ); scrollToBottomOfLog(); processEvents(); } void QgsProcessingAlgorithmDialogBase::pushConsoleInfo( const QString &info ) { txtLog->append( QStringLiteral( "%1" ).arg( formatStringForLog( info.toHtmlEscaped() ) ) ); scrollToBottomOfLog(); processEvents(); } QDialog *QgsProcessingAlgorithmDialogBase::createProgressDialog() { QgsProcessingAlgorithmProgressDialog *dialog = new QgsProcessingAlgorithmProgressDialog( this ); dialog->setWindowModality( Qt::ApplicationModal ); dialog->setWindowTitle( windowTitle() ); dialog->setGeometry( geometry() ); // match size/position to this dialog connect( progressBar, &QProgressBar::valueChanged, dialog->progressBar(), &QProgressBar::setValue ); connect( dialog->cancelButton(), &QPushButton::clicked, buttonCancel, &QPushButton::click ); dialog->logTextEdit()->setHtml( txtLog->toHtml() ); connect( txtLog, &QTextEdit::textChanged, dialog, [this, dialog]() { dialog->logTextEdit()->setHtml( txtLog->toHtml() ); QScrollBar *sb = dialog->logTextEdit()->verticalScrollBar(); sb->setValue( sb->maximum() ); } ); return dialog; } void QgsProcessingAlgorithmDialogBase::clearLog() { txtLog->clear(); } void QgsProcessingAlgorithmDialogBase::saveLog() { QgsSettings settings; const QString lastUsedDir = settings.value( QStringLiteral( "/Processing/lastUsedLogDirectory" ), QDir::homePath() ).toString(); QString filter; const QString txtExt = tr( "Text files" ) + QStringLiteral( " (*.txt *.TXT)" ); const QString htmlExt = tr( "HTML files" ) + QStringLiteral( " (*.html *.HTML)" ); const QString path = QFileDialog::getSaveFileName( this, tr( "Save Log to File" ), lastUsedDir, txtExt + ";;" + htmlExt, &filter ); // return dialog focus on Mac activateWindow(); raise(); if ( path.isEmpty() ) { return; } settings.setValue( QStringLiteral( "/Processing/lastUsedLogDirectory" ), QFileInfo( path ).path() ); LogFormat format = FormatPlainText; if ( filter == htmlExt ) { format = FormatHtml; } saveLogToFile( path, format ); } void QgsProcessingAlgorithmDialogBase::copyLogToClipboard() { QMimeData *m = new QMimeData(); m->setText( txtLog->toPlainText() ); m->setHtml( txtLog->toHtml() ); QClipboard *cb = QApplication::clipboard(); #ifdef Q_OS_LINUX cb->setMimeData( m, QClipboard::Selection ); #endif cb->setMimeData( m, QClipboard::Clipboard ); } void QgsProcessingAlgorithmDialogBase::closeEvent( QCloseEvent *e ) { if ( !mHelpCollapsed ) { QgsSettings settings; settings.setValue( QStringLiteral( "/Processing/dialogBaseSplitter" ), splitter->saveState() ); } QDialog::closeEvent( e ); if ( !mAlgorithmTask && isFinalized() ) { // when running a background task, the dialog is kept around and deleted only when the task // completes. But if not running a task, we auto cleanup (later - gotta give callers a chance // to retrieve results and execution status). deleteLater(); } } void QgsProcessingAlgorithmDialogBase::runAlgorithm() { } void QgsProcessingAlgorithmDialogBase::setPercentage( double percent ) { // delay setting maximum progress value until we know algorithm reports progress if ( progressBar->maximum() == 0 ) progressBar->setMaximum( 100 ); progressBar->setValue( percent ); processEvents(); } void QgsProcessingAlgorithmDialogBase::setProgressText( const QString &text ) { lblProgress->setText( text ); setInfo( text, false ); scrollToBottomOfLog(); processEvents(); } QString QgsProcessingAlgorithmDialogBase::formatHelp( QgsProcessingAlgorithm *algorithm ) { const QString text = algorithm->shortHelpString(); if ( !text.isEmpty() ) { const QStringList paragraphs = text.split( '\n' ); QString help; for ( const QString ¶graph : paragraphs ) { help += QStringLiteral( "

%1

" ).arg( paragraph ); } return QStringLiteral( "

%1

%2" ).arg( algorithm->displayName(), help ); } else if ( !algorithm->shortDescription().isEmpty() ) { return QStringLiteral( "

%1

%2

" ).arg( algorithm->displayName(), algorithm->shortDescription() ); } else return QString(); } void QgsProcessingAlgorithmDialogBase::processEvents() { if ( mAlgorithmTask ) { // no need to call this - the algorithm is running in a thread. // in fact, calling it causes a crash on Windows when the algorithm // is running in a background thread... unfortunately we need something // like this for non-threadable algorithms, otherwise there's no chance // for users to hit cancel or see progress updates... return; } // So that we get a chance of hitting the Abort button #ifdef Q_OS_LINUX // One iteration is actually enough on Windows to get good interactivity // whereas on Linux we must allow for far more iterations. // For safety limit the number of iterations int nIters = 0; while ( ++nIters < 100 ) #endif { QCoreApplication::processEvents(); } } void QgsProcessingAlgorithmDialogBase::scrollToBottomOfLog() { QScrollBar *sb = txtLog->verticalScrollBar(); sb->setValue( sb->maximum() ); } void QgsProcessingAlgorithmDialogBase::resetGui() { lblProgress->clear(); progressBar->setMaximum( 100 ); progressBar->setValue( 0 ); mButtonRun->setEnabled( true ); mButtonChangeParameters->setEnabled( true ); mButtonClose->setEnabled( true ); if ( mMainWidget ) { mMainWidget->setEnabled( true ); } updateRunButtonVisibility(); resetAdditionalGui(); } void QgsProcessingAlgorithmDialogBase::updateRunButtonVisibility() { // Activate run button if current tab is Parameters const bool runButtonVisible = mTabWidget->currentIndex() == 0; mButtonRun->setVisible( runButtonVisible ); if ( runButtonVisible ) progressBar->resetFormat(); mButtonChangeParameters->setVisible( !runButtonVisible && mExecutedAnyResult && mButtonChangeParameters->isEnabled() ); } void QgsProcessingAlgorithmDialogBase::resetAdditionalGui() { } void QgsProcessingAlgorithmDialogBase::blockControlsWhileRunning() { mButtonRun->setEnabled( false ); mButtonChangeParameters->setEnabled( false ); if ( mMainWidget ) { mMainWidget->setEnabled( false ); } blockAdditionalControlsWhileRunning(); } void QgsProcessingAlgorithmDialogBase::blockAdditionalControlsWhileRunning() { } QgsMessageBar *QgsProcessingAlgorithmDialogBase::messageBar() { return mMessageBar; } void QgsProcessingAlgorithmDialogBase::hideShortHelp() { textShortHelp->setVisible( false ); } void QgsProcessingAlgorithmDialogBase::setCurrentTask( QgsProcessingAlgRunnerTask *task ) { mAlgorithmTask = task; connect( mAlgorithmTask, &QgsProcessingAlgRunnerTask::executed, this, &QgsProcessingAlgorithmDialogBase::algExecuted ); QgsApplication::taskManager()->addTask( mAlgorithmTask ); } QString QgsProcessingAlgorithmDialogBase::formatStringForLog( const QString &string ) { QString s = string; s.replace( '\n', QLatin1String( "
" ) ); return s; } bool QgsProcessingAlgorithmDialogBase::isFinalized() { return true; } void QgsProcessingAlgorithmDialogBase::applyContextOverrides( QgsProcessingContext *context ) { if ( !context ) return; context->setLogLevel( logLevel() ); if ( mOverrideDefaultContextSettings ) { context->setInvalidGeometryCheck( mGeometryCheck ); context->setDistanceUnit( mDistanceUnits ); context->setAreaUnit( mAreaUnits ); context->setTemporaryFolder( mTemporaryFolderOverride ); context->setMaximumThreads( mMaximumThreads ); } } void QgsProcessingAlgorithmDialogBase::setInfo( const QString &message, bool isError, bool escapeHtml, bool isWarning ) { constexpr int MESSAGE_COUNT_LIMIT = 10000; // Avoid logging too many messages, which might blow memory. if ( mMessageLoggedCount == MESSAGE_COUNT_LIMIT ) return; ++mMessageLoggedCount; // note -- we have to wrap the message in a span block, or QTextEdit::append sometimes gets confused // and varies between treating it as a HTML string or a plain text string! (see https://github.com/qgis/QGIS/issues/37934) if ( mMessageLoggedCount == MESSAGE_COUNT_LIMIT ) txtLog->append( QStringLiteral( "%1" ).arg( tr( "Message log truncated" ) ) ); else if ( isError || isWarning ) txtLog->append( QStringLiteral( "%2" ).arg( isError ? QStringLiteral( "red" ) : QStringLiteral( "#b85a20" ), escapeHtml ? formatStringForLog( message.toHtmlEscaped() ) : formatStringForLog( message ) ) ); else if ( escapeHtml ) txtLog->append( QStringLiteral( "%1append( QStringLiteral( "%1" ).arg( formatStringForLog( message ) ) ); scrollToBottomOfLog(); processEvents(); } void QgsProcessingAlgorithmDialogBase::reject() { if ( !mAlgorithmTask && isFinalized() ) { setAttribute( Qt::WA_DeleteOnClose ); } QDialog::reject(); } // // QgsProcessingAlgorithmProgressDialog // QgsProcessingAlgorithmProgressDialog::QgsProcessingAlgorithmProgressDialog( QWidget *parent ) : QDialog( parent ) { setupUi( this ); } QProgressBar *QgsProcessingAlgorithmProgressDialog::progressBar() { return mProgressBar; } QPushButton *QgsProcessingAlgorithmProgressDialog::cancelButton() { return mButtonBox->button( QDialogButtonBox::Cancel ); } QTextEdit *QgsProcessingAlgorithmProgressDialog::logTextEdit() { return mTxtLog; } void QgsProcessingAlgorithmProgressDialog::reject() { } // // QgsProcessingContextOptionsWidget // QgsProcessingContextOptionsWidget::QgsProcessingContextOptionsWidget( QWidget *parent ) : QgsPanelWidget( parent ) { setupUi( this ); setPanelTitle( tr( "Algorithm Settings" ) ); mComboInvalidFeatureFiltering->addItem( tr( "Do not Filter (Better Performance)" ), QVariant::fromValue( Qgis::InvalidGeometryCheck::NoCheck ) ); mComboInvalidFeatureFiltering->addItem( tr( "Skip (Ignore) Features with Invalid Geometries" ), QVariant::fromValue( Qgis::InvalidGeometryCheck::SkipInvalid ) ); mComboInvalidFeatureFiltering->addItem( tr( "Stop Algorithm Execution When a Geometry is Invalid" ), QVariant::fromValue( Qgis::InvalidGeometryCheck::AbortOnInvalid ) ); mTemporaryFolderWidget->setDialogTitle( tr( "Select Temporary Directory" ) ); mTemporaryFolderWidget->setStorageMode( QgsFileWidget::GetDirectory ); mTemporaryFolderWidget->lineEdit()->setPlaceholderText( tr( "Default" ) ); mLogLevelComboBox->addItem( tr( "Default" ), static_cast< int >( Qgis::ProcessingLogLevel::DefaultLevel ) ); mLogLevelComboBox->addItem( tr( "Verbose" ), static_cast< int >( Qgis::ProcessingLogLevel::Verbose ) ); mLogLevelComboBox->addItem( tr( "Verbose (Model Debugging)" ), static_cast< int >( Qgis::ProcessingLogLevel::ModelDebug ) ); mDistanceUnitsCombo->addItem( tr( "Default" ), QVariant::fromValue( Qgis::DistanceUnit::Unknown ) ); for ( Qgis::DistanceUnit unit : { Qgis::DistanceUnit::Meters, Qgis::DistanceUnit::Kilometers, Qgis::DistanceUnit::Centimeters, Qgis::DistanceUnit::Millimeters, Qgis::DistanceUnit::Feet, Qgis::DistanceUnit::Miles, Qgis::DistanceUnit::NauticalMiles, Qgis::DistanceUnit::Yards, Qgis::DistanceUnit::Inches, Qgis::DistanceUnit::Degrees, } ) { QString title; if ( ( QgsGui::higFlags() & QgsGui::HigDialogTitleIsTitleCase ) ) { title = QgsStringUtils::capitalize( QgsUnitTypes::toString( unit ), Qgis::Capitalization::TitleCase ); } else { title = QgsUnitTypes::toString( unit ); } mDistanceUnitsCombo->addItem( title, QVariant::fromValue( unit ) ); } mAreaUnitsCombo->addItem( tr( "Default" ), QVariant::fromValue( Qgis::AreaUnit::Unknown ) ); for ( Qgis::AreaUnit unit : { Qgis::AreaUnit::SquareMeters, Qgis::AreaUnit::Hectares, Qgis::AreaUnit::SquareKilometers, Qgis::AreaUnit::SquareCentimeters, Qgis::AreaUnit::SquareMillimeters, Qgis::AreaUnit::SquareFeet, Qgis::AreaUnit::SquareMiles, Qgis::AreaUnit::SquareNauticalMiles, Qgis::AreaUnit::SquareYards, Qgis::AreaUnit::SquareInches, Qgis::AreaUnit::Acres, Qgis::AreaUnit::SquareDegrees, } ) { QString title; if ( ( QgsGui::higFlags() & QgsGui::HigDialogTitleIsTitleCase ) ) { title = QgsStringUtils::capitalize( QgsUnitTypes::toString( unit ), Qgis::Capitalization::TitleCase ); } else { title = QgsUnitTypes::toString( unit ); } mAreaUnitsCombo->addItem( title, QVariant::fromValue( unit ) ); } mThreadsSpinBox->setRange( 1, QThread::idealThreadCount() ); connect( mLogLevelComboBox, qOverload< int >( &QComboBox::currentIndexChanged ), this, &QgsPanelWidget::widgetChanged ); connect( mComboInvalidFeatureFiltering, qOverload< int >( &QComboBox::currentIndexChanged ), this, &QgsPanelWidget::widgetChanged ); connect( mDistanceUnitsCombo, qOverload< int >( &QComboBox::currentIndexChanged ), this, &QgsPanelWidget::widgetChanged ); connect( mAreaUnitsCombo, qOverload< int >( &QComboBox::currentIndexChanged ), this, &QgsPanelWidget::widgetChanged ); connect( mTemporaryFolderWidget, &QgsFileWidget::fileChanged, this, &QgsPanelWidget::widgetChanged ); connect( mThreadsSpinBox, qOverload< int >( &QSpinBox::valueChanged ), this, &QgsPanelWidget::widgetChanged ); } void QgsProcessingContextOptionsWidget::setFromContext( const QgsProcessingContext *context ) { whileBlocking( mComboInvalidFeatureFiltering )->setCurrentIndex( mComboInvalidFeatureFiltering->findData( QVariant::fromValue( context->invalidGeometryCheck() ) ) ); whileBlocking( mDistanceUnitsCombo )->setCurrentIndex( mDistanceUnitsCombo->findData( QVariant::fromValue( context->distanceUnit() ) ) ); whileBlocking( mAreaUnitsCombo )->setCurrentIndex( mAreaUnitsCombo->findData( QVariant::fromValue( context->areaUnit() ) ) ); whileBlocking( mTemporaryFolderWidget )->setFilePath( context->temporaryFolder() ); whileBlocking( mThreadsSpinBox )->setValue( context->maximumThreads() ); whileBlocking( mLogLevelComboBox )->setCurrentIndex( mLogLevelComboBox->findData( static_cast< int >( context->logLevel() ) ) ); } Qgis::InvalidGeometryCheck QgsProcessingContextOptionsWidget::invalidGeometryCheck() const { return mComboInvalidFeatureFiltering->currentData().value< Qgis::InvalidGeometryCheck >(); } Qgis::DistanceUnit QgsProcessingContextOptionsWidget::distanceUnit() const { return mDistanceUnitsCombo->currentData().value< Qgis::DistanceUnit >(); } Qgis::AreaUnit QgsProcessingContextOptionsWidget::areaUnit() const { return mAreaUnitsCombo->currentData().value< Qgis::AreaUnit >(); } QString QgsProcessingContextOptionsWidget::temporaryFolder() { return mTemporaryFolderWidget->filePath(); } int QgsProcessingContextOptionsWidget::maximumThreads() const { return mThreadsSpinBox->value(); } void QgsProcessingContextOptionsWidget::setLogLevel( Qgis::ProcessingLogLevel level ) { whileBlocking( mLogLevelComboBox )->setCurrentIndex( mLogLevelComboBox->findData( static_cast< int >( level ) ) ); } Qgis::ProcessingLogLevel QgsProcessingContextOptionsWidget::logLevel() const { return static_cast< Qgis::ProcessingLogLevel >( mLogLevelComboBox->currentData().toInt() ); } ///@endcond