2

I am looking for a way to highlight rows of a QTreeView without reimplementing a QTreeView subclass.

I have seen similar questions:

But they all use delegate. The reason is that I am creating a search text in widgets tool which browse all widgets and find and highlights text in them. Hence, I cannot use delegate.

Is there any possible solution? Painting a semi-transparent item above it?


Here is the code of the tool which stores the widget and the way to search for text in them.

class GUI_EXPORT QgsSearchHighlightOptionWidget : public QObject
{
    Q_OBJECT
  public:

    /**
     * Constructor
     * \param widget the widget used to search text into
     */
    explicit QgsSearchHighlightOptionWidget( QWidget *widget = nullptr );

    /**
     * Returns if it valid: if the widget type is handled and if the widget is not still available
     */
    bool isValid() { return mWidget && mValid; }

    /**
     * search for a text pattern and highlight the widget if the text is found
     * \returns true if the text pattern is found
     */
    bool searchHighlight( const QString &searchText );

    /**
     *  reset the style to the original state
     */
    void reset();

    /**
     * return the widget
     */
    QWidget *widget() { return mWidget; }

    bool eventFilter( QObject *obj, QEvent *event ) override;

  private slots:
    void widgetDestroyed();

  private:
    QPointer< QWidget > mWidget;
    QString mStyleSheet;
    bool mValid = true;
    bool mChangedStyle = false;
    std::function < bool( QString )> mTextFound = []( QString searchText ) {Q_UNUSED( searchText ); return false;};
    bool mInstalledFilter = false;
};

QgsSearchHighlightOptionWidget::QgsSearchHighlightOptionWidget( QWidget *widget )
  : QObject( widget )
  , mWidget( widget )
{
  if ( qobject_cast<QLabel *>( widget ) )
  {
    mStyleSheet = QStringLiteral( "QLabel { background-color: yellow; color: blue;}" );
    mTextFound = [ = ]( QString searchText ) {return qobject_cast<QLabel *>( mWidget )->text().contains( searchText, Qt::CaseInsensitive );};
  }
  else if ( qobject_cast<QCheckBox *>( widget ) )
  {
    mStyleSheet = QStringLiteral( "QCheckBox { background-color: yellow; color: blue;}" );
    mTextFound = [ = ]( QString searchText ) {return qobject_cast<QCheckBox *>( mWidget )->text().contains( searchText, Qt::CaseInsensitive );};
  }
  else if ( qobject_cast<QAbstractButton *>( widget ) )
  {
    mStyleSheet = QStringLiteral( "QAbstractButton { background-color: yellow; color: blue;}" );
    mTextFound = [ = ]( QString searchText ) {return qobject_cast<QAbstractButton *>( mWidget )->text().contains( searchText, Qt::CaseInsensitive );};
  }
  else if ( qobject_cast<QGroupBox *>( widget ) )
  {
    mStyleSheet = QStringLiteral( "QGroupBox::title { background-color: yellow; color: blue;}" );
    mTextFound = [ = ]( QString searchText ) {return qobject_cast<QGroupBox *>( mWidget )->title().contains( searchText, Qt::CaseInsensitive );};
  }
  else if ( qobject_cast<QTreeView *>( widget ) )
  {
    // TODO - style individual matching items
    mTextFound = [ = ]( QString searchText )
    {
      QTreeView *tree = qobject_cast<QTreeView *>( mWidget );
      if ( !tree )
        return false;
      QModelIndexList hits = tree->model()->match( tree->model()->index( 0, 0 ), Qt::DisplayRole, searchText, 1, Qt::MatchContains | Qt::MatchRecursive );
      return !hits.isEmpty();
    };
  }
  else
  {
    mValid = false;
  }
  if ( mValid )
  {
    mStyleSheet.prepend( "/*!search!*/" ).append( "/*!search!*/" );
    QgsDebugMsgLevel( mStyleSheet, 4 );
    connect( mWidget, &QWidget::destroyed, this, &QgsSearchHighlightOptionWidget::widgetDestroyed );
  }
}

bool QgsSearchHighlightOptionWidget::searchHighlight( const QString &searchText )
{
  bool found = false;
  if ( !mWidget )
    return found;

  if ( !searchText.isEmpty() )
  {
    found = mTextFound( searchText );
  }

  if ( found && !mChangedStyle )
  {
    if ( !mWidget->isVisible() )
    {
      // show the widget to get initial stylesheet in case it's modified
      QgsDebugMsg( QString( "installing event filter on: %1 (%2)" )
                   .arg( mWidget->objectName() )
                   .arg( qobject_cast<QLabel *>( mWidget ) ? qobject_cast<QLabel *>( mWidget )->text() : QString() ) );
      mWidget->installEventFilter( this );
      mInstalledFilter = true;
    }
    else
    {
      mWidget->setStyleSheet( mWidget->styleSheet() + mStyleSheet );
      mChangedStyle = true;
    }
  }

  return found;
}

bool QgsSearchHighlightOptionWidget::eventFilter( QObject *obj, QEvent *event )
{
  if ( mInstalledFilter && event->type() == QEvent::Show && obj == mWidget )
  {
    mWidget->removeEventFilter( this );
    mInstalledFilter = false;
    // instead of catching the event and calling show again
    // it might be better to use a timer to change the style
    // after the widget is shown
#if 1
    mWidget->show();
    mWidget->setStyleSheet( mWidget->styleSheet() + mStyleSheet );
    return true;
#else
    QTimer::singleShot( 500, this, [ = ]
    {
      mWidget->setStyleSheet( mWidget->styleSheet() + mStyleSheet );
      mChangedStyle = true;
    } );
#endif
  }
  return QObject::eventFilter( obj, event );
}

void QgsSearchHighlightOptionWidget::reset()
{
  if ( mWidget && mValid )
  {
    if ( mChangedStyle )
    {
      QString ss = mWidget->styleSheet();
      ss.remove( mStyleSheet );
      mWidget->setStyleSheet( ss );
      mChangedStyle = false;
    }
    else if ( mInstalledFilter )
    {
      mWidget->removeEventFilter( this );
      mInstalledFilter = false;
    }
  }
}

void QgsSearchHighlightOptionWidget::widgetDestroyed()
{
  mWidget = nullptr;
  mValid = false;
}

And here is the code to actually register the widgets from the dialog:

void QgsOptionsDialogBase::registerTextSearchWidgets()
{
  mRegisteredSearchWidgets.clear();

  for ( int i = 0; i < mOptStackedWidget->count(); i++ )
  {
    Q_FOREACH ( QWidget *w, mOptStackedWidget->widget( i )->findChildren<QWidget *>() )
    {
      QgsSearchHighlightOptionWidget *shw = new QgsSearchHighlightOptionWidget( w );
      if ( shw->isValid() )
      {
        QgsDebugMsgLevel( QString( "Registering: %1" ).arg( w->objectName() ), 4 );
        mRegisteredSearchWidgets.append( qMakePair( shw, i ) );
      }
      else
      {
        delete shw;
      }
    }
  }
}
Denis Rouzaud
  • 2,412
  • 2
  • 26
  • 45
  • If it shows the code that highlights the text we could give you more options, placing a transparent element would be difficult and immanent, spending resources unnecessarily. – eyllanesc Jan 24 '18 at 19:01
  • I have added the code of the tool. – Denis Rouzaud Jan 24 '18 at 19:19
  • @DenisRouzaud, If you use `QTreeView` you definitely have a model for it? If you have a model for your tree view, it's easy to implement row highlighting there. – vahancho Jan 25 '18 at 08:58
  • @vahancho without subclassing I don't know how. Sorry, but saying it's easy is not a really good pointer... – Denis Rouzaud Jan 25 '18 at 12:32
  • @DenisRouzaud, the question was whether you have a model or not. If you have, just let me know. – vahancho Jan 25 '18 at 12:49

1 Answers1

2

Reading the docs, I don't know how to do it directly. Here are some suggestions. Hopefully one works for you:

1) Could you use QTreeWidget instead of QTreeView ? With this, it should be easy. Use the item functions and e.g. setBackground() on the item.

2) How about you filter tree instead of highlighting? Using setRowHidden()?

3) If you haven't already, you could also try using QTreeView::keyboardSearch() and see what this does.

4) you could use select* and add a next/previous button to your search box. I.e. you hop through the tree selecting the current search result.

Ronny Brendel
  • 4,777
  • 5
  • 35
  • 55
  • thanks a lot for these clear options. Hiding rows and digging keyboardSearch are the most promising in my use case. Cheers. – Denis Rouzaud Jan 25 '18 at 12:33