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