QGIS API Documentation 4.3.0-Master (c8cdfe19d7b)
Loading...
Searching...
No Matches
qgsattributesformview.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsattributesformview.cpp
3 ---------------------
4 begin : June 2025
5 copyright : (C) 2025 by Germán Carrillo
6 email : german at opengis dot ch
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
17
18#include "qgsapplication.h"
20#include "qgsattributetypedialog.h"
22#include "qgscodeeditorhtml.h"
25#include "qgsexpressionfinder.h"
26#include "qgsgui.h"
28#include "qgsqmlwidgetwrapper.h"
29#include "qgsscrollarea.h"
31
32#include <QAction>
33#include <QClipboard>
34#include <QDropEvent>
35#include <QFileDialog>
36#include <QFormLayout>
37#include <QHBoxLayout>
38#include <QMenu>
39#include <QMessageBox>
40#include <QMimeData>
41#include <QPlainTextEdit>
42#include <QPushButton>
43#include <QSpacerItem>
44#include <QSpinBox>
45#include <QString>
46#include <QTreeView>
47#include <QWidget>
48
49#include "moc_qgsattributesformview.cpp"
50
51using namespace Qt::StringLiterals;
52
54 : QTreeView( parent )
55 , mLayer( layer )
56{}
57
59{
60 if ( selectionModel()->selectedRows( 0 ).count() == 0 )
61 return QModelIndex();
62
63 return mModel->mapToSource( selectionModel()->selectedRows( 0 ).at( 0 ) );
64}
65
77
79{
80 // To be used with Relations, fields and actions
81 const auto *model = static_cast< QgsAttributesFormModel * >( mModel->sourceModel() );
82 QModelIndex index = mModel->mapFromSource( model->firstRecursiveMatchingModelIndex( itemType, itemId ) );
83
84 if ( index.isValid() )
85 {
86 // Check if selection is index is already selected (e.g., duplicate fields in
87 // form layout, and selecting one after the other), otherwise set new selection
88 const QModelIndexList selectedIndexes = selectionModel()->selectedRows( 0 );
89 if ( !( selectedIndexes.count() == 1 && selectedIndexes.at( 0 ) == index ) )
90 {
91 selectionModel()->setCurrentIndex( index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows );
92 }
93 }
94 else
95 {
96 selectionModel()->clearSelection();
97 }
98}
99
101{
102 mModel->setFilterText( text );
103}
104
105const QList<QgsAttributesFormTreeViewIndicator *> QgsAttributesFormBaseView::indicators( const QModelIndex &index ) const
106{
107 QgsAttributesFormItem *item = static_cast< QgsAttributesFormModel *>( mModel->sourceModel() )->itemForIndex( mModel->mapToSource( index ) );
108 return mIndicators.value( item );
109}
110
111const QList<QgsAttributesFormTreeViewIndicator *> QgsAttributesFormBaseView::indicators( QgsAttributesFormItem *item ) const
112{
113 return mIndicators.value( item );
114}
115
117{
118 if ( !mIndicators[item].contains( indicator ) )
119 {
120 mIndicators[item].append( indicator );
121 connect( indicator, &QgsAttributesFormTreeViewIndicator::changed, this, [this] {
122 update();
123 viewport()->repaint();
124 } );
125 update();
126 viewport()->repaint(); //update() does not automatically trigger a repaint()
127 }
128}
129
131{
132 mIndicators[item].removeOne( indicator );
133 update();
134}
135
137{
138 const QList<QgsAttributesFormItem *> keys = mIndicators.keys();
139 for ( QgsAttributesFormItem *key : keys )
140 {
141 qDeleteAll( mIndicators[key] );
142 mIndicators[key].clear();
143 }
144 mIndicators.clear();
145 update();
146}
147
149{
150 return mModel->sourceAttributesFormModel();
151}
152
153
157
158void QgsAttributesAvailableWidgetsView::setModel( QAbstractItemModel *model )
159{
160 mModel = qobject_cast<QgsAttributesFormProxyModel *>( model );
161 if ( !mModel )
162 return;
163
164 QTreeView::setModel( mModel );
165}
166
171
172
174 : QgsAttributesFormBaseView( layer, parent )
175{
176 connect( this, &QTreeView::doubleClicked, this, &QgsAttributesFormLayoutView::onItemDoubleClicked );
177}
178
179void QgsAttributesFormLayoutView::setModel( QAbstractItemModel *model )
180{
181 mModel = qobject_cast<QgsAttributesFormProxyModel *>( model );
182 if ( !mModel )
183 return;
184
185 QTreeView::setModel( mModel );
186
187 const auto *formLayoutModel = static_cast< QgsAttributesFormLayoutModel * >( mModel->sourceModel() );
188 connect( formLayoutModel, &QgsAttributesFormLayoutModel::externalItemsDropped, this, &QgsAttributesFormLayoutView::handleExternalDroppedItems );
189 connect( formLayoutModel, &QgsAttributesFormLayoutModel::internalItemsDropped, this, &QgsAttributesFormLayoutView::handleInternalDroppedItems );
190}
191
192
193void QgsAttributesFormLayoutView::selectDroppedItems( const QModelIndexList &indexes )
194{
195 if ( indexes.isEmpty() )
196 return;
197
198 QItemSelection selection;
199 for ( const QModelIndex &index : indexes )
200 {
201 const QModelIndex proxyIndex = mModel->mapFromSource( index );
202 selection.select( proxyIndex, proxyIndex );
203 }
204
205 // Set the current index first without touching the selection, so that the
206 // selection is changed once, keeping all dropped items selected
207 selectionModel()->setCurrentIndex( mModel->mapFromSource( indexes.constLast() ), QItemSelectionModel::NoUpdate );
208 selectionModel()->select( selection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows );
209}
210
211void QgsAttributesFormLayoutView::handleExternalDroppedItems( const QModelIndexList &indexes )
212{
213 selectDroppedItems( indexes );
214
215 for ( const QModelIndex &index : indexes )
216 {
217 const auto itemType = static_cast< QgsAttributesFormData::AttributesFormItemType >( index.data( QgsAttributesFormModel::ItemTypeRole ).toInt() );
218
219 switch ( itemType )
220 {
225 // These need to be configured right after being dropped
226 onItemDoubleClicked( mModel->mapFromSource( index ) );
227 break;
228
234 break;
235 }
236 }
237}
238
239void QgsAttributesFormLayoutView::handleInternalDroppedItems( const QModelIndexList &indexes )
240{
241 selectDroppedItems( indexes );
242
243 // The move resets the model, collapsing the whole tree, so restore the
244 // expanded state captured in dropEvent() before the move took place.
245 const int topLevelRows = mModel->sourceModel()->rowCount( QModelIndex() );
246 for ( int row = 0; row < topLevelRows; ++row )
247 {
248 restoreExpandedState( mModel->sourceModel()->index( row, 0, QModelIndex() ) );
249 }
250 mDraggedExpandedState.clear();
251}
252
253void QgsAttributesFormLayoutView::storeExpandedState( const QModelIndex &sourceIndex )
254{
255 if ( !sourceIndex.isValid() )
256 return;
257
258 if ( auto *item = static_cast< QgsAttributesFormItem * >( sourceIndex.internalPointer() ) )
259 mDraggedExpandedState.insert( item, isExpanded( mModel->mapFromSource( sourceIndex ) ) );
260
261 const int rows = mModel->sourceModel()->rowCount( sourceIndex );
262 for ( int row = 0; row < rows; ++row )
263 {
264 storeExpandedState( mModel->sourceModel()->index( row, 0, sourceIndex ) );
265 }
266}
267
268void QgsAttributesFormLayoutView::restoreExpandedState( const QModelIndex &sourceIndex )
269{
270 if ( !sourceIndex.isValid() )
271 return;
272
273 // Expand parents before children so descendant indexes are correct
274 if ( auto *item = static_cast< QgsAttributesFormItem * >( sourceIndex.internalPointer() ) )
275 setExpanded( mModel->mapFromSource( sourceIndex ), mDraggedExpandedState.value( item, true ) );
276
277 const int rows = mModel->sourceModel()->rowCount( sourceIndex );
278 for ( int row = 0; row < rows; ++row )
279 {
280 restoreExpandedState( mModel->sourceModel()->index( row, 0, sourceIndex ) );
281 }
282}
283
285{
286 const QMimeData *data = event->mimeData();
287
288 if ( data->hasFormat( u"application/x-qgsattributesformavailablewidgetsrelement"_s ) || data->hasFormat( u"application/x-qgsattributesformlayoutelement"_s ) )
289 {
290 // Inner drag and drop actions are always MoveAction
291 if ( event->source() == this )
292 {
293 event->setDropAction( Qt::MoveAction );
294 }
295 }
296 else
297 {
298 event->ignore();
299 }
300
301 QTreeView::dragEnterEvent( event );
302}
303
309{
310 const QMimeData *data = event->mimeData();
311
312 if ( data->hasFormat( u"application/x-qgsattributesformavailablewidgetsrelement"_s ) || data->hasFormat( u"application/x-qgsattributesformlayoutelement"_s ) )
313 {
314 // Inner drag and drop actions are always MoveAction
315 if ( event->source() == this )
316 {
317 event->setDropAction( Qt::MoveAction );
318 }
319 }
320 else
321 {
322 event->ignore();
323 }
324
325 QTreeView::dragMoveEvent( event );
326}
327
329{
330 if ( !( event->mimeData()->hasFormat( u"application/x-qgsattributesformavailablewidgetsrelement"_s ) || event->mimeData()->hasFormat( u"application/x-qgsattributesformlayoutelement"_s ) ) )
331 return;
332
333 const bool internalMove = event->source() == this && event->mimeData()->hasFormat( u"application/x-qgsattributesformlayoutelement"_s );
334
335 if ( internalMove )
336 {
337 // Capture the expanded state of the whole tree now, before the (deferred)
338 // move resets the model, which collapses everything. It is restored
339 // afterwards in handleInternalDroppedItems().
340 mDraggedExpandedState.clear();
341 const int topLevelRows = mModel->sourceModel()->rowCount( QModelIndex() );
342 for ( int row = 0; row < topLevelRows; ++row )
343 {
344 storeExpandedState( mModel->sourceModel()->index( row, 0, QModelIndex() ) );
345 }
346 }
347
348 if ( event->source() == this )
349 {
350 event->setDropAction( Qt::MoveAction );
351 }
352
353 QTreeView::dropEvent( event );
354
355 if ( internalMove && event->isAccepted() )
356 {
357 // Reporting a CopyAction makes QAbstractItemView::startDrag() skip its clearOrRemove() step.
358 event->setDropAction( Qt::CopyAction );
359 event->accept();
360 }
361}
362
363void QgsAttributesFormLayoutView::onItemDoubleClicked( const QModelIndex &index )
364{
365 QModelIndex sourceIndex = mModel->mapToSource( index );
367 const auto itemType = static_cast<QgsAttributesFormData::AttributesFormItemType>( sourceIndex.data( QgsAttributesFormModel::ItemTypeRole ).toInt() );
368 const QString itemName = sourceIndex.data( QgsAttributesFormModel::ItemNameRole ).toString();
369
370 QGroupBox *baseData = new QGroupBox( tr( "Base configuration" ) );
371
372 QFormLayout *baseLayout = new QFormLayout();
373 baseData->setLayout( baseLayout );
374 QCheckBox *showLabelCheckbox = new QCheckBox( u"Show label"_s );
375 showLabelCheckbox->setChecked( itemData.showLabel() );
376 baseLayout->addRow( showLabelCheckbox );
377 QWidget *baseWidget = new QWidget();
378 baseWidget->setLayout( baseLayout );
379
380 switch ( itemType )
381 {
387 break;
388
390 {
391 QDialog dlg;
392 dlg.setObjectName( "QML Form Configuration Widget" );
394 dlg.setWindowTitle( tr( "Configure QML Widget" ) );
395
396 QVBoxLayout *mainLayout = new QVBoxLayout( &dlg );
397 QSplitter *qmlSplitter = new QSplitter();
398 QWidget *qmlConfigWiget = new QWidget();
399 QVBoxLayout *layout = new QVBoxLayout( qmlConfigWiget );
400 layout->setContentsMargins( 0, 0, 0, 0 );
401 mainLayout->addWidget( qmlSplitter );
402 qmlSplitter->addWidget( qmlConfigWiget );
403 layout->addWidget( baseWidget );
404
405 QLineEdit *title = new QLineEdit( itemName );
406
407 //qmlCode
409 qmlCode->setLineNumbersVisible( true );
410 qmlCode->setEditingTimeoutInterval( 250 );
411 qmlCode->setText( itemData.qmlElementEditorConfiguration().qmlCode );
412
413 QgsQmlWidgetWrapper *qmlWrapper = new QgsQmlWidgetWrapper( mLayer, nullptr, this );
414 QgsFeature previewFeature;
415 mLayer->getFeatures().nextFeature( previewFeature );
416
417 //templates
418 QComboBox *qmlObjectTemplate = new QComboBox();
419 qmlObjectTemplate->addItem( tr( "Attribute Form Interaction" ) );
420 qmlObjectTemplate->addItem( tr( "Rectangle" ) );
421 qmlObjectTemplate->addItem( tr( "Pie Chart" ) );
422 qmlObjectTemplate->addItem( tr( "Bar Chart" ) );
423 QToolButton *objectTemplateButton = new QToolButton();
424 objectTemplateButton->setText( tr( "Load Template" ) );
425 connect( objectTemplateButton, &QAbstractButton::clicked, qmlCode, [qmlCode, qmlObjectTemplate]() {
426 qmlCode->clear();
427 switch ( qmlObjectTemplate->currentIndex() )
428 {
429 case 0:
430 {
431 qmlCode->setText( QStringLiteral(
432 "import QtQuick\n"
433 "import QtQuick.Layouts\n"
434 "import QtQuick.Controls.Material\n"
435 "\n"
436 "Item {\n"
437 " width: Math.max(parent.width || 0, 200)\n"
438 " height: mainLayout.childrenRect.height + mainLayout.anchors.margins * 2\n"
439 "\n"
440 " ColumnLayout {\n"
441 " id: mainLayout\n"
442 " anchors.fill: parent\n"
443 " anchors.margins: 4\n"
444 "\n"
445 " RowLayout {\n"
446 " Text {\n"
447 " // The attributeFormBridge.layer property provides access to the\n"
448 " // vector layer attached to the attribute form\n"
449 " text: \"Current layer name: \" + (attributeFormBridge.layer ? attributeFormBridge.layer.name : \"N/A\")\n"
450 " }\n"
451 " }\n"
452 "\n"
453 " RowLayout {\n"
454 " id: rowLayout\n"
455 " spacing: 4\n"
456 "\n"
457 " TextField {\n"
458 " id: myAttributeTextEdit\n"
459 " Layout.fillWidth: true\n"
460 "\n"
461 " // The attributeFormBridge.editable property reflects the\n"
462 " // editable state of the attribute form\n"
463 " enabled: attributeFormBridge.editable\n"
464 "\n"
465 " // The attributeFormBridge.currentFormFeature provides an up-to-date\n"
466 " // QgsFeature object with all of its properties and invokable functions\n"
467 " text: attributeFormBridge.currentFormFeature.attribute(\"my_attribute\") || \"\"\n"
468 " }\n"
469 "\n"
470 " RoundButton {\n"
471 " text: \"Change\"\n"
472 " background.layer.enabled: false\n"
473 " onClicked: {\n"
474 " // The attributeFormBridge enables the change of field values through\n"
475 " // changeAttribute(field_name, value) as well as the change of geometry\n"
476 " // through changeGeometry(geometry)\n"
477 " attributeFormBridge.changeAttribute(\"my_attribute\", myAttributeTextEdit.text);\n"
478 " }\n"
479 " }\n"
480 " }\n"
481 " }\n"
482 "}\n"
483 ) );
484 break;
485 }
486 case 1:
487 {
488 qmlCode->setText( QStringLiteral(
489 "import QtQuick\n"
490 "\n"
491 "Rectangle {\n"
492 " width: 100\n"
493 " height: 100\n"
494 " color: \"steelblue\"\n"
495 " Text{ text: \"A rectangle\" }\n"
496 "}\n"
497 ) );
498 break;
499 }
500 case 2:
501 {
502 qmlCode->setText( QStringLiteral(
503 "import QtQuick\n"
504 "import QtCharts\n"
505 "\n"
506 "ChartView {\n"
507 " width: 400\n"
508 " height: 400\n"
509 "\n"
510 " PieSeries {\n"
511 " id: pieSeries\n"
512 " PieSlice { label: \"First slice\"; value: 25 }\n"
513 " PieSlice { label: \"Second slice\"; value: 45 }\n"
514 " PieSlice { label: \"Third slice\"; value: 30 }\n"
515 " }\n"
516 "}\n"
517 ) );
518 break;
519 }
520 case 3:
521 {
522 qmlCode->setText( QStringLiteral(
523 "import QtQuick\n"
524 "import QtCharts\n"
525 "\n"
526 "ChartView {\n"
527 " title: \"Bar series\"\n"
528 " width: 600\n"
529 " height:400\n"
530 " legend.alignment: Qt.AlignBottom\n"
531 " antialiasing: true\n"
532 " ValueAxis{\n"
533 " id: valueAxisY\n"
534 " min: 0\n"
535 " max: 15\n"
536 " }\n"
537 "\n"
538 " BarSeries {\n"
539 " id: mySeries\n"
540 " axisY: valueAxisY\n"
541 " axisX: BarCategoryAxis { categories: [\"2007\", \"2008\", \"2009\", \"2010\", \"2011\", \"2012\" ] }\n"
542 " BarSet { label: \"Bob\"; values: [2, 2, 3, 4, 5, 6] }\n"
543 " BarSet { label: \"Susan\"; values: [5, 1, 2, 4, 1, 7] }\n"
544 " BarSet { label: \"James\"; values: [3, 5, 8, 13, 5, 8] }\n"
545 " }\n"
546 "}\n"
547 ) );
548 break;
549 }
550 default:
551 break;
552 }
553 } );
554
555 QgsFieldExpressionWidget *expressionWidget = new QgsFieldExpressionWidget;
556 expressionWidget->setButtonVisible( false );
557 expressionWidget->registerExpressionContextGenerator( this );
558 expressionWidget->setLayer( mLayer );
559 QToolButton *addFieldButton = new QToolButton();
560 addFieldButton->setIcon( QgsApplication::getThemeIcon( u"/symbologyAdd.svg"_s ) );
561
562 QToolButton *editExpressionButton = new QToolButton();
563 editExpressionButton->setIcon( QgsApplication::getThemeIcon( u"/mIconExpression.svg"_s ) );
564 editExpressionButton->setToolTip( tr( "Insert/Edit Expression" ) );
565
566 connect( addFieldButton, &QAbstractButton::clicked, this, [expressionWidget, qmlCode] {
567 QString expression = expressionWidget->expression().trimmed().replace( '"', "\\\""_L1 );
568 if ( !expression.isEmpty() )
569 qmlCode->insertText( u"expression.evaluate(\"%1\")"_s.arg( expression ) );
570 } );
571
572 connect( editExpressionButton, &QAbstractButton::clicked, this, [this, qmlCode] {
573 QString expression = QgsExpressionFinder::findAndSelectActiveExpression( qmlCode, u"expression\\.evaluate\\(\\s*\"(.*?)\\s*\"\\s*\\)"_s );
574 expression.replace( "\\\""_L1, "\""_L1 );
575 QgsExpressionContext context = createExpressionContext();
576 QgsExpressionBuilderDialog exprDlg( mLayer, expression, this, u"generic"_s, context );
577
578 exprDlg.setWindowTitle( tr( "Insert Expression" ) );
579 if ( exprDlg.exec() == QDialog::Accepted && !exprDlg.expressionText().trimmed().isEmpty() )
580 {
581 QString expression = exprDlg.expressionText().trimmed().replace( '"', "\\\""_L1 );
582 if ( !expression.isEmpty() )
583 qmlCode->insertText( u"expression.evaluate(\"%1\")"_s.arg( expression ) );
584 }
585 } );
586
587 layout->addWidget( new QLabel( tr( "Title" ) ) );
588 layout->addWidget( title );
589
590 QGroupBox *qmlCodeBox = new QGroupBox( tr( "QML Code" ) );
591 qmlCodeBox->setLayout( new QVBoxLayout() );
592
593 QWidget *qmlObjectTemplateBox = new QWidget();
594 qmlCodeBox->layout()->addWidget( qmlObjectTemplateBox );
595 qmlObjectTemplateBox->setLayout( new QHBoxLayout() );
596 qmlObjectTemplateBox->layout()->setContentsMargins( 0, 0, 0, 0 );
597 qmlObjectTemplateBox->layout()->addWidget( qmlObjectTemplate );
598 qmlObjectTemplateBox->layout()->addWidget( objectTemplateButton );
599
600 QWidget *expressionWidgetBox = new QWidget();
601 qmlCodeBox->layout()->addWidget( expressionWidgetBox );
602 expressionWidgetBox->setLayout( new QHBoxLayout() );
603 expressionWidgetBox->layout()->setContentsMargins( 0, 0, 0, 0 );
604 expressionWidgetBox->layout()->addWidget( expressionWidget );
605 expressionWidgetBox->layout()->addWidget( addFieldButton );
606 expressionWidgetBox->layout()->addWidget( editExpressionButton );
607 layout->addWidget( qmlCodeBox );
608 layout->addWidget( qmlCode );
609
610 QTextEdit *errorFeedbackWidget = new QTextEdit();
611 errorFeedbackWidget->setMaximumHeight( 90 );
612 layout->addWidget( errorFeedbackWidget );
613
614 //update preview on text change
615 connect( qmlCode, &QgsCodeEditor::editingTimeout, this, [qmlWrapper, qmlCode, previewFeature, errorFeedbackWidget] {
616 QQuickWidget *qmlWidget = dynamic_cast<QQuickWidget *>( qmlWrapper->widget() );
617 if ( qmlCode->text().trimmed().isEmpty() )
618 {
619 errorFeedbackWidget->setText( QObject::tr( "No QML code" ) );
620 if ( qmlWidget )
621 {
622 qmlWidget->setSource( QUrl() );
623 }
624 return;
625 }
626 qmlWrapper->setQmlCode( qmlCode->text() );
627 qmlWrapper->reinitWidget();
628 qmlWrapper->setFeature( previewFeature );
629 if ( qmlWidget )
630 {
631 const QList<QQmlError> errors = qmlWidget->errors();
632 if ( !errors.isEmpty() )
633 {
634 QStringList errorStrings;
635 for ( const QQmlError &error : errors )
636 {
637 errorStrings << u"%1:%2: %3"_s.arg( QString::number( error.line() ), QString::number( error.column() ), error.description() );
638 }
639 errorFeedbackWidget->setText( errorStrings.join( "\n" ) );
640 }
641 else
642 {
643 errorFeedbackWidget->setText( QObject::tr( "Valid code" ) );
644 }
645 }
646 } );
647
648 QScrollArea *qmlPreviewBox = new QgsScrollArea();
649 qmlPreviewBox->setWidgetResizable( true );
650 qmlPreviewBox->setMinimumWidth( 200 );
651 qmlPreviewBox->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
652
653 qmlWrapper->widget()->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred );
654
655 QWidget *qmlContainerWidget = new QWidget( qmlPreviewBox );
656 qmlContainerWidget->setMinimumSize( 200, 0 );
657 qmlContainerWidget->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred );
658 QVBoxLayout *qmlLayout = new QVBoxLayout( qmlContainerWidget );
659 qmlLayout->addWidget( qmlWrapper->widget() );
660 qmlLayout->addSpacerItem( new QSpacerItem( 20, 40, QSizePolicy::Expanding, QSizePolicy::Expanding ) );
661 qmlContainerWidget->setLayout( qmlLayout );
662 qmlPreviewBox->setWidget( qmlContainerWidget );
663
664 emit qmlCode->editingTimeout();
665 qmlSplitter->addWidget( qmlPreviewBox );
666 qmlSplitter->setChildrenCollapsible( false );
667 qmlSplitter->setHandleWidth( 6 );
668 qmlSplitter->setSizes( QList<int>() << 1 << 1 );
669
670 QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help );
671
672 connect( buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept );
673 connect( buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject );
674 connect( buttonBox, &QDialogButtonBox::helpRequested, &dlg, [] { QgsHelp::openHelp( u"working_with_vector/vector_properties.html#other-widgets"_s ); } );
675
676 mainLayout->addWidget( buttonBox );
677
678 if ( dlg.exec() )
679 {
680 QgsAttributesFormData::QmlElementEditorConfiguration qmlEdCfg;
681 qmlEdCfg.qmlCode = qmlCode->text();
682 itemData.setQmlElementEditorConfiguration( qmlEdCfg );
683 itemData.setShowLabel( showLabelCheckbox->isChecked() );
684
685 mModel->sourceModel()->setData( sourceIndex, itemData, QgsAttributesFormModel::ItemDataRole );
686 mModel->sourceModel()->setData( sourceIndex, title->text(), QgsAttributesFormModel::ItemNameRole );
687 }
688 }
689 break;
690
692 {
693 QDialog dlg;
694 dlg.setObjectName( "HTML Form Configuration Widget" );
696 dlg.setWindowTitle( tr( "Configure HTML Widget" ) );
697
698 QVBoxLayout *mainLayout = new QVBoxLayout( &dlg );
699 QSplitter *htmlSplitter = new QSplitter();
700 QWidget *htmlConfigWiget = new QWidget();
701 QVBoxLayout *layout = new QVBoxLayout( htmlConfigWiget );
702 layout->setContentsMargins( 0, 0, 0, 0 );
703 mainLayout->addWidget( htmlSplitter );
704 htmlSplitter->addWidget( htmlConfigWiget );
705 htmlSplitter->setChildrenCollapsible( false );
706 htmlSplitter->setHandleWidth( 6 );
707 htmlSplitter->setSizes( QList<int>() << 1 << 1 );
708 layout->addWidget( baseWidget );
709
710 QLineEdit *title = new QLineEdit( itemName );
711
712 //htmlCode
713 QgsCodeEditorHTML *htmlCode = new QgsCodeEditorHTML();
714 htmlCode->setSizePolicy( QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding );
715 htmlCode->setText( itemData.htmlElementEditorConfiguration().htmlCode );
716
717 QgsHtmlWidgetWrapper *htmlWrapper = new QgsHtmlWidgetWrapper( mLayer, nullptr, this );
718 QgsFeature previewFeature;
719 mLayer->getFeatures().nextFeature( previewFeature );
720
721 //update preview on text change
722 connect( htmlCode, &QgsCodeEditorHTML::textChanged, this, [htmlWrapper, htmlCode, previewFeature] {
723 htmlWrapper->setHtmlCode( htmlCode->text() );
724 htmlWrapper->reinitWidget();
725 htmlWrapper->setFeature( previewFeature );
726 } );
727
728 QgsFieldExpressionWidget *expressionWidget = new QgsFieldExpressionWidget;
729 expressionWidget->setButtonVisible( false );
730 expressionWidget->registerExpressionContextGenerator( this );
731 expressionWidget->setLayer( mLayer );
732 QToolButton *addFieldButton = new QToolButton();
733 addFieldButton->setIcon( QgsApplication::getThemeIcon( u"/symbologyAdd.svg"_s ) );
734
735 QToolButton *editExpressionButton = new QToolButton();
736 editExpressionButton->setIcon( QgsApplication::getThemeIcon( u"/mIconExpression.svg"_s ) );
737 editExpressionButton->setToolTip( tr( "Insert/Edit Expression" ) );
738
739 connect( addFieldButton, &QAbstractButton::clicked, this, [expressionWidget, htmlCode] {
740 QString expression = expressionWidget->expression().trimmed().replace( '"', "\\\""_L1 );
741 if ( !expression.isEmpty() )
742 htmlCode->insertText( u"<script>document.write(expression.evaluate(\"%1\"));</script>"_s.arg( expression ) );
743 } );
744
745 connect( editExpressionButton, &QAbstractButton::clicked, this, [this, htmlCode] {
746 QString expression
747 = QgsExpressionFinder::findAndSelectActiveExpression( htmlCode, u"<script>\\s*document\\.write\\(\\s*expression\\.evaluate\\(\\s*\"(.*?)\\s*\"\\s*\\)\\s*\\)\\s*;?\\s*</script>"_s );
748 expression.replace( "\\\""_L1, "\""_L1 );
749 QgsExpressionContext context = createExpressionContext();
750 QgsExpressionBuilderDialog exprDlg( mLayer, expression, this, u"generic"_s, context );
751
752 exprDlg.setWindowTitle( tr( "Insert Expression" ) );
753 if ( exprDlg.exec() == QDialog::Accepted && !exprDlg.expressionText().trimmed().isEmpty() )
754 {
755 QString expression = exprDlg.expressionText().trimmed().replace( '"', "\\\""_L1 );
756 if ( !expression.isEmpty() )
757 htmlCode->insertText( u"<script>document.write(expression.evaluate(\"%1\"));</script>"_s.arg( expression ) );
758 }
759 } );
760
761 layout->addWidget( new QLabel( tr( "Title" ) ) );
762 layout->addWidget( title );
763 QGroupBox *expressionWidgetBox = new QGroupBox( tr( "HTML Code" ) );
764 layout->addWidget( expressionWidgetBox );
765 expressionWidgetBox->setLayout( new QHBoxLayout );
766 expressionWidgetBox->layout()->addWidget( expressionWidget );
767 expressionWidgetBox->layout()->addWidget( addFieldButton );
768 expressionWidgetBox->layout()->addWidget( editExpressionButton );
769 layout->addWidget( htmlCode );
770 QScrollArea *htmlPreviewBox = new QgsScrollArea();
771 htmlPreviewBox->setLayout( new QGridLayout );
772 htmlPreviewBox->setMinimumWidth( 200 );
773 htmlPreviewBox->layout()->addWidget( htmlWrapper->widget() );
774 //emit to load preview for the first time
775 emit htmlCode->textChanged();
776 htmlSplitter->addWidget( htmlPreviewBox );
777 htmlSplitter->setChildrenCollapsible( false );
778 htmlSplitter->setHandleWidth( 6 );
779 htmlSplitter->setSizes( QList<int>() << 1 << 1 );
780
781 QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help );
782
783 connect( buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept );
784 connect( buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject );
785 connect( buttonBox, &QDialogButtonBox::helpRequested, &dlg, [] { QgsHelp::openHelp( u"working_with_vector/vector_properties.html#other-widgets"_s ); } );
786
787 mainLayout->addWidget( buttonBox );
788
789 if ( dlg.exec() )
790 {
791 QgsAttributesFormData::HtmlElementEditorConfiguration htmlEdCfg;
792 htmlEdCfg.htmlCode = htmlCode->text();
793 itemData.setHtmlElementEditorConfiguration( htmlEdCfg );
794 itemData.setShowLabel( showLabelCheckbox->isChecked() );
795
796 mModel->sourceModel()->setData( sourceIndex, itemData, QgsAttributesFormModel::ItemDataRole );
797 mModel->sourceModel()->setData( sourceIndex, title->text(), QgsAttributesFormModel::ItemNameRole );
798 }
799 break;
800 }
801
803 {
804 QDialog dlg;
805 dlg.setObjectName( "Text Form Configuration Widget" );
807 dlg.setWindowTitle( tr( "Configure Text Widget" ) );
808
809 QVBoxLayout *mainLayout = new QVBoxLayout( &dlg );
810 QSplitter *textSplitter = new QSplitter();
811 QWidget *textConfigWiget = new QWidget();
812 QVBoxLayout *layout = new QVBoxLayout( textConfigWiget );
813 layout->setContentsMargins( 0, 0, 0, 0 );
814 mainLayout->addWidget( textSplitter );
815 textSplitter->addWidget( textConfigWiget );
816 layout->addWidget( baseWidget );
817
818 QLineEdit *title = new QLineEdit( itemName );
819
820 QgsCodeEditorHTML *text = new QgsCodeEditorHTML();
821 text->setSizePolicy( QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding );
822 text->setText( itemData.textElementEditorConfiguration().text );
823
824 QgsTextWidgetWrapper *textWrapper = new QgsTextWidgetWrapper( mLayer, nullptr, this );
825 QgsFeature previewFeature;
826 ( void ) mLayer->getFeatures( QgsFeatureRequest().setLimit( 1 ) ).nextFeature( previewFeature );
827
828 //update preview on text change
829 connect( text, &QgsCodeEditorExpression::textChanged, this, [textWrapper, previewFeature, text] {
830 textWrapper->setText( text->text() );
831 textWrapper->reinitWidget();
832 textWrapper->setFeature( previewFeature );
833 } );
834
835 QgsFieldExpressionWidget *expressionWidget = new QgsFieldExpressionWidget;
836 expressionWidget->setButtonVisible( false );
837 expressionWidget->registerExpressionContextGenerator( this );
838 expressionWidget->setLayer( mLayer );
839 QToolButton *addFieldButton = new QToolButton();
840 addFieldButton->setIcon( QgsApplication::getThemeIcon( u"/symbologyAdd.svg"_s ) );
841
842 QToolButton *editExpressionButton = new QToolButton();
843 editExpressionButton->setIcon( QgsApplication::getThemeIcon( u"/mIconExpression.svg"_s ) );
844 editExpressionButton->setToolTip( tr( "Insert/Edit Expression" ) );
845
846 connect( addFieldButton, &QAbstractButton::clicked, this, [expressionWidget, text] {
847 QString expression = expressionWidget->expression().trimmed();
848 if ( !expression.isEmpty() )
849 text->insertText( u"[%%1%]"_s.arg( expression ) );
850 } );
851 connect( editExpressionButton, &QAbstractButton::clicked, this, [this, text] {
852 QString expression = QgsExpressionFinder::findAndSelectActiveExpression( text );
853
854 QgsExpressionContext context = createExpressionContext();
855 QgsExpressionBuilderDialog exprDlg( mLayer, expression, this, u"generic"_s, context );
856
857 exprDlg.setWindowTitle( tr( "Insert Expression" ) );
858 if ( exprDlg.exec() == QDialog::Accepted && !exprDlg.expressionText().trimmed().isEmpty() )
859 {
860 QString expression = exprDlg.expressionText().trimmed();
861 if ( !expression.isEmpty() )
862 text->insertText( u"[%%1%]"_s.arg( expression ) );
863 }
864 } );
865
866 layout->addWidget( new QLabel( tr( "Title" ) ) );
867 layout->addWidget( title );
868 QGroupBox *expressionWidgetBox = new QGroupBox( tr( "Text" ) );
869 layout->addWidget( expressionWidgetBox );
870 expressionWidgetBox->setLayout( new QHBoxLayout );
871 expressionWidgetBox->layout()->addWidget( expressionWidget );
872 expressionWidgetBox->layout()->addWidget( addFieldButton );
873 expressionWidgetBox->layout()->addWidget( editExpressionButton );
874 layout->addWidget( text );
875 QScrollArea *textPreviewBox = new QgsScrollArea();
876 textPreviewBox->setWidgetResizable( true );
877 textPreviewBox->setMinimumWidth( 200 );
878 textPreviewBox->setWidget( textWrapper->widget() );
879 //emit to load preview for the first time
880 emit text->textChanged();
881 textSplitter->addWidget( textPreviewBox );
882 textSplitter->setChildrenCollapsible( false );
883 textSplitter->setHandleWidth( 6 );
884 textSplitter->setSizes( QList<int>() << 1 << 1 );
885
886 QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help );
887
888 connect( buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept );
889 connect( buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject );
890 connect( buttonBox, &QDialogButtonBox::helpRequested, &dlg, [] { QgsHelp::openHelp( u"working_with_vector/vector_properties.html#other-widgets"_s ); } );
891
892 mainLayout->addWidget( buttonBox );
893
894 if ( dlg.exec() )
895 {
896 QgsAttributesFormData::TextElementEditorConfiguration textEdCfg;
897 textEdCfg.text = text->text();
898 itemData.setTextElementEditorConfiguration( textEdCfg );
899 itemData.setShowLabel( showLabelCheckbox->isChecked() );
900
901 mModel->sourceModel()->setData( sourceIndex, itemData, QgsAttributesFormModel::ItemDataRole );
902 mModel->sourceModel()->setData( sourceIndex, title->text(), QgsAttributesFormModel::ItemNameRole );
903 }
904 break;
905 }
906
908 {
909 QDialog dlg;
910 dlg.setObjectName( "Spacer Form Configuration Widget" );
912 dlg.setWindowTitle( tr( "Configure Spacer Widget" ) );
913
914 QVBoxLayout *mainLayout = new QVBoxLayout();
915 mainLayout->addWidget( new QLabel( tr( "Title" ) ) );
916 QLineEdit *title = new QLineEdit( itemName );
917 mainLayout->addWidget( title );
918
919 QHBoxLayout *cbLayout = new QHBoxLayout();
920 mainLayout->addLayout( cbLayout );
921 dlg.setLayout( mainLayout );
922 QCheckBox *cb = new QCheckBox { &dlg };
923 cb->setChecked( itemData.spacerElementEditorConfiguration().drawLine );
924 cbLayout->addWidget( new QLabel( tr( "Draw horizontal line" ), &dlg ) );
925 cbLayout->addWidget( cb );
926
927 QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help );
928
929 connect( buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept );
930 connect( buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject );
931 connect( buttonBox, &QDialogButtonBox::helpRequested, &dlg, [] { QgsHelp::openHelp( u"working_with_vector/vector_properties.html#other-widgets"_s ); } );
932
933 mainLayout->addWidget( buttonBox );
934
935 if ( dlg.exec() )
936 {
937 QgsAttributesFormData::SpacerElementEditorConfiguration spacerEdCfg;
938 spacerEdCfg.drawLine = cb->isChecked();
939 itemData.setSpacerElementEditorConfiguration( spacerEdCfg );
940 itemData.setShowLabel( false );
941
942 mModel->sourceModel()->setData( sourceIndex, itemData, QgsAttributesFormModel::ItemDataRole );
943 mModel->sourceModel()->setData( sourceIndex, title->text(), QgsAttributesFormModel::ItemNameRole );
944 }
945
946 break;
947 }
948 }
949}
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
Manages available widgets when configuring attributes forms.
void setModel(QAbstractItemModel *model) override
Overridden setModel() from base class. Only QgsAttributesFormProxyModel is an acceptable model.
QgsAttributesAvailableWidgetsView(QgsVectorLayer *layer, QWidget *parent=nullptr)
Constructor for QgsAttributesAvailableWidgetsView, with the given parent.
QgsAttributesAvailableWidgetsModel * availableWidgetsModel() const
Access the underlying QgsAttributesAvailableWidgetsModel source model.
void setFilterText(const QString &text)
Sets the filter text to the underlying proxy model.
QgsAttributesFormProxyModel * mModel
QgsAttributesFormModel * sourceModel() const
Returns the underlying QgsAttributesFormModel model where the view gets data from.
void addIndicator(QgsAttributesFormItem *item, QgsAttributesFormTreeViewIndicator *indicator)
Adds the indicator to the given item.
QHash< QgsAttributesFormItem *, QList< QgsAttributesFormTreeViewIndicator * > > mIndicators
Storage of indicators used with the tree view.
void removeIndicator(QgsAttributesFormItem *item, QgsAttributesFormTreeViewIndicator *indicator)
Removes the indicator from the given item.
QgsExpressionContext createExpressionContext() const override
This method needs to be reimplemented in all classes which implement this interface and return an exp...
void removeAllIndicators()
Removes all indicators in the current view.
QgsAttributesFormBaseView(QgsVectorLayer *layer, QWidget *parent=nullptr)
Constructor for QgsAttributesFormBaseView, with the given parent.
const QList< QgsAttributesFormTreeViewIndicator * > indicators(const QModelIndex &index) const
Returns the list of indicators associated with a given index.
void selectFirstMatchingItem(const QgsAttributesFormData::AttributesFormItemType &itemType, const QString &itemId)
Selects the first item that matches a itemType and a itemId.
QModelIndex firstSelectedIndex() const
Returns the source model index corresponding to the first selected row.
Main class to store and transfer editor data contained in a QgsAttributesFormModel.
HtmlElementEditorConfiguration htmlElementEditorConfiguration() const
Returns the HTML editor configuration.
TextElementEditorConfiguration textElementEditorConfiguration() const
Returns the editor configuration for text element.
SpacerElementEditorConfiguration spacerElementEditorConfiguration() const
Returns the spacer element configuration.
void setHtmlElementEditorConfiguration(const HtmlElementEditorConfiguration &htmlElementEditorConfiguration)
Sets the HTML editor configuration.
bool showLabel() const
Returns whether the widget's label is to be shown.
void setShowLabel(bool showLabel)
Sets whether the label for the widget should be shown.
void setQmlElementEditorConfiguration(const QmlElementEditorConfiguration &qmlElementEditorConfiguration)
Sets the QML editor configuration.
void setSpacerElementEditorConfiguration(SpacerElementEditorConfiguration spacerElementEditorConfiguration)
Sets the the spacer element configuration to spacerElementEditorConfiguration.
QmlElementEditorConfiguration qmlElementEditorConfiguration() const
Returns the QML editor configuration.
void setTextElementEditorConfiguration(const TextElementEditorConfiguration &textElementEditorConfiguration)
Sets the editor configuration for text element to textElementEditorConfiguration.
AttributesFormItemType
Custom item types.
@ Container
Container for the form, which may be tab, group or row.
@ Relation
Relation between two vector layers.
@ Field
Vector layer field.
@ SpacerWidget
Spacer widget type,.
@ WidgetType
In the available widgets tree, the type of widget.
@ TextWidget
Text widget type,.
Holds parent-child relations as well as item data contained in a QgsAttributesFormModel.
Manages form layouts when configuring attributes forms via drag and drop designer.
void internalItemsDropped(const QModelIndexList &indexes)
Informs that items were moved (via drop) in the model from the same model.
void externalItemsDropped(const QModelIndexList &indexes)
Informs that items were inserted (via drop) in the model from another model.
void dropEvent(QDropEvent *event) override
void dragEnterEvent(QDragEnterEvent *event) override
QgsAttributesFormLayoutView(QgsVectorLayer *layer, QWidget *parent=nullptr)
Constructor for QgsAttributesFormLayoutView, with the given parent.
void setModel(QAbstractItemModel *model) override
Overridden setModel() from base class. Only QgsAttributesFormProxyModel is an acceptable model.
void dragMoveEvent(QDragMoveEvent *event) override
Is called when mouse is moved over attributes tree before a drop event.
Abstract class for tree models allowing for configuration of attributes forms.
@ ItemNameRole
Prior to QGIS 3.44, this was available as FieldNameRole.
@ ItemDataRole
Prior to QGIS 3.44, this was available as DnDTreeRole.
QgsAttributesFormItem * itemForIndex(const QModelIndex &index) const
Returns the underlying item that corresponds to the given index.
Indicator that can be used in an Attributes Form tree view to display icons next to field items.
void changed()
Emitted when the indicator changes state (e.g.
A text editor based on QScintilla2.
@ ScriptEditor
Standard mode, allows for display and edit of entire scripts.
void setText(const QString &text) override
void setEditingTimeoutInterval(int timeout)
Sets the timeout (in milliseconds) threshold for the editingTimeout() signal to be emitted after an e...
@ CodeFolding
Indicates that code folding should be enabled for the editor.
void insertText(const QString &text)
Insert text at cursor position, or replace any selected text if user has made a selection.
void setLineNumbersVisible(bool visible)
Sets whether line numbers should be visible in the editor.
void editingTimeout()
Emitted when either:
static std::unique_ptr< QgsExpressionContextScope > globalScope()
Creates a new scope which contains variables and functions relating to the global QGIS context.
static std::unique_ptr< QgsExpressionContextScope > layerScope(const QgsMapLayer *layer)
Creates a new scope which contains variables and functions relating to a QgsMapLayer.
static std::unique_ptr< QgsExpressionContextScope > formScope(const QgsFeature &formFeature=QgsFeature(), const QString &formMode=QString())
Creates a new scope which contains functions and variables from the current attribute form/table form...
static std::unique_ptr< QgsExpressionContextScope > projectScope(const QgsProject *project)
Creates a new scope which contains variables and functions relating to a QGIS project.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
void appendScope(QgsExpressionContextScope *scope)
Appends a scope to the end of the context.
static QString findAndSelectActiveExpression(QgsCodeEditor *editor, const QString &pattern=QString())
Find the expression under the cursor in the given editor and select it.
bool nextFeature(QgsFeature &f)
Fetch next feature and stores in f, returns true on success.
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition qgsfeature.h:60
static void enableAutoGeometryRestore(QWidget *widget, const QString &key=QString())
Register the widget to allow its position to be automatically saved and restored when open and closed...
Definition qgsgui.cpp:225
static void openHelp(const QString &key)
Opens help topic for the given help key using default system web browser.
Definition qgshelp.cpp:41
void reinitWidget()
Clears the content and makes new initialization.
void setHtmlCode(const QString &htmlCode)
Sets the HTML code to htmlCode.
void setFeature(const QgsFeature &feature) override
static QgsProject * instance()
Returns the QgsProject singleton instance.
Wraps a QQuickWidget to display QML code.
void setFeature(const QgsFeature &feature) override
void reinitWidget()
Clears the content and makes new initialization.
void setQmlCode(const QString &qmlCode)
writes the qmlCode into a temporary file
void setText(const QString &text)
Sets the text code to htmlCode.
void reinitWidget()
Clears the content and makes new initialization.
void setFeature(const QgsFeature &feature) override
Represents a vector layer which manages a vector based dataset.
QgsFeatureIterator getFeatures(const QgsFeatureRequest &request=QgsFeatureRequest()) const final
Queries the layer for features specified in request.
QWidget * widget()
Access the widget managed by this wrapper.