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 }