File indexing completed on 2024-05-12 09:56:57

0001 /*
0002     SPDX-FileCopyrightText: 2007-2008 Robert Knight <robertknight@gmail.com>
0003     SPDX-FileCopyrightText: 2020 Tomaz Canabrava <tcanabrava@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "FilterChain.h"
0009 #include "Filter.h"
0010 
0011 #include "terminalDisplay/TerminalColor.h"
0012 #include "terminalDisplay/TerminalDisplay.h"
0013 #include "terminalDisplay/TerminalFonts.h"
0014 
0015 #include <QDebug>
0016 #include <QEvent>
0017 #include <QKeyEvent>
0018 #include <QPainter>
0019 #include <QRect>
0020 
0021 #include <algorithm>
0022 
0023 using namespace Konsole;
0024 FilterChain::FilterChain(TerminalDisplay *terminalDisplay)
0025     : _terminalDisplay(terminalDisplay)
0026     , _showUrlHint(false)
0027     , _reverseUrlHints(false)
0028     , _urlHintsModifiers(Qt::NoModifier)
0029 {
0030 }
0031 FilterChain::~FilterChain()
0032 {
0033     qDeleteAll(_filters);
0034 }
0035 
0036 void FilterChain::addFilter(Filter *filter)
0037 {
0038     _filters.append(filter);
0039 }
0040 
0041 void FilterChain::removeFilter(Filter *filter)
0042 {
0043     _filters.removeAll(filter);
0044 }
0045 
0046 void FilterChain::reset()
0047 {
0048     for (auto *filter : _filters) {
0049         filter->reset();
0050     }
0051 }
0052 
0053 void FilterChain::setBuffer(const QString *buffer, const QList<int> *linePositions)
0054 {
0055     for (auto *filter : _filters) {
0056         filter->setBuffer(buffer, linePositions);
0057     }
0058 }
0059 
0060 void FilterChain::process()
0061 {
0062     for (auto *filter : _filters) {
0063         filter->process();
0064     }
0065 }
0066 
0067 void FilterChain::clear()
0068 {
0069     _filters.clear();
0070 }
0071 
0072 QSharedPointer<HotSpot> FilterChain::hotSpotAt(int line, int column) const
0073 {
0074     for (auto *filter : _filters) {
0075         QSharedPointer<HotSpot> spot = filter->hotSpotAt(line, column);
0076         if (spot != nullptr) {
0077             return spot;
0078         }
0079     }
0080     return nullptr;
0081 }
0082 
0083 QList<QSharedPointer<HotSpot>> FilterChain::hotSpots() const
0084 {
0085     QList<QSharedPointer<HotSpot>> list;
0086     for (auto *filter : _filters) {
0087         list.append(filter->hotSpots());
0088     }
0089     return list;
0090 }
0091 
0092 QRegion FilterChain::hotSpotRegion() const
0093 {
0094     QRegion region;
0095     for (const auto &hotSpot : hotSpots()) {
0096         QRect r;
0097         r.setLeft(hotSpot->startColumn());
0098         r.setTop(hotSpot->startLine());
0099         if (hotSpot->startLine() == hotSpot->endLine()) {
0100             r.setRight(hotSpot->endColumn());
0101             r.setBottom(hotSpot->endLine());
0102             region |= _terminalDisplay->imageToWidget(r);
0103         } else {
0104             r.setRight(_terminalDisplay->columns());
0105             r.setBottom(hotSpot->startLine());
0106             region |= _terminalDisplay->imageToWidget(r);
0107 
0108             r.setLeft(0);
0109 
0110             for (int line = hotSpot->startLine() + 1; line < hotSpot->endLine(); line++) {
0111                 r.moveTop(line);
0112                 region |= _terminalDisplay->imageToWidget(r);
0113             }
0114 
0115             r.moveTop(hotSpot->endLine());
0116             r.setRight(hotSpot->endColumn());
0117             region |= _terminalDisplay->imageToWidget(r);
0118         }
0119     }
0120     return region;
0121 }
0122 
0123 int FilterChain::count(HotSpot::Type type) const
0124 {
0125     const auto hSpots = hotSpots();
0126     return std::count_if(std::begin(hSpots), std::end(hSpots), [type](const QSharedPointer<HotSpot> &s) {
0127         return s->type() == type;
0128     });
0129 }
0130 
0131 QList<QSharedPointer<HotSpot>> FilterChain::filterBy(HotSpot::Type type) const
0132 {
0133     QList<QSharedPointer<HotSpot>> hotspots;
0134     for (const auto &spot : hotSpots()) {
0135         if (spot->type() == type) {
0136             hotspots.append(spot);
0137         }
0138     }
0139     return hotspots;
0140 }
0141 
0142 void FilterChain::leaveEvent(TerminalDisplay *td, QEvent *ev)
0143 {
0144     Q_UNUSED(td)
0145     Q_UNUSED(ev)
0146     _showUrlHint = false;
0147 }
0148 
0149 void FilterChain::keyReleaseEvent(TerminalDisplay *td, QKeyEvent *ev, int charLine, int charColumn)
0150 {
0151     if (_showUrlHint) {
0152         _showUrlHint = false;
0153         td->update();
0154     }
0155 
0156     auto spot = hotSpotAt(charLine, charColumn);
0157     if (spot != nullptr) {
0158         spot->keyReleaseEvent(td, ev);
0159     }
0160 }
0161 
0162 bool FilterChain::keyPressEvent(TerminalDisplay *td, QKeyEvent *ev, int charLine, int charColumn)
0163 {
0164     if ((_urlHintsModifiers != 0u) && ev->modifiers() == _urlHintsModifiers) {
0165         QList<QSharedPointer<HotSpot>> hotspots = filterBy(HotSpot::Link);
0166         int nHotSpots = hotspots.count();
0167         int hintSelected = ev->key() - '1';
0168 
0169         // Triggered a Hotspot via shortcut.
0170         if (hintSelected >= 0 && hintSelected <= 9 && hintSelected < nHotSpots) {
0171             if (_reverseUrlHints) {
0172                 hintSelected = nHotSpots - hintSelected - 1;
0173             }
0174             hotspots.at(hintSelected)->activate();
0175             _showUrlHint = false;
0176             td->update();
0177             return true;
0178         }
0179 
0180         if (!_showUrlHint) {
0181             td->processFilters();
0182             _showUrlHint = true;
0183             td->update();
0184         }
0185     }
0186 
0187     auto spot = hotSpotAt(charLine, charColumn);
0188     if (spot != nullptr) {
0189         spot->keyPressEvent(td, ev);
0190     }
0191     return false;
0192 }
0193 
0194 void FilterChain::mouseMoveEvent(TerminalDisplay *td, QMouseEvent *ev, int charLine, int charColumn)
0195 {
0196     auto spot = hotSpotAt(charLine, charColumn);
0197     if (_hotSpotUnderMouse != spot) {
0198         if (_hotSpotUnderMouse != nullptr) {
0199             _hotSpotUnderMouse->mouseLeaveEvent(td, ev);
0200         }
0201         _hotSpotUnderMouse = spot;
0202         if (_hotSpotUnderMouse != nullptr) {
0203             _hotSpotUnderMouse->mouseEnterEvent(td, ev);
0204         }
0205     }
0206 
0207     if (spot != nullptr) {
0208         spot->mouseMoveEvent(td, ev);
0209     }
0210 }
0211 
0212 void FilterChain::mouseReleaseEvent(TerminalDisplay *td, QMouseEvent *ev, int charLine, int charColumn)
0213 {
0214     auto spot = hotSpotAt(charLine, charColumn);
0215     if (!spot) {
0216         return;
0217     }
0218     spot->mouseReleaseEvent(td, ev);
0219 }
0220 
0221 void FilterChain::paint(TerminalDisplay *td, QPainter &painter)
0222 {
0223     // get color of character under mouse and use it to draw
0224     // lines for filters
0225     QPoint cursorPos = td->mapFromGlobal(QCursor::pos());
0226 
0227     auto [cursorLine, cursorColumn] = td->getCharacterPosition(cursorPos, false);
0228 
0229     Character cursorCharacter = td->getCursorCharacter(std::min(cursorColumn, td->columns() - 1), cursorLine);
0230     painter.setPen(QPen(cursorCharacter.foregroundColor.color(td->terminalColor()->colorTable())));
0231 
0232     // iterate over hotspots identified by the display's currently active filters
0233     // and draw appropriate visuals to indicate the presence of the hotspot
0234 
0235     const auto spots = hotSpots();
0236     int urlNumber;
0237     int urlNumInc;
0238 
0239     // TODO: Remove _reverseUrllHints from TerminalDisplay.
0240     // TODO: Access reverseUrlHints from the profile, here.
0241     if (_reverseUrlHints) {
0242         // The URL hint numbering should be 'physically' increasing on the
0243         // keyboard, so they start at one and end up on 10 (on 0, or at least
0244         // they used to, but it seems to have changed, I'll fix that later).
0245         urlNumber = count(HotSpot::Link);
0246         urlNumInc = -1;
0247     } else {
0248         urlNumber = 1;
0249         urlNumInc = 1;
0250     }
0251 
0252     for (const auto &spot : spots) {
0253         QRegion region;
0254         if (spot->type() == HotSpot::Link || spot->type() == HotSpot::EMailAddress || spot->type() == HotSpot::EscapedUrl || spot->type() == HotSpot::File) {
0255             QPair<QRegion, QRect> spotRegion =
0256                 spot->region(td->terminalFont()->fontWidth(), td->terminalFont()->fontHeight(), td->columns(), td->contentRect());
0257             region = spotRegion.first;
0258             QRect r = spotRegion.second;
0259 
0260             // TODO: Move this paint code to HotSpot->drawHint();
0261             // TODO: Fix the Url Hints access from the Profile.
0262             if (_showUrlHint && (spot->type() == HotSpot::Link || spot->type() == HotSpot::File)) {
0263                 if (urlNumber >= 0 && urlNumber < 10) {
0264                     // Position at the beginning of the URL
0265                     QRect hintRect(*region.begin());
0266                     hintRect.setWidth(r.height());
0267                     painter.fillRect(hintRect, QColor(0, 0, 0, 128));
0268                     painter.setPen(Qt::white);
0269                     painter.drawRect(hintRect.adjusted(0, 0, -1, -1));
0270                     painter.drawText(hintRect, Qt::AlignCenter, QString::number(urlNumber));
0271                 }
0272                 urlNumber += urlNumInc;
0273             }
0274         }
0275 
0276         if (spot->startLine() < 0 || spot->endLine() < 0) {
0277             qDebug() << "ERROR, invalid hotspot:";
0278             spot->debug();
0279         }
0280 
0281         for (int line = spot->startLine(); line <= spot->endLine(); line++) {
0282             int startColumn = 0;
0283             int endColumn = td->columns() - 1; // TODO use number of _columns which are actually
0284             // occupied on this line rather than the width of the
0285             // display in _columns
0286 
0287             // FIXME: the left side condition is always false due to the
0288             //        endColumn assignment above
0289             // Check image size so _image[] is valid (see makeImage)
0290             if (endColumn >= td->columns() || line >= td->lines()) {
0291                 break;
0292             }
0293 
0294             // ignore whitespace at the end of the lines
0295             while (td->getCursorCharacter(endColumn, line).isSpace() && endColumn > 0) {
0296                 endColumn--;
0297             }
0298 
0299             // increment here because the column which we want to set 'endColumn' to
0300             // is the first whitespace character at the end of the line
0301             endColumn++;
0302 
0303             if (line == spot->startLine()) {
0304                 startColumn = spot->startColumn();
0305             }
0306             if (line == spot->endLine()) {
0307                 endColumn = spot->endColumn();
0308             }
0309 
0310             // TODO: resolve this comment with the new margin/center code
0311             // subtract one pixel from
0312             // the right and bottom so that
0313             // we do not overdraw adjacent
0314             // hotspots
0315             //
0316             // subtracting one pixel from all sides also prevents an edge case where
0317             // moving the mouse outside a link could still leave it underlined
0318             // because the check below for the position of the cursor
0319             // finds it on the border of the target area
0320             QRect r;
0321             r.setCoords(startColumn * td->terminalFont()->fontWidth() + td->contentRect().left(),
0322                         line * td->terminalFont()->fontHeight() + td->contentRect().top(),
0323                         endColumn * td->terminalFont()->fontWidth() + td->contentRect().left() - 1,
0324                         (line + 1) * td->terminalFont()->fontHeight() + td->contentRect().top() - 1);
0325 
0326             // Underline link hotspots
0327             // TODO: Fix accessing the urlHint here.
0328             // TODO: Move this code to UrlFilterHotSpot.
0329             const bool hasMouse = region.contains(td->mapFromGlobal(QCursor::pos()));
0330             if (((spot->type() == HotSpot::Link || spot->type() == HotSpot::File) && _showUrlHint) || hasMouse) {
0331                 QFontMetrics metrics(td->font());
0332 
0333                 // find the baseline (which is the invisible line that the characters in the font sit on,
0334                 // with some having tails dangling below)
0335                 const int baseline = r.bottom() - metrics.descent();
0336                 // find the position of the underline below that
0337                 const qreal underlinePos = baseline + metrics.underlinePos();
0338                 painter.drawLine(QLineF(r.left(), underlinePos, r.right(), underlinePos));
0339 
0340                 // Marker hotspots simply have a transparent rectangular shape
0341                 // drawn on top of them
0342             } else if (spot->type() == HotSpot::Marker) {
0343                 // TODO - Do not use a hardcoded color for this
0344                 const bool isCurrentResultLine = (td->screenWindow()->currentResultLine() == (spot->startLine() + td->screenWindow()->currentLine()));
0345                 QColor color = isCurrentResultLine ? QColor(255, 255, 0, 120) : QColor(255, 0, 0, 120);
0346                 painter.fillRect(r, color);
0347             }
0348         }
0349     }
0350 }
0351 
0352 void FilterChain::setReverseUrlHints(bool value)
0353 {
0354     _reverseUrlHints = value;
0355 }
0356 
0357 void FilterChain::setUrlHintsModifiers(Qt::KeyboardModifiers value)
0358 {
0359     _urlHintsModifiers = value;
0360 }