1 /*
2 * Stellarium
3 * Copyright (C) 2012 Anton Samoylov
4 *
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
18 */
19
20 #include <QDialog>
21 #include <QStandardItemModel>
22 #include <QDebug>
23
24 #include "StelApp.hpp"
25 #include "StelGui.hpp"
26 #include "StelTranslator.hpp"
27 #include "StelActionMgr.hpp"
28 #include "ShortcutLineEdit.hpp"
29 #include "ShortcutsDialog.hpp"
30 #include "ui_shortcutsDialog.h"
31
32
ShortcutsFilterModel(QObject * parent)33 ShortcutsFilterModel::ShortcutsFilterModel(QObject* parent) :
34 QSortFilterProxyModel(parent)
35 {
36 //
37 }
38
filterAcceptsRow(int source_row,const QModelIndex & source_parent) const39 bool ShortcutsFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
40 {
41 if (filterRegExp().isEmpty())
42 return true;
43
44 if (source_parent.isValid())
45 {
46 QModelIndex index = source_parent.model()->index(source_row, filterKeyColumn(), source_parent);
47 QString data = sourceModel()->data(index, filterRole()).toString();
48 return data.contains(filterRegExp());
49 }
50 else
51 {
52 QModelIndex index = sourceModel()->index(source_row, filterKeyColumn());
53 for (int row = 0; row < sourceModel()->rowCount(index); row++)
54 {
55 if (filterAcceptsRow(row, index))
56 return true;
57 }
58 }
59 return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
60 }
61
62
ShortcutsDialog(QObject * parent)63 ShortcutsDialog::ShortcutsDialog(QObject* parent) :
64 StelDialog("Shortcuts", parent),
65 ui(new Ui_shortcutsDialogForm),
66 filterModel(new ShortcutsFilterModel(this)),
67 mainModel(new QStandardItemModel(this))
68 {
69 actionMgr = StelApp::getInstance().getStelActionManager();
70 }
71
~ShortcutsDialog()72 ShortcutsDialog::~ShortcutsDialog()
73 {
74 collisionItems.clear();
75 delete ui;
76 ui = Q_NULLPTR;
77 }
78
drawCollisions()79 void ShortcutsDialog::drawCollisions()
80 {
81 QBrush brush(Qt::red);
82 for (auto* item : collisionItems)
83 {
84 // change colors of all columns for better visibility
85 item->setForeground(brush);
86 QModelIndex index = item->index();
87 mainModel->itemFromIndex(index.sibling(index.row(), 1))->setForeground(brush);
88 mainModel->itemFromIndex(index.sibling(index.row(), 2))->setForeground(brush);
89 }
90 }
91
resetCollisions()92 void ShortcutsDialog::resetCollisions()
93 {
94 QBrush brush =
95 ui->shortcutsTreeView->palette().brush(QPalette::Foreground);
96 for (auto* item : collisionItems)
97 {
98 item->setForeground(brush);
99 QModelIndex index = item->index();
100 mainModel->itemFromIndex(index.sibling(index.row(), 1))->setForeground(brush);
101 mainModel->itemFromIndex(index.sibling(index.row(), 2))->setForeground(brush);
102 }
103 collisionItems.clear();
104 }
105
retranslate()106 void ShortcutsDialog::retranslate()
107 {
108 if (dialog)
109 {
110 ui->retranslateUi(dialog);
111 setModelHeader();
112 updateTreeData();
113 }
114 }
115
initEditors()116 void ShortcutsDialog::initEditors()
117 {
118 QModelIndex index = filterModel->mapToSource(ui->shortcutsTreeView->currentIndex());
119 index = index.sibling(index.row(), 0);
120 QStandardItem* currentItem = mainModel->itemFromIndex(index);
121 if (itemIsEditable(currentItem))
122 {
123 // current item is shortcut, not group (group items aren't selectable)
124 ui->primaryShortcutEdit->setEnabled(true);
125 ui->altShortcutEdit->setEnabled(true);
126 ui->restoreDefaultsButton->setEnabled(true);
127 // fill editors with item's shortcuts
128 QVariant data = mainModel->data(index.sibling(index.row(), 1));
129 ui->primaryShortcutEdit->setContents(data.value<QKeySequence>());
130 data = mainModel->data(index.sibling(index.row(), 2));
131 ui->altShortcutEdit->setContents(data.value<QKeySequence>());
132 }
133 else
134 {
135 // item is group, not shortcut
136 ui->primaryShortcutEdit->setEnabled(false);
137 ui->altShortcutEdit->setEnabled(false);
138 ui->applyButton->setEnabled(false);
139 ui->restoreDefaultsButton->setEnabled(false);
140 // https://wiki.qt.io/Technical_FAQ#Why_does_the_memory_keep_increasing_when_repeatedly_pasting_text_and_calling_clear.28.29_in_a_QLineEdit.3F
141 ui->primaryShortcutEdit->setText("");
142 ui->altShortcutEdit->setText("");
143 }
144 polish();
145 }
146
prefixMatchKeySequence(const QKeySequence & ks1,const QKeySequence & ks2)147 bool ShortcutsDialog::prefixMatchKeySequence(const QKeySequence& ks1,
148 const QKeySequence& ks2)
149 {
150 if (ks1.isEmpty() || ks2.isEmpty())
151 {
152 return false;
153 }
154 for (int i = 0; i < qMin(ks1.count(), ks2.count()); ++i)
155 {
156 if (ks1[i] != ks2[i])
157 {
158 return false;
159 }
160 }
161 return true;
162 }
163
findCollidingItems(QKeySequence ks)164 QList<QStandardItem*> ShortcutsDialog::findCollidingItems(QKeySequence ks)
165 {
166 QList<QStandardItem*> result;
167 for (int row = 0; row < mainModel->rowCount(); row++)
168 {
169 QStandardItem* group = mainModel->item(row, 0);
170 if (!group->hasChildren())
171 continue;
172 for (int subrow = 0; subrow < group->rowCount(); subrow++)
173 {
174 QKeySequence primary(group->child(subrow, 1)
175 ->data(Qt::DisplayRole).toString());
176 QKeySequence secondary(group->child(subrow, 2)
177 ->data(Qt::DisplayRole).toString());
178 if (prefixMatchKeySequence(ks, primary) ||
179 prefixMatchKeySequence(ks, secondary))
180 result.append(group->child(subrow, 0));
181 }
182 }
183 return result;
184 }
185
handleCollisions(ShortcutLineEdit * currentEdit)186 void ShortcutsDialog::handleCollisions(ShortcutLineEdit *currentEdit)
187 {
188 resetCollisions();
189
190 // handle collisions
191 QString text = currentEdit->text();
192 collisionItems = findCollidingItems(QKeySequence(text));
193 QModelIndex index =
194 filterModel->mapToSource(ui->shortcutsTreeView->currentIndex());
195 index = index.sibling(index.row(), 0);
196 QStandardItem* currentItem = mainModel->itemFromIndex(index);
197 collisionItems.removeOne(currentItem);
198 if (!collisionItems.isEmpty())
199 {
200 drawCollisions();
201 ui->applyButton->setEnabled(false);
202 // scrolling to first collision item
203 QModelIndex first = filterModel->mapFromSource(collisionItems.first()->index());
204 ui->shortcutsTreeView->scrollTo(first);
205 currentEdit->setProperty("collision", true);
206 }
207 else
208 {
209 // scrolling back to current item
210 QModelIndex current = filterModel->mapFromSource(index);
211 ui->shortcutsTreeView->scrollTo(current);
212 currentEdit->setProperty("collision", false);
213 }
214 }
215
handleChanges()216 void ShortcutsDialog::handleChanges()
217 {
218 // work only with changed editor
219 ShortcutLineEdit* editor = qobject_cast<ShortcutLineEdit*>(sender());
220 bool isPrimary = (editor == ui->primaryShortcutEdit);
221 // updating clear buttons
222 if (isPrimary)
223 {
224 ui->primaryBackspaceButton->setEnabled(!editor->isEmpty());
225 }
226 else
227 {
228 ui->altBackspaceButton->setEnabled(!editor->isEmpty());
229 }
230 // updating apply button
231 QModelIndex index = filterModel->mapToSource(ui->shortcutsTreeView->currentIndex());
232 if (!index.isValid() ||
233 (isPrimary && editor->text() == mainModel->data(index.sibling(index.row(), 1))) ||
234 (!isPrimary && editor->text() == mainModel->data(index.sibling(index.row(), 2))))
235 {
236 // nothing to apply
237 ui->applyButton->setEnabled(false);
238 }
239 else
240 {
241 ui->applyButton->setEnabled(true);
242 }
243 handleCollisions(editor);
244 polish();
245 }
246
applyChanges()247 void ShortcutsDialog::applyChanges()
248 {
249 // get ids stored in tree
250 QModelIndex index = filterModel->mapToSource(ui->shortcutsTreeView->currentIndex());
251 if (!index.isValid())
252 return;
253 index = index.sibling(index.row(), 0);
254 QStandardItem* currentItem = mainModel->itemFromIndex(index);
255 QString actionId = currentItem->data(Qt::UserRole).toString();
256
257 StelAction* action = actionMgr->findAction(actionId);
258 action->setShortcut(ui->primaryShortcutEdit->getKeySequence().toString());
259 action->setAltShortcut(ui->altShortcutEdit->getKeySequence().toString());
260 updateShortcutsItem(action);
261
262 // save shortcuts to file
263 actionMgr->saveShortcuts();
264
265 // nothing to apply until edits' content changes
266 ui->applyButton->setEnabled(false);
267 ui->restoreDefaultsButton->setEnabled(true);
268 }
269
switchToEditors(const QModelIndex & index)270 void ShortcutsDialog::switchToEditors(const QModelIndex& index)
271 {
272 QModelIndex mainIndex = filterModel->mapToSource(index);
273 QStandardItem* item = mainModel->itemFromIndex(mainIndex);
274 if (itemIsEditable(item))
275 {
276 ui->primaryShortcutEdit->setFocus();
277 }
278 }
279
createDialogContent()280 void ShortcutsDialog::createDialogContent()
281 {
282 ui->setupUi(dialog);
283 connect(ui->TitleBar, SIGNAL(movedTo(QPoint)), this, SLOT(handleMovedTo(QPoint)));
284
285 resetModel();
286 filterModel->setSourceModel(mainModel);
287 filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
288 filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
289 filterModel->setDynamicSortFilter(true);
290 filterModel->setSortLocaleAware(true);
291 ui->shortcutsTreeView->setModel(filterModel);
292 ui->shortcutsTreeView->header()->setSectionsMovable(false);
293 ui->shortcutsTreeView->sortByColumn(0, Qt::AscendingOrder);
294
295 // Kinetic scrolling
296 kineticScrollingList << ui->shortcutsTreeView;
297 StelGui* gui= dynamic_cast<StelGui*>(StelApp::getInstance().getGui());
298 if (gui)
299 {
300 enableKineticScrolling(gui->getFlagUseKineticScrolling());
301 connect(gui, SIGNAL(flagUseKineticScrollingChanged(bool)), this, SLOT(enableKineticScrolling(bool)));
302 }
303
304 connect(&StelApp::getInstance(), SIGNAL(languageChanged()), this, SLOT(retranslate()));
305 connect(ui->shortcutsTreeView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
306 this, SLOT(initEditors()));
307 connect(ui->shortcutsTreeView, SIGNAL(activated(QModelIndex)),
308 this, SLOT(switchToEditors(QModelIndex)));
309 connect(ui->lineEditSearch, SIGNAL(textChanged(QString)),
310 filterModel, SLOT(setFilterFixedString(QString)));
311
312 // apply button logic
313 connect(ui->applyButton, SIGNAL(clicked()), this, SLOT(applyChanges()));
314 // restore defaults button logic
315 connect(ui->restoreDefaultsButton, SIGNAL(clicked()), this, SLOT(restoreDefaultShortcuts()));
316 connect(ui->restoreAllDefaultsButton, SIGNAL(clicked()), this, SLOT(restoreAllDefaultShortcuts()));
317 // we need to disable all shortcut actions, so we can enter shortcuts without activating any actions
318 connect(ui->primaryShortcutEdit, SIGNAL(focusChanged(bool)), actionMgr, SLOT(setAllActionsEnabled(bool)));
319 connect(ui->altShortcutEdit, SIGNAL(focusChanged(bool)), actionMgr, SLOT(setAllActionsEnabled(bool)));
320 // handling changes in editors
321 connect(ui->primaryShortcutEdit, SIGNAL(contentsChanged()), this, SLOT(handleChanges()));
322 connect(ui->altShortcutEdit, SIGNAL(contentsChanged()), this, SLOT(handleChanges()));
323
324 QString backspaceChar;
325 backspaceChar.append(QChar(0x232B)); // Erase left
326 //test.append(QChar(0x2672));
327 //test.append(QChar(0x267B));
328 //test.append(QChar(0x267C));
329 //test.append(QChar(0x21BA)); // Counter-clockwise
330 //test.append(QChar(0x2221)); // Angle sign
331
332 updateTreeData();
333
334 // Let's improve visibility of the text
335 QString style = "QLabel { color: rgb(238, 238, 238); }";
336 ui->primaryLabel->setStyleSheet(style);
337 ui->altLabel->setStyleSheet(style);
338
339 // set initial focus to action search
340 ui->lineEditSearch->setFocus();
341 }
342
polish()343 void ShortcutsDialog::polish()
344 {
345 ui->primaryShortcutEdit->style()->unpolish(ui->primaryShortcutEdit);
346 ui->primaryShortcutEdit->style()->polish(ui->primaryShortcutEdit);
347 ui->altShortcutEdit->style()->unpolish(ui->altShortcutEdit);
348 ui->altShortcutEdit->style()->polish(ui->altShortcutEdit);
349 }
350
updateGroup(const QString & group)351 QStandardItem* ShortcutsDialog::updateGroup(const QString& group)
352 {
353 QStandardItem* groupItem = findItemByData(QVariant(group),
354 Qt::UserRole);
355 bool isNew = false;
356 if (!groupItem)
357 {
358 // create new
359 groupItem = new QStandardItem();
360 isNew = true;
361 }
362 // group items aren't selectable, so reset default flag
363 groupItem->setFlags(Qt::ItemIsEnabled);
364
365 // setup displayed text
366 groupItem->setText(q_(group));
367 // store id
368 groupItem->setData(group, Qt::UserRole);
369 groupItem->setColumnCount(3);
370 // setup bold font for group lines
371 QFont rootFont = groupItem->font();
372 rootFont.setBold(true);
373 // Font size is 14
374 rootFont.setPixelSize(StelApp::getInstance().getScreenFontSize()+1);
375 groupItem->setFont(rootFont);
376 if (isNew)
377 mainModel->appendRow(groupItem);
378
379
380 QModelIndex index = filterModel->mapFromSource(groupItem->index());
381 ui->shortcutsTreeView->expand(index);
382 ui->shortcutsTreeView->setFirstColumnSpanned(index.row(), QModelIndex(), true);
383 ui->shortcutsTreeView->setRowHidden(index.row(), QModelIndex(), false);
384
385 return groupItem;
386 }
387
findItemByData(QVariant value,int role,int column) const388 QStandardItem* ShortcutsDialog::findItemByData(QVariant value, int role, int column) const
389 {
390 for (int row = 0; row < mainModel->rowCount(); row++)
391 {
392 QStandardItem* item = mainModel->item(row, 0);
393 if (!item)
394 continue; //WTF?
395 if (column == 0)
396 {
397 if (item->data(role) == value)
398 return item;
399 }
400
401 for (int subrow = 0; subrow < item->rowCount(); subrow++)
402 {
403 QStandardItem* subitem = item->child(subrow, column);
404 if (subitem->data(role) == value)
405 return subitem;
406 }
407 }
408 return Q_NULLPTR;
409 }
410
updateShortcutsItem(StelAction * action,QStandardItem * shortcutItem)411 void ShortcutsDialog::updateShortcutsItem(StelAction *action,
412 QStandardItem *shortcutItem)
413 {
414 QVariant shortcutId(action->getId());
415 if (shortcutItem == Q_NULLPTR)
416 {
417 // search for item
418 shortcutItem = findItemByData(shortcutId, Qt::UserRole, 0);
419 }
420 // we didn't find item, create and add new
421 QStandardItem* groupItem = Q_NULLPTR;
422 if (shortcutItem == Q_NULLPTR)
423 {
424 // firstly search for group
425 QVariant groupId(action->getGroup());
426 groupItem = findItemByData(groupId, Qt::UserRole, 0);
427 if (groupItem == Q_NULLPTR)
428 {
429 // create and add new group to treeWidget
430 groupItem = updateGroup(action->getGroup());
431 }
432 // create shortcut item
433 shortcutItem = new QStandardItem();
434 shortcutItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
435 groupItem->appendRow(shortcutItem);
436 // store shortcut id, so we can find it when shortcut changed
437 shortcutItem->setData(shortcutId, Qt::UserRole);
438 QStandardItem* primaryItem = new QStandardItem();
439 QStandardItem* secondaryItem = new QStandardItem();
440 primaryItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
441 secondaryItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
442 groupItem->setChild(shortcutItem->row(), 1, primaryItem);
443 groupItem->setChild(shortcutItem->row(), 2, secondaryItem);
444 }
445 // setup properties of item
446 shortcutItem->setText(action->getText());
447 QModelIndex index = shortcutItem->index();
448 mainModel->setData(index.sibling(index.row(), 1),
449 action->getShortcut(), Qt::DisplayRole);
450 mainModel->setData(index.sibling(index.row(), 2),
451 action->getAltShortcut(), Qt::DisplayRole);
452 }
453
restoreAllDefaultShortcuts()454 void ShortcutsDialog::restoreAllDefaultShortcuts()
455 {
456 if (askConfirmation())
457 {
458 qDebug() << "[Shortcuts] restore defaults...";
459 resetModel();
460 actionMgr->restoreDefaultShortcuts();
461 updateTreeData();
462 initEditors();
463 }
464 else
465 qDebug() << "[Shortcuts] restore defaults is canceled...";
466 }
467
restoreDefaultShortcuts()468 void ShortcutsDialog::restoreDefaultShortcuts()
469 {
470 // get ids stored in tree
471 QModelIndex index = filterModel->mapToSource(ui->shortcutsTreeView->currentIndex());
472 if (!index.isValid())
473 return;
474 index = index.sibling(index.row(), 0);
475 QStandardItem* currentItem = mainModel->itemFromIndex(index);
476 QString actionId = currentItem->data(Qt::UserRole).toString();
477
478 StelAction* action = actionMgr->findAction(actionId);
479 if (action)
480 {
481 actionMgr->restoreDefaultShortcut(action);
482 updateShortcutsItem(action);
483 ui->primaryShortcutEdit->setText(action->getShortcut().toString());
484 ui->altShortcutEdit->setText(action->getAltShortcut().toString());
485 // nothing to apply until edits' content changes
486 ui->applyButton->setEnabled(false);
487 ui->restoreDefaultsButton->setEnabled(false);
488 }
489 }
490
updateTreeData()491 void ShortcutsDialog::updateTreeData()
492 {
493 // Create shortcuts tree
494 QStringList groups = actionMgr->getGroupList();
495 for (const auto& group : groups)
496 {
497 updateGroup(group);
498 // display group's shortcuts
499 QList<StelAction*> actions = actionMgr->getActionList(group);
500 for (auto* action : actions)
501 {
502 updateShortcutsItem(action);
503 }
504 }
505 // ajust columns
506 for(int i=0; i<3; i++)
507 ui->shortcutsTreeView->resizeColumnToContents(i);
508 }
509
itemIsEditable(QStandardItem * item)510 bool ShortcutsDialog::itemIsEditable(QStandardItem *item)
511 {
512 if (item == Q_NULLPTR) return false;
513 // non-editable items(not group items) have no Qt::ItemIsSelectable flag
514 return (Qt::ItemIsSelectable & item->flags());
515 }
516
resetModel()517 void ShortcutsDialog::resetModel()
518 {
519 mainModel->clear();
520 setModelHeader();
521 }
522
setModelHeader()523 void ShortcutsDialog::setModelHeader()
524 {
525 QStringList headerLabels;
526 headerLabels << q_("Action") << qc_("Primary shortcut","column name") << qc_("Alternative shortcut","column name");
527 mainModel->setHorizontalHeaderLabels(headerLabels);
528 }
529