QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
qgsprocessinghistoryprovider.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsprocessinghistoryprovider.cpp
3 -------------------------
4 begin : December 2021
5 copyright : (C) 2021 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8/***************************************************************************
9 * *
10 * This program is free software; you can redistribute it and/or modify *
11 * it under the terms of the GNU General Public License as published by *
12 * the Free Software Foundation; either version 2 of the License, or *
13 * (at your option) any later version. *
14 * *
15 ***************************************************************************/
16
18#include "qgsapplication.h"
19#include "qgsgui.h"
21#include "qgshistoryentry.h"
22#include "qgshistoryentrynode.h"
24#include "qgscodeeditorpython.h"
25#include "qgsjsonutils.h"
26
27#include <nlohmann/json.hpp>
28#include <QFile>
29#include <QTextStream>
30#include <QRegularExpression>
31#include <QRegularExpressionMatch>
32#include <QAction>
33#include <QMenu>
34#include <QMimeData>
35#include <QClipboard>
36
38{
39}
40
42{
43 return QStringLiteral( "processing" );
44}
45
47{
48 const QString logPath = oldLogPath();
49 if ( !QFile::exists( logPath ) )
50 return;
51
52 QFile logFile( logPath );
53 if ( logFile.open( QIODevice::ReadOnly ) )
54 {
55 QTextStream in( &logFile );
56 QList< QgsHistoryEntry > entries;
57 while ( !in.atEnd() )
58 {
59 const QString line = in.readLine().trimmed();
60 QStringList parts = line.split( QStringLiteral( "|~|" ) );
61 if ( parts.size() <= 1 )
62 parts = line.split( '|' );
63
64 if ( parts.size() == 3 && parts.at( 0 ).startsWith( QLatin1String( "ALGORITHM" ), Qt::CaseInsensitive ) )
65 {
66 QVariantMap details;
67 details.insert( QStringLiteral( "python_command" ), parts.at( 2 ) );
68
69 const thread_local QRegularExpression algIdRegEx( QStringLiteral( "processing\\.run\\(\"(.*?)\"" ) );
70 const QRegularExpressionMatch match = algIdRegEx.match( parts.at( 2 ) );
71 if ( match.hasMatch() )
72 details.insert( QStringLiteral( "algorithm_id" ), match.captured( 1 ) );
73
74 entries.append( QgsHistoryEntry( id(),
75 QDateTime::fromString( parts.at( 1 ), QStringLiteral( "yyyy-MM-d hh:mm:ss" ) ),
76 details ) );
77 }
78 }
79
81 }
82}
83
85
86class ProcessingHistoryNode : public QgsHistoryEntryGroup
87{
88 public:
89
90 ProcessingHistoryNode( const QgsHistoryEntry &entry, QgsProcessingHistoryProvider *provider )
91 : mEntry( entry )
92 , mAlgorithmId( mEntry.entry.value( "algorithm_id" ).toString() )
93 , mPythonCommand( mEntry.entry.value( "python_command" ).toString() )
94 , mProcessCommand( mEntry.entry.value( "process_command" ).toString() )
95 , mProvider( provider )
96 {
97
98 const QVariant parameters = mEntry.entry.value( QStringLiteral( "parameters" ) );
99 if ( parameters.type() == QVariant::Map )
100 {
101 const QVariantMap parametersMap = parameters.toMap();
102 mInputs = parametersMap.value( QStringLiteral( "inputs" ) ).toMap();
103 mDescription = QgsProcessingUtils::variantToPythonLiteral( mInputs );
104 }
105 else
106 {
107 // an older history entry which didn't record inputs
108 mDescription = mPythonCommand;
109 }
110
111 if ( mDescription.length() > 300 )
112 {
113 mDescription = QObject::tr( "%1…" ).arg( mDescription.left( 299 ) );
114 }
115 }
116
117 QVariant data( int role = Qt::DisplayRole ) const override
118 {
119 if ( mAlgorithmInformation.displayName.isEmpty() )
120 {
121 mAlgorithmInformation = QgsApplication::processingRegistry()->algorithmInformation( mAlgorithmId );
122 }
123
124 switch ( role )
125 {
126 case Qt::DisplayRole:
127 {
128 const QString algName = mAlgorithmInformation.displayName;
129 if ( !mDescription.isEmpty() )
130 return QStringLiteral( "[%1] %2 - %3" ).arg( mEntry.timestamp.toString( QStringLiteral( "yyyy-MM-dd hh:mm" ) ),
131 algName,
132 mDescription );
133 else
134 return QStringLiteral( "[%1] %2" ).arg( mEntry.timestamp.toString( QStringLiteral( "yyyy-MM-dd hh:mm" ) ),
135 algName );
136 }
137
138 case Qt::DecorationRole:
139 {
140 return mAlgorithmInformation.icon;
141 }
142 }
143 return QVariant();
144 }
145
146 QWidget *createWidget( const QgsHistoryWidgetContext & ) override
147 {
148 QgsCodeEditorPython *codeEditor = new QgsCodeEditorPython( );
149 codeEditor->setReadOnly( true );
150 codeEditor->setCaretLineVisible( false );
151 codeEditor->setLineNumbersVisible( false );
152 codeEditor->setFoldingVisible( false );
153 codeEditor->setEdgeMode( QsciScintilla::EdgeNone );
154 codeEditor->setWrapMode( QsciScintilla::WrapMode::WrapWord );
155
156
157 const QString introText = QStringLiteral( "\"\"\"\n%1\n\"\"\"\n\n " ).arg(
158 QObject::tr( "Double-click on the history item or paste the command below to re-run the algorithm" ) );
159 codeEditor->setText( introText + mPythonCommand );
160
161 return codeEditor;
162 }
163
164 bool doubleClicked( const QgsHistoryWidgetContext & ) override
165 {
166 if ( mPythonCommand.isEmpty() )
167 return true;
168
169 QString execAlgorithmDialogCommand = mPythonCommand;
170 execAlgorithmDialogCommand.replace( QLatin1String( "processing.run(" ), QLatin1String( "processing.execAlgorithmDialog(" ) );
171
172 // adding to this list? Also update the BatchPanel.py imports!!
173 const QStringList script =
174 {
175 QStringLiteral( "import processing" ),
176 QStringLiteral( "from qgis.core import QgsProcessingOutputLayerDefinition, QgsProcessingFeatureSourceDefinition, QgsProperty, QgsCoordinateReferenceSystem, QgsFeatureRequest" ),
177 QStringLiteral( "from qgis.PyQt.QtCore import QDate, QTime, QDateTime" ),
178 QStringLiteral( "from qgis.PyQt.QtGui import QColor" ),
179 execAlgorithmDialogCommand
180 };
181
182 mProvider->emitExecute( script.join( '\n' ) );
183 return true;
184 }
185
186 void populateContextMenu( QMenu *menu, const QgsHistoryWidgetContext & ) override
187 {
188 if ( !mPythonCommand.isEmpty() )
189 {
190 QAction *pythonAction = new QAction(
191 QObject::tr( "Copy as Python Command" ), menu );
192 pythonAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconPythonFile.svg" ) ) );
193 QObject::connect( pythonAction, &QAction::triggered, menu, [ = ]
194 {
195 copyText( mPythonCommand );
196 } );
197 menu->addAction( pythonAction );
198 }
199 if ( !mProcessCommand.isEmpty() )
200 {
201 QAction *processAction = new QAction(
202 QObject::tr( "Copy as qgis_process Command" ), menu );
203 processAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionTerminal.svg" ) ) );
204 QObject::connect( processAction, &QAction::triggered, menu, [ = ]
205 {
206 copyText( mProcessCommand );
207 } );
208 menu->addAction( processAction );
209 }
210 if ( !mInputs.isEmpty() )
211 {
212 QAction *inputsAction = new QAction(
213 QObject::tr( "Copy as JSON" ), menu );
214 inputsAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionEditCopy.svg" ) ) );
215 QObject::connect( inputsAction, &QAction::triggered, menu, [ = ]
216 {
217 copyText( QString::fromStdString( QgsJsonUtils::jsonFromVariant( mInputs ).dump( 2 ) ) );
218 } );
219 menu->addAction( inputsAction );
220 }
221
222 if ( !mPythonCommand.isEmpty() )
223 {
224 if ( !menu->isEmpty() )
225 {
226 menu->addSeparator();
227 }
228
229 QAction *createTestAction = new QAction(
230 QObject::tr( "Create Test…" ), menu );
231 QObject::connect( createTestAction, &QAction::triggered, menu, [ = ]
232 {
233 mProvider->emitCreateTest( mPythonCommand );
234 } );
235 menu->addAction( createTestAction );
236 }
237 }
238
239 void copyText( const QString &text )
240 {
241 QMimeData *m = new QMimeData();
242 m->setText( text );
243 QApplication::clipboard()->setMimeData( m );
244 }
245
246 QgsHistoryEntry mEntry;
247 QString mAlgorithmId;
248 QString mPythonCommand;
249 QString mProcessCommand;
250 QVariantMap mInputs;
251 QString mDescription;
252
253 mutable QgsProcessingAlgorithmInformation mAlgorithmInformation;
254 QgsProcessingHistoryProvider *mProvider = nullptr;
255
256};
257
259
261{
262 return new ProcessingHistoryNode( entry, this );
263}
264
265QString QgsProcessingHistoryProvider::oldLogPath() const
266{
267 const QString userDir = QgsApplication::qgisSettingsDirPath() + QStringLiteral( "/processing" );
268 return userDir + QStringLiteral( "/processing.log" );
269}
270
271void QgsProcessingHistoryProvider::emitExecute( const QString &commands )
272{
273 emit executePython( commands );
274}
275
276void QgsProcessingHistoryProvider::emitCreateTest( const QString &command )
277{
278 emit createTest( command );
279}
static QgsProcessingRegistry * processingRegistry()
Returns the application's processing registry, used for managing processing providers,...
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
static QString qgisSettingsDirPath()
Returns the path to the settings directory in user's home dir.
A Python editor based on QScintilla2.
void setFoldingVisible(bool folding)
Set whether the folding controls are visible in the editor.
void setLineNumbersVisible(bool visible)
Sets whether line numbers should be visible in the editor.
static QgsHistoryProviderRegistry * historyProviderRegistry()
Returns the global history provider registry, used for tracking history providers.
Definition: qgsgui.cpp:184
Base class for history entry "group" nodes, which contain children of their own.
Base class for nodes representing a QgsHistoryEntry.
virtual QWidget * createWidget(const QgsHistoryWidgetContext &context)
Returns a new widget which should be shown to users when selecting the node.
virtual QVariant data(int role=Qt::DisplayRole) const =0
Returns the node's data for the specified model role.
virtual void populateContextMenu(QMenu *menu, const QgsHistoryWidgetContext &context)
Allows the node to populate a context menu before display to the user.
virtual bool doubleClicked(const QgsHistoryWidgetContext &context)
Called when the node is double-clicked.
Encapsulates a history entry.
bool addEntries(const QList< QgsHistoryEntry > &entries, QgsHistoryProviderRegistry::HistoryEntryOptions options=QgsHistoryProviderRegistry::HistoryEntryOptions())
Adds a list of entries to the history logs.
Contains settings which reflect the context in which a history widget is shown, e....
static json jsonFromVariant(const QVariant &v)
Converts a QVariant v to a json object.
Contains basic properties for a Processing algorithm.
QString displayName
Algorithm display name.
History provider for operations performed through the Processing framework.
QString id() const override
Returns the provider's unique id, which is used to associate existing history entries with the provid...
void executePython(const QString &commands)
Emitted when the provider needs to execute python commands in the Processing context.
QgsHistoryEntryNode * createNodeForEntry(const QgsHistoryEntry &entry, const QgsHistoryWidgetContext &context) override
Creates a new history node for the given entry.
void createTest(const QString &command)
Emitted when the provider needs to create a Processing test with the given python command.
void portOldLog()
Ports the old text log to the history framework.
QgsProcessingAlgorithmInformation algorithmInformation(const QString &id) const
Returns basic algorithm information for the algorithm with matching ID.
static QString variantToPythonLiteral(const QVariant &value)
Converts a variant to a Python literal.