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