1 /*
2 For general Scribus (>=1.3.2) copyright and licensing information please refer
3 to the COPYING file provided with the program. Following this notice may exist
4 a copyright and/or license notice that predates the release of Scribus 1.3.2
5 for which a new license (GPL+exception) is in place.
6 */
7 
8 #include <QDebug>
9 #include <QDomDocument>
10 #include <QFileDialog>
11 #include <QInputDialog>
12 #include <QMessageBox>
13 
14 #include "actionmanager.h"
15 #include "commonstrings.h"
16 #include "iconmanager.h"
17 #include "pluginmanager.h"
18 #include "prefscontext.h"
19 #include "prefsfile.h"
20 #include "prefsmanager.h"
21 #include "prefsstructs.h"
22 #include "scpaths.h"
23 #include "scplugin.h"
24 #include "scraction.h"
25 #include "ui/prefs_keyboardshortcuts.h"
26 #include "ui/scmessagebox.h"
27 #include "util.h"
28 
29 
Prefs_KeyboardShortcuts(QWidget * parent,ScribusDoc * doc)30 Prefs_KeyboardShortcuts::Prefs_KeyboardShortcuts(QWidget* parent, ScribusDoc* doc)
31 	: Prefs_Pane(parent)
32 {
33 	setupUi(this);
34 	languageChange();
35 
36 	m_caption = tr("Keyboard Shortcuts");
37 	m_icon = "16/preferences-desktop-keyboard-shortcuts.png";
38 
39 	defMenus=ActionManager::defaultMenus();
40 	defNonMenuActions=ActionManager::defaultNonMenuActions();
41 
42 	QVector< QPair<QString, QStringList> >::Iterator itnmenua = defNonMenuActions->begin();
43 	PluginManager& pluginManager(PluginManager::instance());
44 	QStringList pluginNames(pluginManager.pluginNames(false));
45 	ScPlugin* plugin = nullptr;
46 	ScActionPlugin* ixplug = nullptr;
47 	QString pName;
48 	for (int i = 0; i < pluginNames.count(); ++i)
49 	{
50 		pName = pluginNames.at(i);
51 		plugin = pluginManager.getPlugin(pName, true);
52 		Q_ASSERT(plugin); // all the returned names should represent loaded plugins
53 		if (plugin->inherits("ScActionPlugin"))
54 		{
55 			ixplug = qobject_cast<ScActionPlugin*>(plugin);
56 			Q_ASSERT(ixplug);
57 			ScActionPlugin::ActionInfo ai(ixplug->actionInfo());
58 			itnmenua->second << ai.name;
59 		}
60 	}
61 
62 	Q_CHECK_PTR(defMenus);
63 	lviToActionMap.clear();
64 	lviToMenuMap.clear();
65 	keyTable->clear();
66 	keyMap.clear();
67 	keyCode = 0;
68 	keyDisplay->setMinimumWidth(fontMetrics().horizontalAdvance("CTRL+ALT+SHIFT+W"));
69 	keyDisplay->setText("");
70 	selectedLVI = nullptr;
71 
72 	clearSearchButton->setIcon(IconManager::instance().loadIcon("clear_right.png"));
73 	// signals and slots connections
74 	connect( keyTable, SIGNAL(currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)), this, SLOT(dispKey(QTreeWidgetItem*, QTreeWidgetItem*)));
75 	connect( noKey, SIGNAL(clicked()), this, SLOT(setNoKey()));
76 	connect( setKeyButton, SIGNAL(clicked()), this, SLOT(setKeyText()));
77 	connect( loadSetButton, SIGNAL(clicked()), this, SLOT(loadKeySetFile()));
78 	connect( importSetButton, SIGNAL(clicked()), this, SLOT(importKeySetFile()));
79 	connect( exportSetButton, SIGNAL(clicked()), this, SLOT(exportKeySetFile()));
80 	connect( resetSetButton, SIGNAL(clicked()), this, SLOT(resetKeySet()));
81 	connect( clearSearchButton, SIGNAL(clicked()), this, SLOT(clearSearchString()));
82 	connect( searchTextLineEdit, SIGNAL(textChanged(const QString&)), this, SLOT(applySearch(const QString&)));
83 
84 }
85 
86 Prefs_KeyboardShortcuts::~Prefs_KeyboardShortcuts() = default;
87 
languageChange()88 void Prefs_KeyboardShortcuts::languageChange()
89 {
90 }
91 
restoreDefaults(struct ApplicationPrefs * prefsData)92 void Prefs_KeyboardShortcuts::restoreDefaults(struct ApplicationPrefs *prefsData)
93 {
94 	keyMap=prefsData->keyShortcutPrefs.KeyActions;
95 	loadableSets->clear();
96 	loadableSets->addItems(scanForSets());
97 	insertActions();
98 	dispKey(nullptr);
99 }
100 
saveGuiToPrefs(struct ApplicationPrefs * prefsData) const101 void Prefs_KeyboardShortcuts::saveGuiToPrefs(struct ApplicationPrefs *prefsData) const
102 {
103 	prefsData->keyShortcutPrefs.KeyActions=keyMap;
104 }
105 
setNoKey()106 void Prefs_KeyboardShortcuts::setNoKey()
107 {
108 	if (noKey->isChecked())
109 	{
110 		if (selectedLVI!=nullptr)
111 		{
112 			selectedLVI->setText(1,"");
113 			keyMap[lviToActionMap[selectedLVI]].keySequence=QKeySequence();
114 		}
115 		keyDisplay->setText("");
116 		noKey->setChecked(true);
117 	}
118 }
119 
loadKeySetFile()120 void Prefs_KeyboardShortcuts::loadKeySetFile()
121 {
122 	if (keySetList.contains(loadableSets->currentText()))
123 		importKeySet(keySetList[loadableSets->currentText()]);
124 }
125 
importKeySetFile()126 void Prefs_KeyboardShortcuts::importKeySetFile()
127 {
128 	PrefsContext* dirs = PrefsManager::instance().prefsFile->getContext("dirs");
129 	QString currentPath = dirs->get("keymapprefs_import", ScPaths::instance().shareDir() + "keysets/");
130 	QString s = QFileDialog::getOpenFileName(this, tr("Select a Key set file to read"), currentPath, tr("Key Set XML Files (*.xml)"));
131 	if (!s.isEmpty())
132 		importKeySet(s);
133 }
exportKeySetFile()134 void Prefs_KeyboardShortcuts::exportKeySetFile()
135 {
136 	PrefsContext* dirs = PrefsManager::instance().prefsFile->getContext("dirs");
137 	QString currentPath= dirs->get("keymapprefs_export", ".");
138 	QString s = QFileDialog::getSaveFileName(this, tr("Select a Key set file to save to"), currentPath, tr("Key Set XML Files (*.xml)") );
139 	if (!s.isEmpty())
140 		exportKeySet(s);
141 }
142 
importKeySet(const QString & filename)143 void Prefs_KeyboardShortcuts::importKeySet(const QString& filename)
144 {
145 	searchTextLineEdit->clear();
146 	QFileInfo fi = QFileInfo(filename);
147 	if (fi.exists())
148 	{
149 		//import the file into qdomdoc
150 		QDomDocument doc( "keymapentries" );
151 		QFile file1( filename );
152 		if ( !file1.open( QIODevice::ReadOnly ) )
153 			return;
154 		QTextStream ts(&file1);
155 		ts.setCodec("UTF-8");
156 		QString errorMsg;
157 		int eline;
158 		int ecol;
159 		if ( !doc.setContent( ts.readAll(), &errorMsg, &eline, &ecol ))
160 		{
161 			qDebug("%s", QString("Could not open key set file: %1\nError:%2 at line: %3, row: %4").arg(filename, errorMsg).arg(eline).arg(ecol).toLatin1().constData());
162 			file1.close();
163 			return;
164 		}
165 		file1.close();
166 		//load the file now
167 		QDomElement docElem = doc.documentElement();
168 		if (docElem.tagName()=="shortcutset" && docElem.hasAttribute("name"))
169 		{
170 			QDomAttr keysetAttr = docElem.attributeNode( "name" );
171 
172 			//clear current menu entries
173 			for (QMap<QString,Keys>::Iterator it=keyMap.begin(); it!=keyMap.end(); ++it)
174 				it.value().keySequence = QKeySequence();
175 
176 			//load in new set
177 			QDomNode n = docElem.firstChild();
178 			while (!n.isNull())
179 			{
180 				QDomElement e = n.toElement(); // try to convert the node to an element.
181 				if (!e.isNull())
182 				{
183 					if (e.hasAttribute("name")  && e.hasAttribute( "shortcut" ))
184 					{
185 						QDomAttr nameAttr = e.attributeNode( "name" );
186 						QDomAttr shortcutAttr = e.attributeNode( "shortcut" );
187 						if (keyMap.contains(nameAttr.value()))
188 							keyMap[nameAttr.value()].keySequence=QKeySequence(shortcutAttr.value());
189 					}
190 				}
191 				n = n.nextSibling();
192 			}
193 		}
194 	}
195 	insertActions();
196 }
197 
exportKeySet(const QString & filename)198 bool Prefs_KeyboardShortcuts::exportKeySet(const QString& filename)
199 {
200 	QString exportFileName;
201 	if (filename.endsWith(".xml"))
202 		exportFileName = filename;
203 	else
204 		exportFileName = filename+".xml";
205 	if (overwrite(this, exportFileName))
206 	{
207 		bool ok;
208 		QString setName = QInputDialog::getText(this, tr("Export Keyboard Shortcuts to File"), tr("Enter the name of the shortcut set:"), QLineEdit::Normal, QString(), &ok);
209 		if (!( ok && !setName.isEmpty()) )
210 			return false;
211 
212 		QDomDocument doc( "keymapentries" );
213 		QString keyset=QString("<shortcutset name=\"%1\"></shortcutset>").arg(setName);
214 		doc.setContent(keyset);
215 		QDomElement keySetElement=doc.documentElement();
216 		QMap<QString,Keys>::Iterator itEnd=keyMap.end();
217 		for (QMap<QString,Keys>::Iterator it=keyMap.begin(); it!=itEnd; ++it)
218 		{
219 			if (it.value().keySequence.isEmpty() && it.key().isEmpty())
220 				continue;
221 			QDomElement function_shortcut = doc.createElement("function");
222 			function_shortcut.setAttribute("name", it.key());
223 			function_shortcut.setAttribute("shortcut", getKeyText(it.value().keySequence));
224 			keySetElement.appendChild(function_shortcut);
225 		}
226 		QFile f(filename);
227 		if(!f.open(QIODevice::WriteOnly))
228 			return false;
229 		QDataStream s(&f);
230 		QByteArray xmltag("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
231 		s.writeRawData(xmltag.data(), xmltag.length());
232 		QByteArray xmldoc = doc.toByteArray(4);
233 		s.writeRawData(xmldoc, xmldoc.length());
234 		f.close();
235 	}
236 	return true;
237 }
238 
resetKeySet()239 void Prefs_KeyboardShortcuts::resetKeySet()
240 {
241 	QString location=ScPaths::instance().shareDir();
242 	QString defaultKeySetFileName=QDir::toNativeSeparators(location+"keysets/scribus15.xml");
243 	importKeySet(defaultKeySetFileName);
244 }
245 
scanForSets()246 QStringList Prefs_KeyboardShortcuts::scanForSets()
247 {
248 	keySetList.clear();
249 	QString location(ScPaths::instance().shareDir());
250 	QDir keySetsDir(QDir::toNativeSeparators(location+"keysets/"), "*.xml", QDir::Name, QDir::Files | QDir::NoSymLinks);
251 	if ((!keySetsDir.exists()) || (keySetsDir.count() <= 0))
252 		return QStringList();
253 
254 	QStringList appNames;
255 	for (uint fileCounter = 0; fileCounter < keySetsDir.count(); ++fileCounter)
256 	{
257 		QString filename(QDir::toNativeSeparators(location+"keysets/"+keySetsDir[fileCounter]));
258 
259 		QDomDocument doc( "keymapentries" );
260 		QFile file( filename );
261 		if (!file.open( QIODevice::ReadOnly))
262 			continue;
263 		QString errorMsg;
264 		int eline;
265 		int ecol;
266 
267 		if (!doc.setContent( &file, &errorMsg, &eline, &ecol ))
268 		{
269 			qDebug("%s", QString("Could not open key set file: %1\nError:%2 at line: %3, row: %4").arg(keySetsDir[fileCounter], errorMsg).arg(eline).arg(ecol).toLatin1().constData());
270 			file.close();
271 			continue;
272 		}
273 		file.close();
274 
275 		QDomElement docElem = doc.documentElement();
276 		if (docElem.tagName() == "shortcutset" && docElem.hasAttribute("name"))
277 		{
278 			QDomAttr nameAttr = docElem.attributeNode( "name" );
279 			appNames.append(nameAttr.value());
280 			keySetList.insert(nameAttr.value(), filename);
281 		}
282 	}
283 	return QStringList(appNames);
284 }
285 
getKeyText(const QKeySequence & KeyC)286 QString Prefs_KeyboardShortcuts::getKeyText(const QKeySequence& KeyC)
287 {
288 	return KeyC.toString();
289 }
290 
getTrKeyText(const QKeySequence & KeyC)291 QString Prefs_KeyboardShortcuts::getTrKeyText(const QKeySequence& KeyC)
292 {
293 	return KeyC.toString(QKeySequence::NativeText);
294 }
295 
setKeyText()296 void Prefs_KeyboardShortcuts::setKeyText()
297 {
298 	if (keyTable->currentItem()==nullptr)
299 	{
300 		setKeyButton->setChecked(false);
301 		return;
302 	}
303 	if (setKeyButton->isChecked())
304 	{
305 		keyCode = 0;
306 		grabKeyboard();
307 	}
308 	else
309 		releaseKeyboard();
310 }
311 
insertActions()312 void Prefs_KeyboardShortcuts::insertActions()
313 {
314 	lviToActionMap.clear();
315 	lviToMenuMap.clear();
316 	keyTable->clear();
317 	bool first, firstMenu=true;
318 	QTreeWidgetItem *currLVI = nullptr;
319 	QTreeWidgetItem *currMenuLVI = nullptr;
320 	QTreeWidgetItem *prevLVI = nullptr;
321 	QTreeWidgetItem *prevMenuLVI = nullptr;
322 	for (int i = 0; i < defMenus->count(); ++i)
323 	{
324 		const QPair<QString, QStringList> &actionStrings = defMenus->at(i);
325 		if (firstMenu)
326 		{
327 			currMenuLVI = new QTreeWidgetItem(keyTable);
328 			firstMenu = false;
329 		}
330 		else
331 			currMenuLVI = new QTreeWidgetItem(keyTable, prevMenuLVI);
332 		Q_CHECK_PTR(currMenuLVI);
333 		lviToMenuMap.append(currMenuLVI);
334 		currMenuLVI->setText(0, actionStrings.first);
335 		currMenuLVI->setExpanded(true);
336 		currMenuLVI->setFlags(Qt::ItemIsEnabled);
337 		prevMenuLVI=currMenuLVI;
338 		first=true;
339 		currLVI=nullptr;
340 		prevLVI=nullptr;
341 		for (int j = 0; j < actionStrings.second.count(); ++j)
342 		{
343 			QString actionName = actionStrings.second.at(j);
344 			if (!keyMap.contains(actionName))
345 			{
346 				qDebug() << "The action " << actionName << " is not defined in shortcut map";
347 				continue;
348 			}
349 			const Keys &actionKeys = keyMap[actionName];
350 			if (actionKeys.cleanMenuText.isEmpty())
351 				continue;
352 			if (first)
353 			{
354 				currLVI = new QTreeWidgetItem(currMenuLVI);
355 				first = false;
356 			}
357 			else
358 				currLVI = new QTreeWidgetItem(currMenuLVI, prevLVI);
359 			Q_CHECK_PTR(currLVI);
360 			lviToActionMap.insert(currLVI, actionName);
361 			currLVI->setText(0, actionKeys.cleanMenuText);
362 			currLVI->setText(1, actionKeys.keySequence.toString(QKeySequence::NativeText));
363 			prevLVI=currLVI;
364 		}
365 	}
366 	//Non menu actions
367 	for (int i = 0; i < defNonMenuActions->count(); ++i)
368 	{
369 		const QPair<QString, QStringList> &actionStrings = defNonMenuActions->at(i);
370 		if (firstMenu)
371 		{
372 			currMenuLVI = new QTreeWidgetItem(keyTable);
373 			firstMenu = false;
374 		}
375 		else
376 			currMenuLVI = new QTreeWidgetItem(keyTable, prevMenuLVI);
377 		Q_CHECK_PTR(currMenuLVI);
378 		lviToMenuMap.append(currMenuLVI);
379 		currMenuLVI->setText(0, actionStrings.first);
380 		currMenuLVI->setExpanded(true);
381 		currMenuLVI->setFlags(Qt::ItemIsEnabled);
382 		prevMenuLVI=currMenuLVI;
383 		first=true;
384 		currLVI=nullptr;
385 		prevLVI=nullptr;
386 		for (int j = 0; j < actionStrings.second.count(); ++j)
387 		{
388 			QString actionName = actionStrings.second.at(j);
389 			if (!keyMap.contains(actionName))
390 			{
391 				qDebug() << "The action " << actionName << " is not defined in shortcut map";
392 				continue;
393 			}
394 			const Keys &actionKeys = keyMap[actionName];
395 			if (actionKeys.cleanMenuText.isEmpty())
396 				continue;
397 			if (first)
398 			{
399 				currLVI=new QTreeWidgetItem(currMenuLVI);
400 				first=false;
401 			}
402 			else
403 				currLVI=new QTreeWidgetItem(currMenuLVI, prevLVI);
404 			Q_CHECK_PTR(currLVI);
405 			lviToActionMap.insert(currLVI, actionName);
406 			currLVI->setText(0, actionKeys.cleanMenuText);
407 			currLVI->setText(1, actionKeys.keySequence.toString(QKeySequence::NativeText));
408 			prevLVI=currLVI;
409 		}
410 	}
411 	keyTable->resizeColumnToContents(0);
412 }
413 
applySearch(const QString & newss)414 void Prefs_KeyboardShortcuts::applySearch( const QString & newss )
415 {
416 	//Must run this as if newss is not empty and we go to the next for loop, the set visible doesn't work
417 	for (auto it = lviToMenuMap.begin(); it != lviToMenuMap.end(); ++it)
418 		(*it)->setHidden(false);
419 	if (newss.isEmpty())
420 	{
421 		for (auto it = lviToActionMap.begin(); it != lviToActionMap.end(); ++it)
422 			it.key()->setHidden(false);
423 		return;
424 	}
425 	//Seem to need to do this.. isOpen doesn't seem to do what it says
426 	for (auto it = lviToActionMap.begin(); it != lviToActionMap.end(); ++it)
427 	{
428 		if (it.key()->text(0).contains(newss, Qt::CaseInsensitive))
429 			it.key()->setHidden(false);
430 		else
431 			it.key()->setHidden(true);
432 	}
433 }
434 
dispKey(QTreeWidgetItem * qlvi,QTreeWidgetItem *)435 void Prefs_KeyboardShortcuts::dispKey(QTreeWidgetItem* qlvi, QTreeWidgetItem*)
436 {
437 	if (setKeyButton->isChecked())
438 	{
439 		releaseKeyboard();
440 		setKeyButton->setChecked(false);
441 	}
442 	if (qlvi!=nullptr && lviToActionMap.contains(qlvi))
443 	{
444 		selectedLVI = qlvi;
445 		QString actionName=lviToActionMap[qlvi];
446 		if (actionName.isEmpty())
447 			return;
448 		keyDisplay->setText(keyMap[actionName].keySequence.toString(QKeySequence::NativeText));
449 		if (keyMap[actionName].keySequence.isEmpty())
450 			noKey->setChecked(true);
451 		else
452 			userDef->setChecked(true);
453 	}
454 	else
455 	{
456 		noKey->setChecked(true);
457 		keyDisplay->setText("");
458 		selectedLVI = nullptr;
459 	}
460 	noKey->setEnabled(selectedLVI != nullptr);
461 	userDef->setEnabled(selectedLVI != nullptr);
462 	setKeyButton->setEnabled(selectedLVI != nullptr);
463 	keyDisplay->setEnabled(selectedLVI != nullptr);
464 }
465 
event(QEvent * ev)466 bool Prefs_KeyboardShortcuts::event( QEvent* ev )
467 {
468 	bool ret = QWidget::event( ev );
469 	if ( ev->type() == QEvent::KeyPress )
470 		keyPressEvent((QKeyEvent*)ev);
471 	if ( ev->type() == QEvent::KeyRelease )
472 		keyReleaseEvent((QKeyEvent*)ev);
473 	return ret;
474 }
475 
keyPressEvent(QKeyEvent * k)476 void Prefs_KeyboardShortcuts::keyPressEvent(QKeyEvent *k)
477 {
478 	if (setKeyButton->isChecked())
479 	{
480 		switch (k->key())
481 		{
482 			case Qt::Key_Meta:
483 				keyCode |= Qt::META;
484 				break;
485 			case Qt::Key_Shift:
486 				keyCode |= Qt::SHIFT;
487 				break;
488 			case Qt::Key_Alt:
489 				keyCode |= Qt::ALT;
490 				break;
491 			case Qt::Key_Control:
492 				keyCode |= Qt::CTRL;
493 				break;
494 			default:
495 				keyCode |= k->key();
496 				keyDisplay->setText(getTrKeyText(keyCode));
497 				releaseKeyboard();
498 				if (selectedLVI)
499 				{
500 					QString actionName = lviToActionMap[selectedLVI];
501 					if (checkKey(keyCode))
502 					{
503 						ScMessageBox::information(this, CommonStrings::trWarning, tr("The %1 key sequence is already in use by \"%2\"").arg(getTrKeyText(keyCode),getAction(keyCode)));
504 						selectedLVI->setText(1,keyMap[actionName].keySequence.toString(QKeySequence::NativeText));
505 						keyDisplay->setText(keyMap[actionName].keySequence.toString(QKeySequence::NativeText));
506 					}
507 					else
508 					{
509 						QKeySequence newKeySequence(keyCode);
510 						selectedLVI->setText(1, newKeySequence.toString(QKeySequence::NativeText));
511 						keyMap[actionName].keySequence=newKeySequence;
512 						userDef->setChecked(true);
513 					}
514 				}
515 				setKeyButton->setChecked(false);
516 		}
517 	}
518 	if (setKeyButton->isChecked())
519 		keyDisplay->setText(getTrKeyText(keyCode));
520 }
521 
keyReleaseEvent(QKeyEvent * k)522 void Prefs_KeyboardShortcuts::keyReleaseEvent(QKeyEvent *k)
523 {
524 	if (setKeyButton->isChecked())
525 	{
526 		if (k->key() == Qt::Key_Meta)
527 			keyCode &= ~Qt::META;
528 		if (k->key() == Qt::Key_Shift)
529 			keyCode &= ~Qt::SHIFT;
530 		if (k->key() == Qt::Key_Alt)
531 			keyCode &= ~Qt::ALT;
532 		if (k->key() == Qt::Key_Control)
533 			keyCode &= ~Qt::CTRL;
534 		keyDisplay->setText(getTrKeyText(keyCode));
535 	}
536 }
537 
getAction(int code)538 QString Prefs_KeyboardShortcuts::getAction(int code)
539 {
540 	QString ret;
541 	QKeySequence key = QKeySequence(code);
542 	for (QMap<QString,Keys>::Iterator it=keyMap.begin(); it!=keyMap.end(); ++it)
543 	{
544 		if (key.matches(it.value().keySequence) != QKeySequence::NoMatch)
545 		{
546 			ret = it->cleanMenuText;
547 			break;
548 		}
549 	}
550 	return ret;
551 }
552 
checkKey(int code)553 bool Prefs_KeyboardShortcuts::checkKey(int code)
554 {
555 	bool ret = false;
556 	QKeySequence key = QKeySequence(code);
557 	for (QMap<QString,Keys>::Iterator it=keyMap.begin(); it!=keyMap.end(); ++it)
558 	{
559 		if (key.matches(it.value().keySequence) != QKeySequence::NoMatch)
560 		{
561 			ret = true;
562 			break;
563 		}
564 	}
565 	return ret;
566 }
567 
clearSearchString()568 void Prefs_KeyboardShortcuts::clearSearchString( )
569 {
570 	searchTextLineEdit->clear();
571 }
572 
573