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