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