File indexing completed on 2024-04-28 16:54:25

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