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