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