File indexing completed on 2024-04-28 05:35:29

0001 /*
0002     SPDX-FileCopyrightText: 2004 Esben Mose Hansen <kde@mosehansen.dk>
0003     SPDX-FileCopyrightText: Andrew Stanley-Jones <asj@cban.com>
0004     SPDX-FileCopyrightText: 2000 Carsten Pfeiffer <pfeiffer@kde.org>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 #include "klipperpopup.h"
0009 
0010 #include <QGuiApplication>
0011 #include <QKeyEvent>
0012 #include <QScreen>
0013 #include <QWidgetAction>
0014 
0015 #include <KColorScheme>
0016 #include <KHelpMenu>
0017 #include <KLineEdit>
0018 #include <KLocalizedString>
0019 #include <KWindowInfo>
0020 #include <KWindowSystem>
0021 
0022 #include "history.h"
0023 #include "klipper.h"
0024 #include "popupproxy.h"
0025 
0026 namespace
0027 {
0028 // Index 0 is the menu header, index 1 is the search widget.
0029 static const int TOP_HISTORY_ITEM_INDEX = 2;
0030 }
0031 
0032 // #define DEBUG_EVENTS__
0033 
0034 #ifdef DEBUG_EVENTS__
0035 kdbgstream &operator<<(kdbgstream &stream, const QKeyEvent &e)
0036 {
0037     stream << "(QKeyEvent(text=" << e.text() << ",key=" << e.key() << (e.isAccepted() ? ",accepted" : ",ignored)") << ",count=" << e.count();
0038     if (e.modifiers() & Qt::AltModifier) {
0039         stream << ",ALT";
0040     }
0041     if (e.modifiers() & Qt::ControlModifier) {
0042         stream << ",CTRL";
0043     }
0044     if (e.modifiers() & Qt::MetaModifier) {
0045         stream << ",META";
0046     }
0047     if (e.modifiers() & Qt::ShiftModifier) {
0048         stream << ",SHIFT";
0049     }
0050     if (e.isAutoRepeat()) {
0051         stream << ",AUTOREPEAT";
0052     }
0053     stream << ")";
0054 
0055     return stream;
0056 }
0057 #endif
0058 
0059 KlipperPopup::KlipperPopup(History *history)
0060     : m_dirty(true)
0061     , m_history(history)
0062     , m_popupProxy(nullptr)
0063     , m_filterWidget(nullptr)
0064     , m_filterWidgetAction(nullptr)
0065     , m_lastEvent(nullptr)
0066 {
0067     ensurePolished();
0068     QScreen *screen = nullptr;
0069     if (KWindowSystem::isPlatformX11()) {
0070         KWindowInfo windowInfo(winId(), NET::WMGeometry);
0071         QRect geometry = windowInfo.geometry();
0072         screen = QGuiApplication::screenAt(geometry.center());
0073     }
0074     if (screen == nullptr) {
0075         screen = QGuiApplication::screens()[0];
0076     }
0077     int menuHeight = (screen->geometry().height()) * 3 / 4;
0078     int menuWidth = (screen->geometry().width()) * 1 / 3;
0079 
0080     m_popupProxy = new PopupProxy(this, menuHeight, menuWidth);
0081 
0082     connect(this, &KlipperPopup::aboutToShow, this, &KlipperPopup::slotAboutToShow);
0083 }
0084 
0085 void KlipperPopup::slotAboutToShow()
0086 {
0087     if (m_filterWidget) {
0088         if (!m_filterWidget->text().isEmpty()) {
0089             m_dirty = true;
0090             m_filterWidget->clear();
0091         }
0092     }
0093     ensureClean();
0094 }
0095 
0096 void KlipperPopup::ensureClean()
0097 {
0098     // If the history is unchanged since last menu build, there is no reason
0099     // to rebuild it,
0100     if (m_dirty) {
0101         rebuild();
0102     }
0103 }
0104 
0105 void KlipperPopup::buildFromScratch()
0106 {
0107     addSection(QIcon::fromTheme(QStringLiteral("klipper")),
0108                i18nc("%1 is application display name", "%1 - Clipboard Items", QGuiApplication::applicationDisplayName()));
0109 
0110     m_filterWidget = new KLineEdit(this);
0111     m_filterWidget->setFocusPolicy(Qt::NoFocus);
0112     m_filterWidget->setPlaceholderText(i18n("Search…"));
0113     m_filterWidgetAction = new QWidgetAction(this);
0114     m_filterWidgetAction->setDefaultWidget(m_filterWidget);
0115     addAction(m_filterWidgetAction);
0116 
0117     Q_ASSERT(actions().count() == TOP_HISTORY_ITEM_INDEX);
0118 }
0119 
0120 void KlipperPopup::showStatus(const QString &errorText)
0121 {
0122     const KColorScheme colorScheme(QPalette::Normal, KColorScheme::View);
0123     QPalette palette = m_filterWidget->palette();
0124 
0125     if (errorText.isEmpty()) { // no error
0126         palette.setColor(m_filterWidget->foregroundRole(), colorScheme.foreground(KColorScheme::NormalText).color());
0127         palette.setColor(m_filterWidget->backgroundRole(), colorScheme.background(KColorScheme::NormalBackground).color());
0128         // add no action, rebuild() will fill with history items
0129     } else { // there is an error
0130         palette.setColor(m_filterWidget->foregroundRole(), colorScheme.foreground(KColorScheme::NegativeText).color());
0131         palette.setColor(m_filterWidget->backgroundRole(), colorScheme.background(KColorScheme::NegativeBackground).color());
0132         addAction(new QAction(errorText, this));
0133     }
0134 
0135     m_filterWidget->setPalette(palette);
0136 }
0137 
0138 void KlipperPopup::rebuild(const QString &filter)
0139 {
0140     if (actions().isEmpty()) {
0141         buildFromScratch();
0142     } else {
0143         while (actions().count() > TOP_HISTORY_ITEM_INDEX) {
0144             // The old actions allocated by KlipperPopup::rebuild()
0145             // and PopupProxy::tryInsertItem() are deleted here when
0146             // the menu is rebuilt.
0147             QAction *action = actions().last();
0148             removeAction(action);
0149             action->deleteLater();
0150         }
0151     }
0152 
0153     QRegularExpression filterexp(filter);
0154     // The search is case insensitive unless at least one uppercase character
0155     // appears in the search term.
0156     //
0157     // This is not really a rigourous check, since the test below for an
0158     // uppercase character should really check for an uppercase character that
0159     // is not part of a special regexp character class or escape sequence:
0160     // for example, using "\S" to mean a non-whitespace character should not
0161     // force the match to be case sensitive.  However, that is not possible
0162     // without fully parsing the regexp.  The user is not likely to be searching
0163     // for complex regular expressions here.
0164     if (filter.toLower() == filter) {
0165         filterexp.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
0166     }
0167 
0168     QString errorText;
0169     if (!filterexp.isValid()) {
0170         errorText = i18n("Invalid regular expression, %1", filterexp.errorString());
0171     } else {
0172         const int nHistoryItems = m_popupProxy->buildParent(TOP_HISTORY_ITEM_INDEX, filterexp);
0173         if (nHistoryItems == 0) {
0174             if (m_history->empty()) {
0175                 errorText = i18n("Clipboard is empty");
0176             } else {
0177                 errorText = i18n("No matches");
0178             }
0179         } else {
0180             if (history()->topIsUserSelected()) {
0181                 QAction *topAction = actions().at(TOP_HISTORY_ITEM_INDEX);
0182                 topAction->setCheckable(true);
0183                 topAction->setChecked(true);
0184             }
0185         }
0186     }
0187 
0188     showStatus(errorText);
0189     m_dirty = false;
0190 }
0191 
0192 void KlipperPopup::slotTopIsUserSelectedSet()
0193 {
0194     if (!m_dirty && actions().count() > TOP_HISTORY_ITEM_INDEX && history()->topIsUserSelected()) {
0195         QAction *topAction = actions().at(TOP_HISTORY_ITEM_INDEX);
0196         topAction->setCheckable(true);
0197         topAction->setChecked(true);
0198     }
0199 }
0200 
0201 void KlipperPopup::showEvent(QShowEvent *)
0202 {
0203     popup(QCursor::pos());
0204 }
0205 
0206 void KlipperPopup::keyPressEvent(QKeyEvent *e)
0207 {
0208     // Most events are send down directly to the m_filterWidget.
0209     // If the m_filterWidget does not handle the event, it will
0210     // come back to this method. Remembering the last event stops
0211     // the infinite event loop
0212     if (m_lastEvent == e) {
0213         m_lastEvent = nullptr;
0214         return;
0215     }
0216     m_lastEvent = e;
0217     // If alt-something is pressed, select a shortcut
0218     // from the menu. Do this by sending a keyPress
0219     // without the alt-modifier to the superobject.
0220     if (e->modifiers() & Qt::AltModifier) {
0221         QKeyEvent ke(QEvent::KeyPress, e->key(), e->modifiers() ^ Qt::AltModifier, e->text(), e->isAutoRepeat(), e->count());
0222         QMenu::keyPressEvent(&ke);
0223 #ifdef DEBUG_EVENTS__
0224         qCDebug(KLIPPER_LOG) << "Passing this event to ancestor (KMenu): " << e << "->" << ke;
0225 #endif
0226         if (ke.isAccepted()) {
0227             e->accept();
0228             return;
0229         } else {
0230             e->ignore();
0231         }
0232     }
0233 
0234     // Otherwise, send most events to the search
0235     // widget, except a few used for navigation:
0236     // These go to the superobject.
0237     switch (e->key()) {
0238     case Qt::Key_Up:
0239     case Qt::Key_Down:
0240     case Qt::Key_Right:
0241     case Qt::Key_Left:
0242     case Qt::Key_Tab:
0243     case Qt::Key_Backtab:
0244     case Qt::Key_Escape: {
0245 #ifdef DEBUG_EVENTS__
0246         qCDebug(KLIPPER_LOG) << "Passing this event to ancestor (KMenu): " << e;
0247 #endif
0248         QMenu::keyPressEvent(e);
0249 
0250         break;
0251     }
0252     case Qt::Key_Return:
0253     case Qt::Key_Enter: {
0254         QMenu::keyPressEvent(e);
0255         this->hide();
0256 
0257         if (activeAction() == m_filterWidgetAction)
0258             setActiveAction(actions().at(TOP_HISTORY_ITEM_INDEX));
0259 
0260         break;
0261     }
0262     default: {
0263 #ifdef DEBUG_EVENTS__
0264         qCDebug(KLIPPER_LOG) << "Passing this event down to child (KLineEdit): " << e;
0265 #endif
0266         setActiveAction(actions().at(actions().indexOf(m_filterWidgetAction)));
0267         QString lastString = m_filterWidget->text();
0268 
0269         QCoreApplication::sendEvent(m_filterWidget, e);
0270         if (m_filterWidget->text() != lastString) {
0271             m_dirty = true;
0272             rebuild(m_filterWidget->text());
0273         }
0274 
0275         break;
0276     } // default:
0277     } // case
0278     m_lastEvent = nullptr;
0279 }
0280 
0281 void KlipperPopup::slotSetTopActive()
0282 {
0283     if (actions().size() > TOP_HISTORY_ITEM_INDEX) {
0284         setActiveAction(actions().at(TOP_HISTORY_ITEM_INDEX));
0285     }
0286 }