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