1 /************************************************************************
2 **
3 **  Copyright (C) 2015-2021 Kevin B. Hendricks, Stratford Ontario Canada
4 **  Copyright (C) 2011      John Schember <john@nachtimwald.com>
5 **  Copyright (C) 2011      Grzegorz Wolszczak <grzechu81@gmail.com>
6 **
7 **  This file is part of Sigil.
8 **
9 **  Sigil is free software: you can redistribute it and/or modify
10 **  it under the terms of the GNU General Public License as published by
11 **  the Free Software Foundation, either version 3 of the License, or
12 **  (at your option) any later version.
13 **
14 **  Sigil is distributed in the hope that it will be useful,
15 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
16 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 **  GNU General Public License for more details.
18 **
19 **  You should have received a copy of the GNU General Public License
20 **  along with Sigil.  If not, see <http://www.gnu.org/licenses/>.
21 **
22 *************************************************************************/
23 
24 #include <QtCore/QEvent>
25 #include <QtCore/QStringList>
26 #include <QtGui/QKeyEvent>
27 #include <QDebug>
28 
29 #include "KeyboardShortcutsWidget.h"
30 #include "Misc/KeyboardShortcutManager.h"
31 #include "Misc/SettingsStore.h"
32 
33 #ifdef Q_OS_WIN32
34 #include "Windows.h"
35 #endif
36 
37 #define DBG if(1)
38 
39 const int COL_NAME = 0;
40 const int COL_SHORTCUT = 1;
41 const int COL_DESCRIPTION = 2;
42 // This column is not displayed but we still need it so we can reference
43 // The short cut in the shortcut manager.
44 const int COL_ID = 3;
45 
KeyboardShortcutsWidget()46 KeyboardShortcutsWidget::KeyboardShortcutsWidget()
47     : m_EnableAltGr(false)
48 {
49     ui.setupUi(this);
50     connectSignalsSlots();
51     readSettings();
52 }
53 
saveSettings()54 PreferencesWidget::ResultActions KeyboardShortcutsWidget::saveSettings()
55 {
56     PreferencesWidget::ResultActions results = PreferencesWidget::ResultAction_None;
57 
58     KeyboardShortcutManager *sm = KeyboardShortcutManager::instance();
59 
60     for (int i = 0; i < ui.commandList->topLevelItemCount(); i++) {
61         QTreeWidgetItem *item = ui.commandList->topLevelItem(i);
62         const QString id = item->text(COL_ID);
63         const QKeySequence keySequence = item->text(COL_SHORTCUT);
64         sm->setKeySequence(id, keySequence);
65     }
66 
67     SettingsStore settings;
68     settings.setEnableAltGr(ui.EnableAltGr->checkState() == Qt::Checked);
69     if (m_EnableAltGr != ui.EnableAltGr->isChecked()) {
70         results = results | PreferencesWidget::ResultAction_RestartSigil;
71     }
72     return results;
73 }
74 
eventFilter(QObject * object,QEvent * event)75 bool KeyboardShortcutsWidget::eventFilter(QObject *object, QEvent *event)
76 {
77     Q_UNUSED(object)
78 
79     if (event->type() == QEvent::KeyPress) {
80         QKeyEvent *k = static_cast<QKeyEvent *>(event);
81         handleKeyEvent(k);
82         return true;
83     }
84 
85     if (event->type() == QEvent::Shortcut || event->type() == QEvent::KeyRelease) {
86         return true;
87     }
88 
89     if (event->type() == QEvent::ShortcutOverride) {
90         // For shortcut overrides, we need to accept as well
91         event->accept();
92         return true;
93     }
94 
95     return false;
96 }
97 
showEvent(QShowEvent * event)98 void KeyboardShortcutsWidget::showEvent(QShowEvent *event)
99 {
100     // Fill out the tree view
101     readSettings();
102     ui.commandList->resizeColumnToContents(COL_NAME);
103     ui.commandList->resizeColumnToContents(COL_SHORTCUT);
104     QWidget::showEvent(event);
105 }
106 
treeWidgetItemChangedSlot(QTreeWidgetItem * current,QTreeWidgetItem * previous)107 void KeyboardShortcutsWidget::treeWidgetItemChangedSlot(QTreeWidgetItem *current, QTreeWidgetItem *previous)
108 {
109     if (!current) {
110         ui.targetEdit->setText("");
111         ui.assignButton->setEnabled(false);
112         ui.removeButton->setEnabled(false);
113         ui.infoLabel->setText("");
114         return;
115     }
116 
117     const QString shortcut_text = current->text(COL_SHORTCUT);
118     ui.targetEdit->setText(shortcut_text);
119     ui.assignButton->setEnabled(false);
120     ui.removeButton->setEnabled(shortcut_text.length() > 0);
121 }
122 
removeButtonClicked()123 void KeyboardShortcutsWidget::removeButtonClicked()
124 {
125     QTreeWidgetItem *item = ui.commandList->currentItem();
126     if (item) {
127         item->setText(COL_SHORTCUT, "");
128     }
129 
130     ui.targetEdit->setText("");
131     ui.targetEdit->setFocus();
132     markSequencesAsDuplicatedIfNeeded();
133 }
134 
filterEditTextChangedSlot(const QString & text)135 void KeyboardShortcutsWidget::filterEditTextChangedSlot(const QString &text)
136 {
137     const QString newText = text.toUpper();
138 
139     // If text is empty - show all items.
140     if (text.isEmpty()) {
141         for (int i = 0; i < ui.commandList->topLevelItemCount(); i++) {
142             ui.commandList->topLevelItem(i)->setHidden(false);
143         }
144     } else {
145         for (int i = 0; i < ui.commandList->topLevelItemCount(); i++) {
146             const QString name = ui.commandList->topLevelItem(i)->text(COL_NAME).toUpper();
147             const QString description = ui.commandList->topLevelItem(i)->text(COL_DESCRIPTION).toUpper();
148             const QString sequence = ui.commandList->topLevelItem(i)->text(COL_SHORTCUT).toUpper();
149 
150             if (name.contains(newText) ||
151                 description.contains(newText) ||
152                 sequence.contains(newText)
153                ) {
154                 ui.commandList->topLevelItem(i)->setHidden(false);
155             } else {
156                 ui.commandList->topLevelItem(i)->setHidden(true);
157             }
158         }
159     }
160 }
161 
targetEditTextChangedSlot(const QString & text)162 void KeyboardShortcutsWidget::targetEditTextChangedSlot(const QString &text)
163 {
164     QTreeWidgetItem *item = ui.commandList->currentItem();
165 
166     if (item != 0) {
167         ui.assignButton->setEnabled(item->text(COL_SHORTCUT) != text);
168         ui.removeButton->setEnabled(item->text(COL_SHORTCUT).length() > 0);
169     }
170 
171     showDuplicatesTextIfNeeded();
172 }
173 
assignButtonClickedSlot()174 void KeyboardShortcutsWidget::assignButtonClickedSlot()
175 {
176     const QString new_shortcut = ui.targetEdit->text();
177 
178     if (ui.commandList->currentItem() != 0) {
179         ui.commandList->currentItem()->setText(COL_SHORTCUT, new_shortcut);
180     }
181 
182     if (new_shortcut.length() > 0) {
183         // Go through all items to remove any conflicting shortcuts
184         for (int i = 0; i < ui.commandList->topLevelItemCount(); i++) {
185             if (i != ui.commandList->currentIndex().row()) {
186                 QTreeWidgetItem *item = ui.commandList->topLevelItem(i);
187 
188                 if (item->text(COL_SHORTCUT) == new_shortcut) {
189                     item->setText(COL_SHORTCUT, "");
190                 }
191             }
192         }
193     }
194 
195     ui.infoLabel->clear();
196     markSequencesAsDuplicatedIfNeeded();
197     ui.assignButton->setEnabled(false);
198     ui.removeButton->setEnabled(new_shortcut.length() > 0);
199     ui.commandList->setFocus();
200 }
201 
resetAllButtonClickedSlot()202 void KeyboardShortcutsWidget::resetAllButtonClickedSlot()
203 {
204     KeyboardShortcutManager *sm = KeyboardShortcutManager::instance();
205 
206     // Go through all items
207     for (int i = 0; i < ui.commandList->topLevelItemCount(); i++) {
208         QTreeWidgetItem *item = ui.commandList->topLevelItem(i);
209         QKeySequence seq = sm->keyboardShortcut(item->text(COL_ID)).defaultKeySequence();
210 #ifdef Q_OS_MAC
211         item->setText(COL_SHORTCUT, seq.toString());
212 #else
213         item->setText(COL_SHORTCUT, seq.toString(QKeySequence::PortableText));
214 #endif
215     }
216 
217     markSequencesAsDuplicatedIfNeeded();
218 }
219 
readSettings()220 void KeyboardShortcutsWidget::readSettings()
221 {
222     KeyboardShortcutManager *sm = KeyboardShortcutManager::instance();
223     ui.commandList->clear();
224     QStringList ids = sm->ids();
225     foreach(QString id, ids) {
226         KeyboardShortcut shortcut = sm->keyboardShortcut(id);
227 
228         if (!shortcut.isEmpty()) {
229             QTreeWidgetItem *item = new QTreeWidgetItem();
230             item->setText(COL_NAME, shortcut.name());
231 #ifdef Q_OS_MAC
232             item->setText(COL_SHORTCUT, shortcut.keySequence().toString());
233 #else
234             item->setText(COL_SHORTCUT, shortcut.keySequence().toString(QKeySequence::PortableText));
235 #endif
236             item->setText(COL_DESCRIPTION, shortcut.description());
237             item->setToolTip(COL_DESCRIPTION, shortcut.toolTip());
238             item->setText(COL_ID, id);
239             ui.commandList->addTopLevelItem(item);
240         }
241     }
242     ui.commandList->sortItems(0, Qt::AscendingOrder);
243     markSequencesAsDuplicatedIfNeeded();
244 
245 #if defined(Q_OS_WIN32)
246     SettingsStore settings;
247     ui.EnableAltGr->setChecked(settings.enableAltGr());
248 #endif
249 #if !defined(Q_OS_WIN32)
250     ui.EnableAltGr->setVisible(false);
251     ui.EnableAltGr->setChecked(false);
252 #endif
253     m_EnableAltGr = ui.EnableAltGr->isChecked();
254 }
255 
connectSignalsSlots()256 void KeyboardShortcutsWidget::connectSignalsSlots()
257 {
258     connect(ui.commandList, SIGNAL(currentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *)), this, SLOT(treeWidgetItemChangedSlot(QTreeWidgetItem *, QTreeWidgetItem *)));
259     connect(ui.removeButton, SIGNAL(clicked()), this, SLOT(removeButtonClicked()));
260     connect(ui.filterEdit, SIGNAL(textChanged(QString)), this, SLOT(filterEditTextChangedSlot(QString)));
261     connect(ui.targetEdit, SIGNAL(textChanged(QString)), this, SLOT(targetEditTextChangedSlot(QString)));
262     connect(ui.assignButton, SIGNAL(clicked()), this, SLOT(assignButtonClickedSlot()));
263     connect(ui.resetAllButton, SIGNAL(clicked()), this, SLOT(resetAllButtonClickedSlot()));
264     ui.targetEdit->installEventFilter(this);
265 }
266 
handleKeyEvent(QKeyEvent * event)267 void KeyboardShortcutsWidget::handleKeyEvent(QKeyEvent *event)
268 {
269     int nextKey = event->key();
270 
271     if (nextKey == Qt::Key_Control    ||
272         nextKey == Qt::Key_Shift      ||
273         nextKey == Qt::Key_Meta       ||
274         nextKey == Qt::Key_Alt        ||
275         nextKey == Qt::Key_AltGr      ||
276         nextKey == Qt::Key_CapsLock   ||
277         nextKey == Qt::Key_NumLock    ||
278         nextKey == Qt::Key_ScrollLock ||
279         nextKey == 0                  ||
280         nextKey == Qt::Key_unknown    ||
281         nextKey == Qt::Key_Backspace  || // This button cannot be assigned, because we want to 'clear' shortcut after backspace push
282         ui.commandList->currentItem() == 0 // Do not allow writting in shortcut line edit if no item is selected
283        ) {
284         // If key was Qt::Key_Backspace additionaly clear sequence dialog
285         if (nextKey == Qt::Key_Backspace) {
286             removeButtonClicked();
287         }
288 
289         return;
290     }
291 
292     QString letter = QKeySequence(nextKey).toString(QKeySequence::PortableText);
293     Qt::KeyboardModifiers state = event->modifiers();
294 
295     DBG qDebug() << "key(): " << QString::number(nextKey) << "  " << QChar(nextKey);
296     DBG qDebug() << "text(): " << event->text();
297     DBG qDebug() << "nativeVirtualKey(): " << event->nativeVirtualKey();
298     DBG qDebug() << "letter: " << letter;
299     DBG qDebug() << "modifiers: " << state;
300 
301     // Key event generation for shortcuts is one of the most
302     // non-crossplatform things in all of Qt so lots of ifdefs
303 
304 #ifdef Q_OS_WIN32
305     if ((state & Qt::GroupSwitchModifier)) {
306         // The GroupSwitchModifier via windows altgr option on command line
307         // indicates this is AltGr which should not be used for Keyboard Shortcuts.
308         // it messes with QKeySequence.toString so fixup letter and turn off
309         // the GroupSwitchModifier
310         // letter = "" + QChar(nextKey);
311         // state = state & ~((int)Qt::GroupSwitchModifier);
312         return;
313     }
314     // try using the Windows call MapVirtualKeyW
315     const qint32 vk = event->nativeVirtualKey();
316     UINT  result = MapVirtualKeyW(vk, MAPVK_VK_TO_CHAR);
317     bool isDeadKey = (result & 0x80000000) == 0x80000000;
318     result = result & 0x0000FFFF;
319     DBG qDebug() << "MapVK_VK_TO_CHAR: " << QString::number(result) << " " << QChar(result) << " " << isDeadKey;
320     if (result != 0) {
321         // Dead keys (ie. diacritics should not be used in Keyboard Shortcuts
322         if (isDeadKey) return;
323         ui.targetEdit->setText(QKeySequence(result | state).toString(QKeySequence::PortableText));
324     } else {
325         if ((state & Qt::ShiftModifier) && letter.toUpper() == letter.toLower()) {
326             // remove the shift since it is included in the keycode
327             state = state & ~Qt::SHIFT;
328         }
329         ui.targetEdit->setText(QKeySequence(nextKey | state).toString(QKeySequence::PortableText));
330     }
331 #else  /* linux and macos */
332 
333 #ifdef Q_OS_MAC
334     // macOS treats some META+SHIFT+key sequences incorrectly so try to fix it up
335     if ((state & Qt::MetaModifier) && (letter.toUpper() == letter.toLower())) {
336         state = state & ~Qt::SHIFT;
337     }
338 #endif
339 
340     nextKey |= translateModifiers(state, event->text());
341     ui.targetEdit->setText(QKeySequence(nextKey).toString(QKeySequence::PortableText));
342 
343 #endif
344 
345     DBG qDebug() << "\n";
346 
347     event->accept();
348 }
349 
350 
translateModifiers(Qt::KeyboardModifiers state,const QString & text)351 int KeyboardShortcutsWidget::translateModifiers(Qt::KeyboardModifiers state, const QString &text)
352 {
353     int result = 0;
354 
355     // The shift modifier only counts when it is not used to type a symbol
356     // that is only reachable using the shift key anyway
357     if ((state & Qt::ShiftModifier) && (text.size() == 0
358                                         || !text.at(0).isPrint()
359                                         || text.at(0).isLetterOrNumber()
360                                         || text.at(0).isSpace()
361                                        )
362        ) {
363         result |= Qt::SHIFT;
364     }
365 
366     if (state & Qt::ControlModifier) {
367         result |= Qt::CTRL;
368     }
369 
370     if (state & Qt::MetaModifier) {
371         result |= Qt::META;
372     }
373 
374     if (state & Qt::AltModifier) {
375         result |= Qt::ALT;
376     }
377 
378     return result;
379 }
380 
markSequencesAsDuplicatedIfNeeded()381 void KeyboardShortcutsWidget::markSequencesAsDuplicatedIfNeeded()
382 {
383     // This is not optized , but since this will be called rather seldom
384     // effort for optimization may not be worth it
385     QMap<QKeySequence, QSet<QTreeWidgetItem *>> seqMap;
386 
387     // Go through all items
388     for (int i = 0; i < ui.commandList->topLevelItemCount(); i++) {
389         QTreeWidgetItem *item = ui.commandList->topLevelItem(i);
390         const QKeySequence keySequence = item->text(COL_SHORTCUT);
391         seqMap[keySequence].insert(item);
392     }
393 
394     // Now, mark all conflicting sequences
395     foreach(QKeySequence sequence, seqMap.keys()) {
396         QSet<QTreeWidgetItem *> itemSet = seqMap[sequence];
397 
398         if (itemSet.size() > 1) {
399             //mark items as conflicted items
400             foreach(QTreeWidgetItem * item, itemSet.values()) {
401                 QFont font = item->font(COL_SHORTCUT);
402                 font.setBold(true);
403                 item->setForeground(COL_SHORTCUT, QColor(Qt::red));
404                 item->setFont(COL_SHORTCUT, font);
405             }
406         } else {
407             // Mark as non-confilicted
408             foreach(QTreeWidgetItem * item, itemSet.values()) {
409                 QFont font = item->font(COL_SHORTCUT);
410                 font.setBold(false);
411                 item->setForeground(COL_SHORTCUT, QPalette().color(QPalette::Text));
412                 item->setFont(COL_SHORTCUT, font);
413             }
414         }
415     }
416 }
417 
showDuplicatesTextIfNeeded()418 void KeyboardShortcutsWidget::showDuplicatesTextIfNeeded()
419 {
420     ui.infoLabel->clear();
421     const QString new_shortcut = ui.targetEdit->text();
422 
423     if (new_shortcut.length() > 0) {
424         // Display any conflicting shortcuts in the info label
425         QStringList *conflicts = new QStringList();
426 
427         for (int i = 0; i < ui.commandList->topLevelItemCount(); i++) {
428             if (i != ui.commandList->currentIndex().row()) {
429                 QTreeWidgetItem *item = ui.commandList->topLevelItem(i);
430 
431                 if (item->text(COL_SHORTCUT) == new_shortcut) {
432                     conflicts->append(item->text(COL_NAME));
433                 }
434             }
435         }
436 
437         if (conflicts->count() > 0) {
438             ui.infoLabel->setText(tr("Conflicts with: <b>") + conflicts->join("</b>, <b>") + "</b>");
439         }
440     }
441 }
442