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 }