QGIS API Documentation 3.99.0-Master (2fe06baccd8)
Loading...
Searching...
No Matches
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
19#include <nlohmann/json.hpp>
20
21#include "qgsapplication.h"
22#include "qgscodeeditorjson.h"
23#include "qgscodeeditorpython.h"
24#include "qgscodeeditorshell.h"
25#include "qgsgui.h"
26#include "qgshistoryentry.h"
27#include "qgshistoryentrynode.h"
29#include "qgsjsonutils.h"
31
32#include <QAction>
33#include <QClipboard>
34#include <QFile>
35#include <QMenu>
36#include <QMimeData>
37#include <QRegularExpression>
38#include <QRegularExpressionMatch>
39#include <QTextStream>
40
41#include "moc_qgsprocessinghistoryprovider.cpp"
42
46
48{
49 return QStringLiteral( "processing" );
50}
51
53{
54 const QString logPath = oldLogPath();
55 if ( !QFile::exists( logPath ) )
56 return;
57
58 QFile logFile( logPath );
59 if ( logFile.open( QIODevice::ReadOnly ) )
60 {
61 QTextStream in( &logFile );
62 QList<QgsHistoryEntry> entries;
63 while ( !in.atEnd() )
64 {
65 const QString line = in.readLine().trimmed();
66 QStringList parts = line.split( QStringLiteral( "|~|" ) );
67 if ( parts.size() <= 1 )
68 parts = line.split( '|' );
69
70 if ( parts.size() == 3 && parts.at( 0 ).startsWith( QLatin1String( "ALGORITHM" ), Qt::CaseInsensitive ) )
71 {
72 QVariantMap details;
73 details.insert( QStringLiteral( "python_command" ), parts.at( 2 ) );
74
75 const thread_local QRegularExpression algIdRegEx( QStringLiteral( "processing\\.run\\(\"(.*?)\"" ) );
76 const QRegularExpressionMatch match = algIdRegEx.match( parts.at( 2 ) );
77 if ( match.hasMatch() )
78 details.insert( QStringLiteral( "algorithm_id" ), match.captured( 1 ) );
79
80 entries.append( QgsHistoryEntry( id(), QDateTime::fromString( parts.at( 1 ), QStringLiteral( "yyyy-MM-d hh:mm:ss" ) ), details ) );
81 }
82 }
83
85 }
86}
87
89
90
91class ProcessingHistoryBaseNode : public QgsHistoryEntryGroup
92{
93 public:
94 ProcessingHistoryBaseNode( const QgsHistoryEntry &entry, QgsProcessingHistoryProvider *provider )
95 : mEntry( entry )
96 , mAlgorithmId( mEntry.entry.value( "algorithm_id" ).toString() )
97 , mPythonCommand( mEntry.entry.value( "python_command" ).toString() )
98 , mProcessCommand( mEntry.entry.value( "process_command" ).toString() )
99 , mProvider( provider )
100 {
101 const QVariant parameters = mEntry.entry.value( QStringLiteral( "parameters" ) );
102 if ( parameters.userType() == QMetaType::Type::QVariantMap )
103 {
104 const QVariantMap parametersMap = parameters.toMap();
105 mInputs = parametersMap.value( QStringLiteral( "inputs" ) ).toMap();
106 }
107 }
108
109 bool doubleClicked( const QgsHistoryWidgetContext & ) override
110 {
111 if ( mPythonCommand.isEmpty() )
112 return true;
113
114 QString execAlgorithmDialogCommand = mPythonCommand;
115 execAlgorithmDialogCommand.replace( QLatin1String( "processing.run(" ), QLatin1String( "processing.execAlgorithmDialog(" ) );
116
117 // adding to this list? Also update the BatchPanel.py imports!!
118 const QStringList script = {
119 QStringLiteral( "import processing" ),
120 QStringLiteral( "from qgis.core import QgsProcessingOutputLayerDefinition, QgsProcessingFeatureSourceDefinition, QgsProperty, QgsCoordinateReferenceSystem, QgsFeatureRequest" ),
121 QStringLiteral( "from qgis.PyQt.QtCore import QDate, QTime, QDateTime" ),
122 QStringLiteral( "from qgis.PyQt.QtGui import QColor" ),
123 execAlgorithmDialogCommand
124 };
125
126 mProvider->emitExecute( script.join( '\n' ) );
127 return true;
128 }
129
130 void populateContextMenu( QMenu *menu, const QgsHistoryWidgetContext & ) override
131 {
132 if ( !mPythonCommand.isEmpty() )
133 {
134 QAction *pythonAction = new QAction(
135 QObject::tr( "Copy as Python Command" ), menu
136 );
137 pythonAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconPythonFile.svg" ) ) );
138 QObject::connect( pythonAction, &QAction::triggered, menu, [this] {
139 copyText( mPythonCommand );
140 } );
141 menu->addAction( pythonAction );
142 }
143 if ( !mProcessCommand.isEmpty() )
144 {
145 QAction *processAction = new QAction(
146 QObject::tr( "Copy as qgis_process Command" ), menu
147 );
148 processAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionTerminal.svg" ) ) );
149 QObject::connect( processAction, &QAction::triggered, menu, [this] {
150 copyText( mProcessCommand );
151 } );
152 menu->addAction( processAction );
153 }
154 if ( !mInputs.isEmpty() )
155 {
156 QAction *inputsAction = new QAction(
157 QObject::tr( "Copy as JSON" ), menu
158 );
159 inputsAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionEditCopy.svg" ) ) );
160 QObject::connect( inputsAction, &QAction::triggered, menu, [this] {
161 copyText( QString::fromStdString( QgsJsonUtils::jsonFromVariant( mInputs ).dump( 2 ) ) );
162 } );
163 menu->addAction( inputsAction );
164 }
165
166 if ( !mPythonCommand.isEmpty() )
167 {
168 if ( !menu->isEmpty() )
169 {
170 menu->addSeparator();
171 }
172
173 QAction *createTestAction = new QAction(
174 QObject::tr( "Create Test…" ), menu
175 );
176 QObject::connect( createTestAction, &QAction::triggered, menu, [this] {
177 mProvider->emitCreateTest( mPythonCommand );
178 } );
179 menu->addAction( createTestAction );
180 }
181 }
182
183 void copyText( const QString &text )
184 {
185 QMimeData *m = new QMimeData();
186 m->setText( text );
187 QApplication::clipboard()->setMimeData( m );
188 }
189
190 QgsHistoryEntry mEntry;
191 QString mAlgorithmId;
192 QString mPythonCommand;
193 QString mProcessCommand;
194 QVariantMap mInputs;
195
196 QgsProcessingHistoryProvider *mProvider = nullptr;
197};
198
199class ProcessingHistoryPythonCommandNode : public ProcessingHistoryBaseNode
200{
201 public:
202 ProcessingHistoryPythonCommandNode( const QgsHistoryEntry &entry, QgsProcessingHistoryProvider *provider )
203 : ProcessingHistoryBaseNode( entry, provider )
204 {}
205
206 QVariant data( int role = Qt::DisplayRole ) const override
207 {
208 switch ( role )
209 {
210 case Qt::DisplayRole:
211 {
212 QString display = mPythonCommand;
213 if ( display.length() > 300 )
214 {
215 display = QObject::tr( "%1…" ).arg( display.left( 299 ) );
216 }
217 return display;
218 }
219 case Qt::DecorationRole:
220 return QgsApplication::getThemeIcon( QStringLiteral( "mIconPythonFile.svg" ) );
221
222 default:
223 break;
224 }
225 return QVariant();
226 }
227
228 QWidget *createWidget( const QgsHistoryWidgetContext & ) override
229 {
230 QgsCodeEditorPython *codeEditor = new QgsCodeEditorPython();
231 codeEditor->setReadOnly( true );
232 codeEditor->setCaretLineVisible( false );
233 codeEditor->setLineNumbersVisible( false );
234 codeEditor->setFoldingVisible( false );
235 codeEditor->setEdgeMode( QsciScintilla::EdgeNone );
236 codeEditor->setWrapMode( QsciScintilla::WrapMode::WrapWord );
237
238
239 const QString introText = QStringLiteral( "\"\"\"\n%1\n\"\"\"\n\n " ).arg( QObject::tr( "Double-click on the history item or paste the command below to re-run the algorithm" ) );
240 codeEditor->setText( introText + mPythonCommand );
241
242 return codeEditor;
243 }
244};
245
246class ProcessingHistoryProcessCommandNode : public ProcessingHistoryBaseNode
247{
248 public:
249 ProcessingHistoryProcessCommandNode( const QgsHistoryEntry &entry, QgsProcessingHistoryProvider *provider )
250 : ProcessingHistoryBaseNode( entry, provider )
251 {}
252
253 QVariant data( int role = Qt::DisplayRole ) const override
254 {
255 switch ( role )
256 {
257 case Qt::DisplayRole:
258 {
259 QString display = mProcessCommand;
260 if ( display.length() > 300 )
261 {
262 display = QObject::tr( "%1…" ).arg( display.left( 299 ) );
263 }
264 return display;
265 }
266 case Qt::DecorationRole:
267 return QgsApplication::getThemeIcon( QStringLiteral( "mActionTerminal.svg" ) );
268
269 default:
270 break;
271 }
272 return QVariant();
273 }
274
275 QWidget *createWidget( const QgsHistoryWidgetContext & ) override
276 {
277 QgsCodeEditorShell *codeEditor = new QgsCodeEditorShell();
278 codeEditor->setReadOnly( true );
279 codeEditor->setCaretLineVisible( false );
280 codeEditor->setLineNumbersVisible( false );
281 codeEditor->setFoldingVisible( false );
282 codeEditor->setEdgeMode( QsciScintilla::EdgeNone );
283 codeEditor->setWrapMode( QsciScintilla::WrapMode::WrapWord );
284
285 codeEditor->setText( mProcessCommand );
286
287 return codeEditor;
288 }
289};
290
291
292class ProcessingHistoryJsonNode : public ProcessingHistoryBaseNode
293{
294 public:
295 ProcessingHistoryJsonNode( const QgsHistoryEntry &entry, QgsProcessingHistoryProvider *provider )
296 : ProcessingHistoryBaseNode( entry, provider )
297 {
298 mJson = QString::fromStdString( QgsJsonUtils::jsonFromVariant( mInputs ).dump( 2 ) );
299 mJsonSingleLine = QString::fromStdString( QgsJsonUtils::jsonFromVariant( mInputs ).dump() );
300 }
301
302 QVariant data( int role = Qt::DisplayRole ) const override
303 {
304 switch ( role )
305 {
306 case Qt::DisplayRole:
307 {
308 QString display = mJsonSingleLine;
309 if ( display.length() > 300 )
310 {
311 display = QObject::tr( "%1…" ).arg( display.left( 299 ) );
312 }
313 return display;
314 }
315 case Qt::DecorationRole:
316 return QgsApplication::getThemeIcon( QStringLiteral( "mIconFieldJson.svg" ) );
317
318 default:
319 break;
320 }
321 return QVariant();
322 }
323
324 QWidget *createWidget( const QgsHistoryWidgetContext & ) override
325 {
326 QgsCodeEditorJson *codeEditor = new QgsCodeEditorJson();
327 codeEditor->setReadOnly( true );
328 codeEditor->setCaretLineVisible( false );
329 codeEditor->setLineNumbersVisible( false );
330 codeEditor->setFoldingVisible( false );
331 codeEditor->setEdgeMode( QsciScintilla::EdgeNone );
332 codeEditor->setWrapMode( QsciScintilla::WrapMode::WrapWord );
333
334 codeEditor->setText( mJson );
335
336 return codeEditor;
337 }
338
339 QString mJson;
340 QString mJsonSingleLine;
341};
342
343
344class ProcessingHistoryRootNode : public ProcessingHistoryBaseNode
345{
346 public:
347 ProcessingHistoryRootNode( const QgsHistoryEntry &entry, QgsProcessingHistoryProvider *provider )
348 : ProcessingHistoryBaseNode( entry, provider )
349 {
350 const QVariant parameters = mEntry.entry.value( QStringLiteral( "parameters" ) );
351 if ( parameters.type() == QVariant::Map )
352 {
353 mDescription = QgsProcessingUtils::variantToPythonLiteral( mInputs );
354 }
355 else
356 {
357 // an older history entry which didn't record inputs
358 mDescription = mPythonCommand;
359 }
360
361 if ( mDescription.length() > 300 )
362 {
363 mDescription = QObject::tr( "%1…" ).arg( mDescription.left( 299 ) );
364 }
365
366 addChild( new ProcessingHistoryPythonCommandNode( mEntry, mProvider ) );
367 addChild( new ProcessingHistoryProcessCommandNode( mEntry, mProvider ) );
368 addChild( new ProcessingHistoryJsonNode( mEntry, mProvider ) );
369 }
370
371 void setEntry( const QgsHistoryEntry &entry )
372 {
373 mEntry = entry;
374 }
375
376 QVariant data( int role = Qt::DisplayRole ) const override
377 {
378 if ( mAlgorithmInformation.displayName.isEmpty() )
379 {
380 mAlgorithmInformation = QgsApplication::processingRegistry()->algorithmInformation( mAlgorithmId );
381 }
382
383 switch ( role )
384 {
385 case Qt::DisplayRole:
386 {
387 const QString algName = mAlgorithmInformation.displayName;
388 if ( !mDescription.isEmpty() )
389 return QStringLiteral( "[%1] %2 - %3" ).arg( mEntry.timestamp.toString( QStringLiteral( "yyyy-MM-dd hh:mm" ) ), algName, mDescription );
390 else
391 return QStringLiteral( "[%1] %2" ).arg( mEntry.timestamp.toString( QStringLiteral( "yyyy-MM-dd hh:mm" ) ), algName );
392 }
393
394 case Qt::DecorationRole:
395 {
396 return mAlgorithmInformation.icon;
397 }
398
399 default:
400 break;
401 }
402 return QVariant();
403 }
404
405 QString html( const QgsHistoryWidgetContext & ) const override
406 {
407 return mEntry.entry.value( QStringLiteral( "log" ) ).toString();
408 }
409
410 QString mDescription;
411 mutable QgsProcessingAlgorithmInformation mAlgorithmInformation;
412};
413
415
417{
418 return new ProcessingHistoryRootNode( entry, this );
419}
420
422{
423 if ( ProcessingHistoryRootNode *rootNode = dynamic_cast<ProcessingHistoryRootNode *>( node ) )
424 {
425 rootNode->setEntry( entry );
426 }
427}
428
429QString QgsProcessingHistoryProvider::oldLogPath() const
430{
431 const QString userDir = QgsApplication::qgisSettingsDirPath() + QStringLiteral( "/processing" );
432 return userDir + QStringLiteral( "/processing.log" );
433}
434
435void QgsProcessingHistoryProvider::emitExecute( const QString &commands )
436{
437 emit executePython( commands );
438}
439
440void QgsProcessingHistoryProvider::emitCreateTest( const QString &command )
441{
442 emit createTest( command );
443}
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.
void setText(const QString &text) override
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:211
Base class for history entry "group" nodes, which contain children of their own.
Base class for nodes representing a QgsHistoryEntry.
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.
History provider for operations performed through the Processing framework.
void updateNodeForEntry(QgsHistoryEntryNode *node, const QgsHistoryEntry &entry, const QgsHistoryWidgetContext &context) override
Updates an existing history node for the given entry.
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.