File indexing completed on 2024-05-05 04:40:56

0001 /*
0002     SPDX-FileCopyrightText: 2007 Andreas Pakulat <apaku@gmx.de>
0003     SPDX-FileCopyrightText: 2007 Dukju Ahn <dukjuahn@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "outputwidget.h"
0009 
0010 #include "standardoutputview.h"
0011 
0012 #include <QAction>
0013 #include <QApplication>
0014 #include <QClipboard>
0015 #include <QIcon>
0016 #include <QLineEdit>
0017 #include <QRegularExpression>
0018 #include <QSortFilterProxyModel>
0019 #include <QStackedWidget>
0020 #include <QTabWidget>
0021 #include <QToolButton>
0022 #include <QTreeView>
0023 #include <QVBoxLayout>
0024 #include <QWidgetAction>
0025 
0026 #include <KActionCollection>
0027 #include <KColorScheme>
0028 #include <KLocalizedString>
0029 #include <KStandardAction>
0030 #include <KToggleAction>
0031 
0032 #include <outputview/ioutputviewmodel.h>
0033 #include <util/focusedtreeview.h>
0034 #include <util/expandablelineedit.h>
0035 
0036 #include "outputmodel.h"
0037 #include "outputwidgetconfig.h"
0038 #include "toolviewdata.h"
0039 #include <debug.h>
0040 
0041 namespace {
0042 QString validFilterInputToolTip()
0043 {
0044     return i18nc("@info:tooltip", "Enter a case-insensitive regular expression to filter the output view");
0045 }
0046 }
0047 
0048 OutputWidget::OutputWidget(QWidget* parent, const ToolViewData* tvdata)
0049     : QWidget(parent)
0050     , m_tabwidget(nullptr)
0051     , m_stackwidget(nullptr)
0052     , data(tvdata)
0053     , m_closeButton(nullptr)
0054     , m_closeOthersAction(nullptr)
0055     , m_nextAction(nullptr)
0056     , m_previousAction(nullptr)
0057     , m_activateOnSelect(nullptr)
0058     , m_focusOnSelect(nullptr)
0059     , m_filterInput(nullptr)
0060     , m_filterAction(nullptr)
0061     , m_outputWidgetConfig(nullptr)
0062 {
0063     setWindowTitle(i18nc("@title:window", "Output View"));
0064     setWindowIcon(tvdata->icon);
0065     auto* layout = new QVBoxLayout(this);
0066     layout->setContentsMargins(0, 0, 0, 0);
0067     if( data->type & KDevelop::IOutputView::MultipleView )
0068     {
0069         m_tabwidget = new QTabWidget(this);
0070         layout->addWidget( m_tabwidget );
0071         m_closeButton = new QToolButton( this );
0072         connect( m_closeButton, &QToolButton::clicked, this, &OutputWidget::closeActiveView );
0073         m_closeButton->setIcon( QIcon::fromTheme( QStringLiteral( "tab-close") ) );
0074         m_closeButton->setToolTip(i18nc("@info:tooltip", "Close the currently active output view"));
0075         m_closeButton->setAutoRaise(true);
0076 
0077         m_closeOthersAction = new QAction( this );
0078         connect(m_closeOthersAction, &QAction::triggered, this, &OutputWidget::closeOtherViews);
0079         m_closeOthersAction->setIcon(QIcon::fromTheme(QStringLiteral("tab-close-other")));
0080         m_closeOthersAction->setToolTip(i18nc("@info:tooltip", "Close all other output views"));
0081         m_closeOthersAction->setText( m_closeOthersAction->toolTip() );
0082         addAction(m_closeOthersAction);
0083 
0084         m_tabwidget->setCornerWidget(m_closeButton, Qt::TopRightCorner);
0085         m_tabwidget->setDocumentMode(true);
0086     } else if ( data->type == KDevelop::IOutputView::HistoryView )
0087     {
0088         m_stackwidget = new QStackedWidget( this );
0089         layout->addWidget( m_stackwidget );
0090 
0091         m_previousAction = new QAction(QIcon::fromTheme(QStringLiteral("arrow-left")), i18nc("@action", "Previous Output"), this);
0092         connect(m_previousAction, &QAction::triggered, this, &OutputWidget::previousOutput);
0093         addAction(m_previousAction);
0094         m_nextAction = new QAction(QIcon::fromTheme(QStringLiteral("arrow-right")), i18nc("@action", "Next Output"), this);
0095         connect(m_nextAction, &QAction::triggered, this, &OutputWidget::nextOutput);
0096         addAction(m_nextAction);
0097     }
0098 
0099     m_activateOnSelect = new KToggleAction( QIcon(), i18nc("@action", "Select Activated Item"), this );
0100     m_activateOnSelect->setChecked( true );
0101     m_focusOnSelect = new KToggleAction( QIcon(), i18nc("@action", "Focus when Selecting Item"), this );
0102     m_focusOnSelect->setChecked( false );
0103     if( data->option & KDevelop::IOutputView::ShowItemsButton )
0104     {
0105         addAction(m_activateOnSelect);
0106         addAction(m_focusOnSelect);
0107     }
0108 
0109     QAction* action;
0110     action = new QAction(QIcon::fromTheme(QStringLiteral("text-wrap")), i18nc("@action", "Word Wrap"), this);
0111     action->setCheckable(true);
0112     connect(action, &QAction::toggled, this, &OutputWidget::setWordWrap);
0113     addAction(action);
0114 
0115     auto *separator = new QAction(this);
0116     separator->setSeparator(true);
0117     addAction(separator);
0118 
0119     action = new QAction(QIcon::fromTheme(QStringLiteral("go-first")), i18nc("@action", "First Item"), this);
0120     connect(action, &QAction::triggered, this, &OutputWidget::selectFirstItem);
0121     addAction(action);
0122 
0123     action = new QAction(QIcon::fromTheme(QStringLiteral("go-previous")), i18nc("@action", "Previous Item"), this);
0124     connect(action, &QAction::triggered, this, &OutputWidget::selectPreviousItem);
0125     addAction(action);
0126 
0127     action = new QAction(QIcon::fromTheme(QStringLiteral("go-next")), i18nc("@action", "Next Item"), this);
0128     connect(action, &QAction::triggered, this, &OutputWidget::selectNextItem);
0129     addAction(action);
0130 
0131     action = new QAction(QIcon::fromTheme(QStringLiteral("go-last")), i18nc("@action", "Last Item"), this);
0132     connect(action, &QAction::triggered, this, &OutputWidget::selectLastItem);
0133     addAction(action);
0134 
0135     QAction* selectAllAction = KStandardAction::selectAll(this, SLOT(selectAll()), this);
0136     selectAllAction->setShortcut(QKeySequence()); //FIXME: why does CTRL-A conflict with Katepart (while CTRL-Cbelow doesn't) ?
0137     selectAllAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0138     addAction(selectAllAction);
0139 
0140     QAction* copyAction = KStandardAction::copy(this, SLOT(copySelection()), this);
0141     copyAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0142     addAction(copyAction);
0143 
0144     auto* clearAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-list")), i18nc("@action", "Clear"), this);
0145     connect(clearAction, &QAction::triggered, this, &OutputWidget::clearModel);
0146     addAction(clearAction);
0147 
0148     if (data->type & KDevelop::IOutputView::MultipleView) {
0149         connect(m_tabwidget, &QTabWidget::currentChanged, this, &OutputWidget::currentViewChanged);
0150     } else if (data->type == KDevelop::IOutputView::HistoryView) {
0151         connect(m_stackwidget, &QStackedWidget::currentChanged, this, &OutputWidget::currentViewChanged);
0152     }
0153 
0154     if( data->option & KDevelop::IOutputView::AddFilterAction )
0155     {
0156         auto *separator = new QAction(this);
0157         separator->setSeparator(true);
0158         addAction(separator);
0159 
0160         m_filterInput = new KExpandableLineEdit(this);
0161         m_filterInput->setPlaceholderText(i18nc("@info:placeholder", "Search..."));
0162         m_filterInput->setClearButtonEnabled(true);
0163         m_filterInput->setToolTip(validFilterInputToolTip());
0164         m_filterAction = new QWidgetAction(this);
0165         m_filterAction->setText(m_filterInput->placeholderText());
0166         connect(m_filterAction, &QAction::triggered, this, [this]() {m_filterInput->setFocus();});
0167         m_filterAction->setDefaultWidget(m_filterInput);
0168         addAction(m_filterAction);
0169 
0170         connect(m_filterInput, &QLineEdit::textEdited,
0171                 this, &OutputWidget::outputFilter );
0172     }
0173 
0174     if (!data->configSubgroupName.isEmpty()
0175         && (data->type & KDevelop::IOutputView::MultipleView || data->type & KDevelop::IOutputView::HistoryView)) {
0176         m_outputWidgetConfig = new OutputWidgetConfig(data->configSubgroupName, data->title, this);
0177         connect(m_outputWidgetConfig, &OutputWidgetConfig::settingsChanged, this, [this]() {
0178             const auto maxViewCount = m_outputWidgetConfig->maxViewCount();
0179             // don't close views when view limit is not enabled
0180             if (!maxViewCount.has_value()) {
0181                 return;
0182             }
0183             if (data->type & KDevelop::IOutputView::MultipleView) {
0184                 closeFirstViewsWhileTooMany(*m_tabwidget, *maxViewCount);
0185             } else {
0186                 closeFirstViewsWhileTooMany(*m_stackwidget, *maxViewCount);
0187             }
0188         });
0189 
0190         QAction* const openConfigAction = KStandardAction::preferences(
0191             m_outputWidgetConfig,
0192             [this]() {
0193                 m_outputWidgetConfig->openDialog(this);
0194             },
0195             this);
0196         openConfigAction->setText(
0197             i18nc("@action %1: output type, e.g. Build or Run", "Configure %1 Output", data->title));
0198         openConfigAction->setShortcut(QKeySequence());
0199         openConfigAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0200         insertAction(separator, openConfigAction);
0201     }
0202 
0203     addActions(data->actionList);
0204 
0205     connect( data, &ToolViewData::outputAdded,
0206              this, &OutputWidget::addOutput );
0207 
0208     connect( this, &OutputWidget::outputRemoved,
0209              data->plugin, &StandardOutputView::outputRemoved );
0210 
0211     for (auto it = data->outputdata.keyBegin(), end =data->outputdata.keyEnd(); it != end; ++it) {
0212         int id = *it;
0213         changeModel( id );
0214         changeDelegate( id );
0215     }
0216     enableActions();
0217 }
0218 
0219 OutputWidget::~OutputWidget()
0220 {
0221     // Disconnect our widget to prevent updateFilter() slot calling from parent's destructor,
0222     // which leads to segfault since m_views hash will be destroyed before.
0223     if (m_tabwidget) {
0224         m_tabwidget->disconnect(this);
0225     } else if (m_stackwidget) {
0226         m_stackwidget->disconnect(this);
0227     }
0228 }
0229 
0230 void OutputWidget::clearModel()
0231 {
0232     auto view = qobject_cast<QAbstractItemView*>(currentWidget());
0233     if( !view || !view->isVisible())
0234         return;
0235 
0236     KDevelop::OutputModel *outputModel = nullptr;
0237     if (auto proxy = qobject_cast<QAbstractProxyModel*>(view->model())) {
0238         outputModel = qobject_cast<KDevelop::OutputModel*>(proxy->sourceModel());
0239     } else {
0240         outputModel = qobject_cast<KDevelop::OutputModel*>(view->model());
0241     }
0242     outputModel->clear();
0243 }
0244 
0245 void OutputWidget::addOutput( int id )
0246 {
0247     QTreeView* listview = createListView(id);
0248     setCurrentWidget( listview );
0249     connect( data->outputdata.value(id), &OutputData::modelChanged, this, &OutputWidget::changeModel);
0250     connect( data->outputdata.value(id), &OutputData::delegateChanged, this, &OutputWidget::changeDelegate);
0251 
0252     enableActions();
0253 }
0254 
0255 void OutputWidget::setCurrentWidget( QTreeView* view )
0256 {
0257     if( data->type & KDevelop::IOutputView::MultipleView )
0258     {
0259         m_tabwidget->setCurrentWidget( view );
0260     } else if( data->type & KDevelop::IOutputView::HistoryView )
0261     {
0262         m_stackwidget->setCurrentWidget( view );
0263     }
0264 }
0265 
0266 void OutputWidget::changeDelegate( int id )
0267 {
0268     const auto viewIt = m_views.constFind(id);
0269     const auto dataIt = data->outputdata.constFind(id);
0270     if (dataIt != data->outputdata.constEnd() && viewIt != m_views.constEnd()) {
0271         (*viewIt).view->setItemDelegate((*dataIt)->delegate);
0272     } else {
0273         addOutput(id);
0274     }
0275 }
0276 
0277 void OutputWidget::changeModel( int id )
0278 {
0279     const auto viewIt = m_views.constFind(id);
0280     const auto dataIt = data->outputdata.constFind(id);
0281     if (dataIt != data->outputdata.constEnd() && viewIt != m_views.constEnd()) {
0282         (*viewIt).view->setModel((*dataIt)->model);
0283     }
0284     else
0285     {
0286         addOutput( id );
0287     }
0288 }
0289 
0290 void OutputWidget::removeOutput( int id )
0291 {
0292     const auto viewIt = m_views.constFind(id);
0293     if (data->outputdata.contains(id) && (viewIt != m_views.constEnd())) {
0294         auto view = viewIt->view;
0295         if( data->type & KDevelop::IOutputView::MultipleView || data->type & KDevelop::IOutputView::HistoryView )
0296         {
0297             if( data->type & KDevelop::IOutputView::MultipleView )
0298             {
0299                 int idx = m_tabwidget->indexOf( view );
0300                 if (idx != -1)
0301                 {
0302                     m_tabwidget->removeTab( idx );
0303                 }
0304             } else
0305             {
0306                 int idx = m_stackwidget->indexOf( view );
0307                 if (idx != -1)
0308                 {
0309                     m_stackwidget->removeWidget(view);
0310                 }
0311             }
0312         } else {
0313             // KDevelop::IOutputView::OneView case
0314             // Do nothig here since our single view will be automatically removed from layout
0315             // during it's destroy
0316         }
0317 
0318         m_views.erase(viewIt);
0319         // remove our view with proxy model which is view's child (see outputFilter() method).
0320         delete view;
0321 
0322         emit outputRemoved( data->toolViewId, id );
0323     }
0324     enableActions();
0325 }
0326 
0327 bool OutputWidget::closeView(const QWidget* view)
0328 {
0329     Q_ASSERT(view);
0330     Q_ASSERT(qobject_cast<const QAbstractItemView*>(view));
0331 
0332     const auto fvIt = constFindFilteredView(static_cast<const QAbstractItemView*>(view));
0333     if (fvIt == m_views.cend()) {
0334         return false;
0335     }
0336 
0337     const auto id = fvIt.key();
0338     if (!(data->outputdata.value(id)->behaviour & KDevelop::IOutputView::AllowUserClose)) {
0339         return false;
0340     }
0341 
0342     data->plugin->removeOutput(id);
0343     enableActions();
0344     return true;
0345 }
0346 
0347 template<class ViewContainer>
0348 void OutputWidget::closeFirstViewIfTooMany(const ViewContainer& viewContainer)
0349 {
0350     if (!m_outputWidgetConfig) {
0351         return;
0352     }
0353     const auto maxViewCount = m_outputWidgetConfig->maxViewCount();
0354     Q_ASSERT(!maxViewCount.has_value() || *maxViewCount > 0);
0355     if (maxViewCount.has_value() && viewContainer.count() > *maxViewCount) {
0356         closeView(viewContainer.widget(0));
0357     }
0358 }
0359 
0360 template<class ViewContainer>
0361 void OutputWidget::closeFirstViewsWhileTooMany(const ViewContainer& viewContainer, int maxViewCount)
0362 {
0363     Q_ASSERT(maxViewCount > 0);
0364     while (viewContainer.count() > maxViewCount) {
0365         if (!closeView(viewContainer.widget(0))) {
0366             break; // Closing a view usually succeeds. Prevent endless loop if it fails.
0367         }
0368     }
0369 }
0370 
0371 void OutputWidget::closeActiveView()
0372 {
0373     Q_ASSERT(m_tabwidget);
0374     if (const auto* view = m_tabwidget->currentWidget()) {
0375         closeView(view);
0376     }
0377 }
0378 
0379 void OutputWidget::closeOtherViews()
0380 {
0381     Q_ASSERT(m_tabwidget);
0382     QWidget* widget = m_tabwidget->currentWidget();
0383     if (!widget)
0384         return;
0385 
0386     const auto ids = m_views.keys();
0387     for (int id : ids) {
0388         if (m_views.value(id).view == widget) {
0389             continue; // leave the active view open
0390         }
0391 
0392         OutputData* od = data->outputdata.value(id);
0393         if (od->behaviour & KDevelop::IOutputView::AllowUserClose) {
0394             data->plugin->removeOutput( id );
0395         }
0396     }
0397     enableActions();
0398 }
0399 
0400 QWidget* OutputWidget::currentWidget() const
0401 {
0402     QWidget* widget;
0403     if( data->type & KDevelop::IOutputView::MultipleView )
0404     {
0405         widget = m_tabwidget->currentWidget();
0406     } else if( data->type & KDevelop::IOutputView::HistoryView )
0407     {
0408         widget = m_stackwidget->currentWidget();
0409     } else
0410     {
0411         widget = m_views.begin()->view;
0412     }
0413     return widget;
0414 }
0415 
0416 KDevelop::IOutputViewModel *OutputWidget::outputViewModel() const
0417 {
0418     auto view = qobject_cast<QAbstractItemView*>(currentWidget());
0419     if( !view || !view->isVisible())
0420         return nullptr;
0421 
0422     QAbstractItemModel *absmodel = view->model();
0423     auto* iface = qobject_cast<KDevelop::IOutputViewModel*>(absmodel);
0424     if ( ! iface )
0425     {
0426         // try if it's a proxy model?
0427         if ( auto* proxy = qobject_cast<QAbstractProxyModel*>(absmodel) )
0428         {
0429             iface = qobject_cast<KDevelop::IOutputViewModel*>(proxy->sourceModel());
0430         }
0431     }
0432     return iface;
0433 }
0434 
0435 void OutputWidget::eventuallyDoFocus()
0436 {
0437     QWidget* widget = currentWidget();
0438     if( m_focusOnSelect->isChecked() && !widget->hasFocus() ) {
0439         widget->setFocus( Qt::OtherFocusReason );
0440     }
0441 }
0442 
0443 QAbstractItemView *OutputWidget::outputView() const
0444 {
0445     return qobject_cast<QAbstractItemView*>(currentWidget());
0446 }
0447 
0448 void OutputWidget::activateIndex(const QModelIndex &index, QAbstractItemView *view, KDevelop::IOutputViewModel *iface)
0449 {
0450     if( ! index.isValid() )
0451         return;
0452     QModelIndex sourceIndex = index;
0453     QModelIndex viewIndex = index;
0454     const auto fvIt = constFindFilteredView(view);
0455     if (fvIt != m_views.cend() && fvIt->proxyModel) {
0456         auto proxy = fvIt->proxyModel;
0457         if ( index.model() == proxy ) {
0458             // index is from the proxy, map it to the source
0459             sourceIndex = proxy->mapToSource(index);
0460         } else if (proxy == view->model()) {
0461             // index is from the source, map it to the proxy
0462             viewIndex = proxy->mapFromSource(index);
0463         }
0464     }
0465 
0466     view->setCurrentIndex( viewIndex );
0467     view->scrollTo( viewIndex );
0468 
0469     if( m_activateOnSelect->isChecked() ) {
0470         iface->activate( sourceIndex );
0471     }
0472 }
0473 
0474 void OutputWidget::selectFirstItem()
0475 {
0476     selectItem(First);
0477 }
0478 
0479 void OutputWidget::selectNextItem()
0480 {
0481     selectItem(Next);
0482 }
0483 
0484 void OutputWidget::selectPreviousItem()
0485 {
0486     selectItem(Previous);
0487 }
0488 
0489 void OutputWidget::selectLastItem()
0490 {
0491     selectItem(Last);
0492 }
0493 
0494 void OutputWidget::selectItem(SelectionMode selectionMode)
0495 {
0496     auto view = outputView();
0497     auto iface = outputViewModel();
0498     if ( ! view || ! iface )
0499         return;
0500     eventuallyDoFocus();
0501 
0502     auto index = view->currentIndex();
0503     const auto fvIt = constFindFilteredView(view);
0504     if (fvIt != m_views.cend() && fvIt->proxyModel) {
0505         auto proxy = fvIt->proxyModel;
0506         if ( index.model() == proxy ) {
0507             // index is from the proxy, map it to the source
0508             index = proxy->mapToSource(index);
0509         }
0510     }
0511 
0512     QModelIndex newIndex;
0513     switch (selectionMode) {
0514         case First:
0515             newIndex = iface->firstHighlightIndex();
0516             break;
0517         case Next:
0518             newIndex = iface->nextHighlightIndex( index );
0519             break;
0520         case Previous:
0521             newIndex = iface->previousHighlightIndex( index );
0522             break;
0523         case Last:
0524             newIndex = iface->lastHighlightIndex();
0525             break;
0526     }
0527 
0528     qCDebug(PLUGIN_STANDARDOUTPUTVIEW) << "old:" << index << "- new:" << newIndex;
0529     activateIndex(newIndex, view, iface);
0530 }
0531 
0532 void OutputWidget::activate(const QModelIndex& index)
0533 {
0534     auto iface = outputViewModel();
0535     auto view = outputView();
0536     if( ! view || ! iface )
0537         return;
0538     activateIndex(index, view, iface);
0539 }
0540 
0541 QTreeView* OutputWidget::createListView(int id)
0542 {
0543     auto createHelper = [&]() -> QTreeView* {
0544         auto* listview = new KDevelop::FocusedTreeView(this);
0545         listview->setEditTriggers( QAbstractItemView::NoEditTriggers );
0546         listview->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); //Always enable the scrollbar, so it doesn't flash around
0547         listview->setHeaderHidden(true);
0548         listview->setRootIsDecorated(false);
0549         listview->setUniformRowHeights(!m_wordWrap);
0550         listview->setWordWrap(m_wordWrap);
0551         listview->setSelectionMode( QAbstractItemView::ContiguousSelection );
0552 
0553         if (data->outputdata.value(id)->behaviour & KDevelop::IOutputView::AutoScroll) {
0554             listview->setAutoScrollAtEnd(true);
0555         }
0556 
0557         connect(listview, &QTreeView::activated, this, &OutputWidget::activate);
0558         connect(listview, &QTreeView::clicked, this, &OutputWidget::activate);
0559 
0560         return listview;
0561     };
0562 
0563     QTreeView* listview = nullptr;
0564     const auto viewIt = m_views.constFind(id);
0565     if (viewIt != m_views.constEnd()) {
0566         listview = viewIt->view;
0567     } else {
0568         bool newView = true;
0569 
0570         if( data->type & KDevelop::IOutputView::MultipleView || data->type & KDevelop::IOutputView::HistoryView )
0571         {
0572             qCDebug(PLUGIN_STANDARDOUTPUTVIEW) << "creating listview";
0573             listview = createHelper();
0574 
0575             if( data->type & KDevelop::IOutputView::MultipleView )
0576             {
0577                 m_tabwidget->addTab(listview, data->outputdata.value(id)->title);
0578                 closeFirstViewIfTooMany(*m_tabwidget);
0579             } else
0580             {
0581                 const int index = m_stackwidget->addWidget(listview);
0582                 m_stackwidget->setCurrentIndex(index);
0583                 closeFirstViewIfTooMany(*m_stackwidget);
0584             }
0585         } else
0586         {
0587             if( m_views.isEmpty() )
0588             {
0589                 listview = createHelper();
0590                 layout()->addWidget(listview);
0591             } else
0592             {
0593                 listview = m_views.cbegin().value().view;
0594                 newView = false;
0595             }
0596         }
0597         m_views[id].view = listview;
0598 
0599         changeModel( id );
0600         changeDelegate( id );
0601 
0602         if (newView)
0603             listview->scrollToBottom();
0604     }
0605     enableActions();
0606     return listview;
0607 }
0608 
0609 void OutputWidget::raiseOutput(int id)
0610 {
0611     const auto viewIt = m_views.constFind(id);
0612     if (viewIt != m_views.constEnd()) {
0613         auto view = viewIt->view;
0614         if( data->type & KDevelop::IOutputView::MultipleView )
0615         {
0616             int idx = m_tabwidget->indexOf(view);
0617             if( idx >= 0 )
0618             {
0619                 m_tabwidget->setCurrentIndex( idx );
0620             }
0621         } else if( data->type & KDevelop::IOutputView::HistoryView )
0622         {
0623             int idx = m_stackwidget->indexOf(view);
0624             if( idx >= 0 )
0625             {
0626                 m_stackwidget->setCurrentIndex( idx );
0627             }
0628         }
0629     }
0630     enableActions();
0631 }
0632 
0633 void OutputWidget::nextOutput()
0634 {
0635     if( m_stackwidget && m_stackwidget->currentIndex() < m_stackwidget->count()-1 )
0636     {
0637         m_stackwidget->setCurrentIndex( m_stackwidget->currentIndex()+1 );
0638     }
0639     enableActions();
0640 }
0641 
0642 void OutputWidget::previousOutput()
0643 {
0644     if( m_stackwidget && m_stackwidget->currentIndex() > 0 )
0645     {
0646         m_stackwidget->setCurrentIndex( m_stackwidget->currentIndex()-1 );
0647     }
0648     enableActions();
0649 }
0650 
0651 void OutputWidget::setWordWrap(bool enable)
0652 {
0653     m_wordWrap = enable;
0654 
0655     auto* const widget = currentWidget();
0656     if (!widget) {
0657         return; // this happens in test_standardoutputview when the last view is removed
0658     }
0659     auto* const view = qobject_cast<KDevelop::FocusedTreeView*>(widget);
0660     if (!view) {
0661         qCWarning(PLUGIN_STANDARDOUTPUTVIEW) << "current widget is not a FocusedTreeView:" << widget;
0662         return;
0663     }
0664 
0665     if (view->wordWrap() == m_wordWrap) {
0666         Q_ASSERT(view->uniformRowHeights() == !m_wordWrap);
0667         return; // nothing changed
0668     }
0669 
0670     view->setUniformRowHeights(!m_wordWrap);
0671     view->setWordWrap(m_wordWrap);
0672     view->fitColumns();
0673 }
0674 
0675 void OutputWidget::enableActions()
0676 {
0677     if( data->type == KDevelop::IOutputView::HistoryView )
0678     {
0679         Q_ASSERT(m_stackwidget);
0680         Q_ASSERT(m_nextAction);
0681         Q_ASSERT(m_previousAction);
0682         m_previousAction->setEnabled( ( m_stackwidget->currentIndex() > 0 ) );
0683         m_nextAction->setEnabled( ( m_stackwidget->currentIndex() < m_stackwidget->count() - 1 ) );
0684     }
0685 }
0686 
0687 void OutputWidget::scrollToIndex( const QModelIndex& idx )
0688 {
0689     QWidget* w = currentWidget();
0690     if( !w )
0691         return;
0692     auto *view = static_cast<QAbstractItemView*>(w);
0693     view->scrollTo( idx );
0694 }
0695 
0696 void OutputWidget::copySelection()
0697 {
0698     QWidget* widget = currentWidget();
0699     if( !widget )
0700         return;
0701     auto* view = qobject_cast<QAbstractItemView*>(widget);
0702     if( !view )
0703         return;
0704 
0705     QClipboard *cb = QApplication::clipboard();
0706     const QModelIndexList indexes = view->selectionModel()->selectedRows();
0707     QStringList content;
0708     content.reserve(indexes.size());
0709     for (const QModelIndex& index : indexes) {
0710         content += index.data().toString();
0711     }
0712     cb->setText(content.join(QLatin1Char('\n')));
0713 }
0714 
0715 void OutputWidget::selectAll()
0716 {
0717     if (auto *view = qobject_cast<QAbstractItemView*>(currentWidget()))
0718         view->selectAll();
0719 }
0720 
0721 int OutputWidget::currentOutputIndex()
0722 {
0723     int index = 0;
0724     if( data->type & KDevelop::IOutputView::MultipleView )
0725     {
0726         index = m_tabwidget->currentIndex();
0727     } else if( data->type & KDevelop::IOutputView::HistoryView )
0728     {
0729         index = m_stackwidget->currentIndex();
0730     }
0731     return index;
0732 }
0733 
0734 void OutputWidget::outputFilter(const QString& filter)
0735 {
0736     auto *view = qobject_cast<QAbstractItemView*>(currentWidget());
0737     if( !view )
0738         return;
0739 
0740     auto fvIt = findFilteredView(view);
0741     auto proxyModel = qobject_cast<QSortFilterProxyModel*>(view->model());
0742     if( !proxyModel )
0743     {
0744         // create new proxy model and make view it's parent. This allows us destroy view and
0745         // it's model with "one shot" (see removeOutput() method).
0746         fvIt->proxyModel = proxyModel = new QSortFilterProxyModel(view);
0747         proxyModel->setDynamicSortFilter(true);
0748         proxyModel->setSourceModel(view->model());
0749         view->setModel(proxyModel);
0750     }
0751 
0752     // Don't capture anything as the captures are not used anyway.
0753     QRegularExpression regex(filter, QRegularExpression::CaseInsensitiveOption | QRegularExpression::DontCaptureOption);
0754     fvIt->filter = regex; // store away the original regex to be restored when the current tab/widget changes
0755 
0756     if (!regex.isValid()) {
0757         // Setting an invalid regex as the model filter hides all output as expected, but also causes runtime warnings:
0758         // "QString::contains: invalid QRegularExpression object".
0759         // Set a valid regex that matches nothing to hide all output and avoid the warnings.
0760         static const QRegularExpression matchNothing(QStringLiteral("$z"));
0761         regex = matchNothing;
0762     }
0763     proxyModel->setFilterRegularExpression(regex);
0764 
0765     updateFilterInputAppearance(constIterator(fvIt));
0766 }
0767 
0768 void OutputWidget::updateFilter(int index)
0769 {
0770     QWidget *view = (data->type & KDevelop::IOutputView::MultipleView)
0771         ? m_tabwidget->widget(index) : m_stackwidget->widget(index);
0772     const auto fvIt = constFindFilteredView(qobject_cast<QAbstractItemView*>(view));
0773 
0774     const QString filterText = fvIt == m_views.cend() ? QString{} : fvIt->filter.pattern();
0775     if (filterText.isEmpty()) {
0776         m_filterInput->clear();
0777     } else {
0778         m_filterInput->setText(filterText);
0779     }
0780 
0781     updateFilterInputAppearance(fvIt);
0782 }
0783 
0784 void OutputWidget::currentViewChanged(int index)
0785 {
0786     if (data->option & KDevelop::IOutputView::AddFilterAction) {
0787         updateFilter(index);
0788     }
0789 
0790     // Update wordwrap status for new view
0791     setWordWrap(m_wordWrap);
0792 }
0793 
0794 void OutputWidget::setTitle(int outputId, const QString& title)
0795 {
0796     auto fview = m_views.value(outputId, FilteredView{});
0797     if (fview.view && (data->type & KDevelop::IOutputView::MultipleView)) {
0798         const int idx = m_tabwidget->indexOf(fview.view);
0799         if (idx >= 0) {
0800             m_tabwidget->setTabText(idx, title);
0801         }
0802     }
0803 }
0804 
0805 auto OutputWidget::constIterator(FilteredViews::iterator it) -> FilteredViews::const_iterator
0806 {
0807     return static_cast<FilteredViews::const_iterator>(it);
0808 }
0809 
0810 template<typename ForwardIt>
0811 ForwardIt OutputWidget::findFilteredView(ForwardIt first, ForwardIt last, const QAbstractItemView* view)
0812 {
0813     return std::find_if(first, last, [view](const FilteredView& filteredView) {
0814         return filteredView.view == view;
0815     });
0816 }
0817 
0818 auto OutputWidget::findFilteredView(const QAbstractItemView* view) -> FilteredViews::iterator
0819 {
0820     return findFilteredView(m_views.begin(), m_views.end(), view);
0821 }
0822 
0823 auto OutputWidget::constFindFilteredView(const QAbstractItemView* view) const -> FilteredViews::const_iterator
0824 {
0825     return findFilteredView(m_views.cbegin(), m_views.cend(), view);
0826 }
0827 
0828 void OutputWidget::updateFilterInputAppearance(FilteredViews::const_iterator currentView)
0829 {
0830     if (currentView == m_views.cend() || currentView->filter.isValid()) {
0831         m_filterInput->setPalette(QPalette{});
0832         m_filterInput->setToolTip(validFilterInputToolTip());
0833     } else {
0834         QPalette background(m_filterInput->palette());
0835         KColorScheme::adjustBackground(background, KColorScheme::NegativeBackground);
0836         m_filterInput->setPalette(background);
0837 
0838         m_filterInput->setToolTip(
0839             i18nc("@info:tooltip %1 - position in the pattern, %2 - textual description of the error",
0840                   "Filter regular expression pattern error at offset %1: %2", currentView->filter.patternErrorOffset(),
0841                   currentView->filter.errorString()));
0842     }
0843 }
0844 
0845 #include "moc_outputwidget.cpp"