diff --git a/autotests/kxmlgui_unittest.cpp b/autotests/kxmlgui_unittest.cpp index 1d23d3b..0889edf 100644 --- a/autotests/kxmlgui_unittest.cpp +++ b/autotests/kxmlgui_unittest.cpp @@ -1,1099 +1,1110 @@ /* This file is part of the KDE libraries Copyright 2007-2009 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kxmlgui_unittest.h" #include "testxmlguiwindow.h" #include "testguiclient.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include // it's not exported, so we need to include the code here #include QTEST_MAIN(KXmlGui_UnitTest) enum Flags { NoFlags = 0, AddToolBars = 1, AddModifiedToolBars = 2, AddActionProperties = 4, AddModifiedMenus = 8 // next item is 16 }; static void createXmlFile(QFile &file, int version, int flags, const QByteArray &toplevelTag = "gui") { const QByteArray xml = "\n" "\n" "<" + toplevelTag + " version=\"" + QByteArray::number(version) + "\" name=\"foo\" >\n" "\n"; file.write(xml); if (flags & AddModifiedMenus) { file.write( "\n" "&File\n" "\n" "\n" ); } file.write("\n"); if (flags & AddToolBars) { file.write( "\n" " Main Toolbar\n" " \n" "\n" "\n" " Bookmark Toolbar\n" "\n" ); } if (flags & AddModifiedToolBars) { file.write( "\n" " Main Toolbar\n" " \n" "\n" "\n" " Modified toolbars\n" "\n" ); } if (flags & AddActionProperties) { file.write( "\n" " \n" "\n" ); } file.write("\n"); } static void clickApply(KEditToolBar *dialog) { QDialogButtonBox *box = dialog->findChild(); Q_ASSERT(box != nullptr); box->button(QDialogButtonBox::Apply)->setEnabled(true); box->button(QDialogButtonBox::Apply)->click(); } void KXmlGui_UnitTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); // Leftover configuration breaks testAutoSaveSettings const QString configFile = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, KSharedConfig::openConfig()->name()); if (!configFile.isEmpty()) { qDebug() << "Removing old config file"; QFile::remove(configFile); KSharedConfig::openConfig()->reparseConfiguration(); } } void KXmlGui_UnitTest::testFindVersionNumber_data() { QTest::addColumn("xml"); QTest::addColumn("version"); QTest::newRow("simple test") << "\n" "\n" "\n" << "3"; QTest::newRow("two digits") << "\n" "\n" << "42"; QTest::newRow("with spaces") << // as found in dirfilterplugin.rc for instance "\n" "\n" << "1"; QTest::newRow("with a dot") << // as was found in autorefresh.rc "\n" "\n" << QString() /*error*/; QTest::newRow("with a comment") << // as found in kmail.rc "\n" "\n" "\n" << "452"; } void KXmlGui_UnitTest::testFindVersionNumber() { QFETCH(QString, xml); QFETCH(QString, version); QCOMPARE(KXmlGuiVersionHandler::findVersionNumber(xml), version); } void KXmlGui_UnitTest::testVersionHandlerSameVersion() { // This emulates the case where the user has modified stuff locally // and the application hasn't changed since, so the version number is unchanged. QTemporaryFile userFile; QVERIFY(userFile.open()); createXmlFile(userFile, 2, AddActionProperties | AddModifiedToolBars); const QString firstFile = userFile.fileName(); QTemporaryFile appFile; QVERIFY(appFile.open()); createXmlFile(appFile, 2, AddToolBars); const QString secondFile = appFile.fileName(); QStringList files; files << firstFile << secondFile; userFile.close(); appFile.close(); KXmlGuiVersionHandler versionHandler(files); QCOMPARE(versionHandler.finalFile(), firstFile); QString finalDoc = versionHandler.finalDocument(); QVERIFY(finalDoc.startsWith(QStringLiteral(""))); QVERIFY(finalDoc.contains(QStringLiteral("sidebartng"))); // Check that the toolbars modified by the user were kept QVERIFY(finalDoc.contains(QStringLiteral(" fileToVersionMap; // makes QCOMPARE failures more readable than just temp filenames QDir().mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); QFile fileV2(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + QStringLiteral("testui.rc")); QVERIFY2(fileV2.open(QIODevice::WriteOnly), qPrintable(fileV2.fileName())); createXmlFile(fileV2, 2, NoFlags); fileToVersionMap.insert(fileV2.fileName(), 2); QTemporaryFile fileV5; QVERIFY(fileV5.open()); createXmlFile(fileV5, 5, NoFlags); fileToVersionMap.insert(fileV5.fileName(), 5); // The highest version is neither the first nor last one in the list, // to make sure the code really selects the highest version, not just by chance :) // (This is why we add the v1 version at the end of the list) QTemporaryFile fileV1; QVERIFY(fileV1.open()); createXmlFile(fileV1, 1, NoFlags); fileToVersionMap.insert(fileV1.fileName(), 1); QStringList files; files << fileV2.fileName() << fileV5.fileName() << fileV1.fileName(); fileV2.close(); fileV5.close(); fileV1.close(); KXmlGuiVersionHandler versionHandler(files); QCOMPARE(fileToVersionMap.value(versionHandler.finalFile()), 5); QString finalDoc = versionHandler.finalDocument(); QVERIFY(finalDoc.startsWith(QStringLiteral(" fileToVersionMap; // makes QCOMPARE failures more readable than just temp filenames // local file QFile fileV2(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + QStringLiteral("testui.rc")); QVERIFY(fileV2.open(QIODevice::WriteOnly)); createXmlFile(fileV2, 2, AddActionProperties | AddModifiedToolBars); fileToVersionMap.insert(fileV2.fileName(), 2); // more-global file QTemporaryFile fileV5; QVERIFY(fileV5.open()); createXmlFile(fileV5, 5, AddToolBars | AddModifiedMenus, "kpartgui"); fileToVersionMap.insert(fileV5.fileName(), 5); // The highest version is neither the first nor last one in the list, // to make sure the code really selects the highest version, not just by chance :) // (This is why we add the v1 version at the end of the list) QTemporaryFile fileV1; QVERIFY(fileV1.open()); createXmlFile(fileV1, 1, AddToolBars); fileToVersionMap.insert(fileV1.fileName(), 1); QStringList files; files << fileV2.fileName() << fileV5.fileName() << fileV1.fileName(); fileV2.close(); fileV5.close(); fileV1.close(); KXmlGuiVersionHandler versionHandler(files); // We end up with the local file, so in our map it has version 2. // But of course by now it says "version=5" in it :) QCOMPARE(fileToVersionMap.value(versionHandler.finalFile()), 2); const QString finalDoc = versionHandler.finalDocument(); //qDebug() << finalDoc; QVERIFY(finalDoc.startsWith(QStringLiteral(""))); QVERIFY(finalDoc.contains(QStringLiteral("sidebartng"))); // Check that the menus modified by the app are still there QVERIFY(finalDoc.contains(QStringLiteral(" containers = factory.containers(QStringLiteral("Menu")); QStringList containerNames; Q_FOREACH (QWidget *w, containers) { containerNames << w->objectName(); } return containerNames; } void debugActions(const QList &actions) { Q_FOREACH (QAction *action, actions) { qDebug() << (action->isSeparator() ? QString::fromLatin1("separator") : action->objectName()); } } static void checkActions(const QList &actions, const QStringList &expectedActions) { for (int i = 0; i < expectedActions.count(); ++i) { if (i >= actions.count()) break; QAction *action = actions.at(i); if (action->isSeparator()) { QCOMPARE(QStringLiteral("separator"), expectedActions[i]); } else { QCOMPARE(action->objectName(), expectedActions[i]); } } QCOMPARE(actions.count(), expectedActions.count()); } void KXmlGui_UnitTest::testPartMerging() { const QByteArray hostXml = "\n" "\n" "\n" "\n" " &Go\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " Section title\n" " \n" " \n" " &File\n" " \n" " \n" "\n" "\n"; TestGuiClient hostClient; hostClient.createActions(QStringList() << QStringLiteral("go_up") << QStringLiteral("go_back") << QStringLiteral("go_forward") << QStringLiteral("go_home") << QStringLiteral("host_after_merge") << QStringLiteral("host_after_merge_2") << QStringLiteral("last_from_host") << QStringLiteral("file_new") << QStringLiteral("file_open") << QStringLiteral("file_quit")); hostClient.createGUI(hostXml, true /*ui_standards.rc*/); KMainWindow mainWindow; KXMLGUIBuilder builder(&mainWindow); KXMLGUIFactory factory(&builder); factory.addClient(&hostClient); const QString hostDomDoc = hostClient.domDocument().toString(); QWidget *goMenuW = factory.container(QStringLiteral("go"), &hostClient); QVERIFY(goMenuW); QMenu *goMenu = qobject_cast(goMenuW); QVERIFY(goMenu); QMenu *fileMenu = qobject_cast(factory.container(QStringLiteral("file"), &hostClient)); //debugActions(goMenu->actions()); checkActions(goMenu->actions(), QStringList() << QStringLiteral("go_up") << QStringLiteral("go_back") << QStringLiteral("go_forward") << QStringLiteral("go_home") << QStringLiteral("separator") << QStringLiteral("host_after_merge") << QStringLiteral("host_after_merge_2") << QStringLiteral("separator") << QStringLiteral("separator") // separator << QStringLiteral("last_from_host")); checkActions(fileMenu->actions(), QStringList() << QStringLiteral("file_new") << QStringLiteral("file_open") << QStringLiteral("separator") << QStringLiteral("file_quit")); qDebug() << "Now merging the part"; const QByteArray partXml = "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n" "<gui version=\"1\" name=\"part\" >\n" "<MenuBar>\n" " <Menu name=\"go\"><text>&Go</text>\n" " <Action name=\"go_previous\" group=\"before_merge\"/>\n" " <Action name=\"go_next\" group=\"before_merge\"/>\n" " <Separator/>\n" " <Action name=\"first_page\"/>\n" " <Action name=\"last_page\"/>\n" " <Title>Part Section\n" " \n" " \n" " \n" " \n" " \n" " &File\n" " \n" " \n" " \n" "\n" "\n"; TestGuiClient partClient(partXml); partClient.createActions(QStringList() << QStringLiteral("go_previous") << QStringLiteral("go_next") << QStringLiteral("first_page") << QStringLiteral("last_page") << QStringLiteral("last_from_part") << QStringLiteral("action_in_merge_group") << QStringLiteral("undefined_group") << QStringLiteral("action_in_placed_merge") << QStringLiteral("other_file_action") << QStringLiteral("action1") << QStringLiteral("action2")); const QList actionList = { partClient.actionCollection()->action(QStringLiteral("action1")), partClient.actionCollection()->action(QStringLiteral("action2")) }; for (int i = 0 ; i < 5 ; ++i) { //qDebug() << "addClient, iteration" << i; factory.addClient(&partClient); partClient.plugActionList(QStringLiteral("action_list"), actionList); //debugActions(goMenu->actions()); checkActions(goMenu->actions(), QStringList() << QStringLiteral("go_up") << QStringLiteral("go_back") << QStringLiteral("go_forward") << QStringLiteral("go_home") << QStringLiteral("separator") << QStringLiteral("go_previous") << QStringLiteral("go_next") // Contents of the : << QStringLiteral("separator") << QStringLiteral("first_page") << QStringLiteral("last_page") << QStringLiteral("separator") // in the part << QStringLiteral("action1") << QStringLiteral("action2") << QStringLiteral("undefined_group") << QStringLiteral("last_from_part") // End of <Merge> << QStringLiteral("host_after_merge") << QStringLiteral("host_after_merge_2") << QStringLiteral("separator") // Contents of <DefineGroup> << QStringLiteral("action_in_merge_group") // End of <DefineGroup> << QStringLiteral("separator") // <title> is a separator qaction with text << QStringLiteral("last_from_host") ); checkActions(fileMenu->actions(), QStringList() << QStringLiteral("file_new") << QStringLiteral("action_in_placed_merge") << QStringLiteral("file_open") << QStringLiteral("separator") << QStringLiteral("file_quit") << QStringLiteral("other_file_action")); factory.removeClient(&partClient); QCOMPARE(hostClient.domDocument().toString(), hostDomDoc); } factory.removeClient(&hostClient); } void KXmlGui_UnitTest::testPartMergingSettings() // #252911 { const QByteArray hostXml = "<?xml version = '1.0'?>\n" "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n" "<gui version=\"1\" name=\"foo\" >\n" "<MenuBar>\n" // The solution was to remove the duplicated definition // " <Menu name=\"settings\"><text>&Settings</text>\n" // " <Action name=\"options_configure_keybinding\"/>\n" // " <Action name=\"options_configure_toolbars\"/>\n" // " <Merge name=\"configure_merge\"/>\n" // " <Separator/>\n" // " <Merge/>\n" // " </Menu>\n" "</MenuBar></gui>\n"; TestGuiClient hostClient; hostClient.createActions(QStringList() << QStringLiteral("options_configure_keybinding") << QStringLiteral("options_configure_toolbars")); hostClient.createGUI(hostXml, true /*ui_standards.rc*/); //qDebug() << hostClient.domDocument().toString(); KMainWindow mainWindow; KXMLGUIBuilder builder(&mainWindow); KXMLGUIFactory factory(&builder); factory.addClient(&hostClient); QWidget *settingsMenu = qobject_cast<QMenu *>(factory.container(QStringLiteral("settings"), &hostClient)); QVERIFY(settingsMenu); //debugActions(settingsMenu->actions()); const QByteArray partXml = "<?xml version = '1.0'?>\n" "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n" "<gui version=\"1\" name=\"foo\" >\n" "<MenuBar>\n" " <Menu name=\"settings\"><text>&Settings</text>\n" " <Action name=\"configure_klinkstatus\"/>\n" " </Menu>\n" "</MenuBar></gui>\n"; TestGuiClient partClient(partXml); partClient.createActions(QStringList() << QStringLiteral("configure_klinkstatus")); factory.addClient(&partClient); //debugActions(settingsMenu->actions()); checkActions(settingsMenu->actions(), QStringList() << QStringLiteral("separator") // that's ok, QMenuPrivate::filterActions won't show it << QStringLiteral("options_configure_keybinding") << QStringLiteral("options_configure_toolbars") << QStringLiteral("configure_klinkstatus")); factory.removeClient(&partClient); factory.removeClient(&hostClient); } void KXmlGui_UnitTest::testUiStandardsMerging_data() { QTest::addColumn<QByteArray>("xml"); QTest::addColumn<QStringList>("actions"); QTest::addColumn<QStringList>("expectedMenus"); const QByteArray xmlBegin = "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n" "<gui version=\"1\" name=\"foo\" >\n" "<MenuBar>\n"; const QByteArray xmlEnd = "</MenuBar>\n" "</gui>"; // Merging an empty menu (or a menu with only non-existing actions) would make // the empty menu appear at the end after all other menus (fixed for KDE-4.2) QTest::newRow("empty file menu, implicit settings menu") << QByteArray(xmlBegin + "<Menu name=\"file\"/>\n" + xmlEnd) << (QStringList() << QStringLiteral("options_configure_toolbars")) << (QStringList() << QStringLiteral("settings")); QTest::newRow("file menu with non existing action, implicit settings menu") << QByteArray(xmlBegin + "<Menu name=\"file\"><Action name=\"foo\"/></Menu>\n" + xmlEnd) << (QStringList() << QStringLiteral("options_configure_toolbars")) << (QStringList() << QStringLiteral("settings")); QTest::newRow("file menu with existing action, implicit settings menu") << QByteArray(xmlBegin + "<Menu name=\"file\"><Action name=\"open\"/></Menu>\n" + xmlEnd) << (QStringList() << QStringLiteral("open") << QStringLiteral("options_configure_toolbars")) << (QStringList() << QStringLiteral("file") << QStringLiteral("settings")); QTest::newRow("implicit file and settings menu") << QByteArray(xmlBegin + xmlEnd) << (QStringList() << QStringLiteral("file_open") << QStringLiteral("options_configure_toolbars")) << (QStringList() << QStringLiteral("file") << QStringLiteral("settings")); // we could check that file_open is in the mainToolBar, too // Check that unknown non-empty menus are added at the "MergeLocal" position (before settings). QTest::newRow("foo menu added at the end") << QByteArray(xmlBegin + "<Menu name=\"foo\"><Action name=\"foo_action\"/></Menu>\n" + xmlEnd) << (QStringList() << QStringLiteral("file_open") << QStringLiteral("options_configure_toolbars") << QStringLiteral("foo_action")) << (QStringList() << QStringLiteral("file") << QStringLiteral("foo") << QStringLiteral("settings")); QTest::newRow("Bille's testcase: menu patch + menu edit") << QByteArray(xmlBegin + "<Menu name=\"patch\"><Action name=\"patch_generate\"/></Menu>\n" + "<Menu name=\"edit\"><Action name=\"edit_foo\"/></Menu>\n" + xmlEnd) << (QStringList() << QStringLiteral("file_open") << QStringLiteral("patch_generate") << QStringLiteral("edit_foo")) << (QStringList() << QStringLiteral("file") << QStringLiteral("edit") << QStringLiteral("patch")); QTest::newRow("Bille's testcase: menu patch + menu edit, lowercase tag") << QByteArray(xmlBegin + "<Menu name=\"patch\"><Action name=\"patch_generate\"/></Menu>\n" + "<menu name=\"edit\"><Action name=\"edit_foo\"/></menu>\n" + xmlEnd) << (QStringList() << QStringLiteral("file_open") << QStringLiteral("patch_generate") << QStringLiteral("edit_foo")) << (QStringList() << QStringLiteral("file") << QStringLiteral("edit") << QStringLiteral("patch")); // Check that <Menu append="..."> allows to insert menus at specific positions QTest::newRow("Menu append") << QByteArray(xmlBegin + "<Menu name=\"foo\" append=\"settings_merge\"><Action name=\"foo_action\"/></Menu>\n" + xmlEnd) << (QStringList() << QStringLiteral("file_open") << QStringLiteral("options_configure_toolbars") << QStringLiteral("foo_action") << QStringLiteral("help_contents")) << (QStringList() << QStringLiteral("file") << QStringLiteral("settings") << QStringLiteral("foo") << QStringLiteral("help")); QTest::newRow("Custom first menu") << QByteArray(xmlBegin + "<Menu name=\"foo\" append=\"first_menu\"><Action name=\"foo_action\"/></Menu>\n" + xmlEnd) << (QStringList() << QStringLiteral("edit_undo") << QStringLiteral("foo_action") << QStringLiteral("help_contents")) << (QStringList() << QStringLiteral("foo") << QStringLiteral("edit") << QStringLiteral("help")); // Tests for noMerge="1" QTest::newRow("noMerge empty file menu, implicit settings menu") << QByteArray(xmlBegin + "<Menu name=\"file\" noMerge=\"1\"/>\n" + xmlEnd) << (QStringList() << QStringLiteral("file_open") << QStringLiteral("options_configure_toolbars")) << (QStringList() << QStringLiteral("file") << QStringLiteral("settings")); // we keep empty menus, see #186382 QTest::newRow("noMerge empty file menu, file_open moved elsewhere") << QByteArray(xmlBegin + "<Menu name=\"file\" noMerge=\"1\"/>\n<Menu name=\"foo\"><Action name=\"file_open\"/></Menu>" + xmlEnd) << (QStringList() << QStringLiteral("file_open")) << (QStringList() << QStringLiteral("file") << QStringLiteral("foo")); QTest::newRow("noMerge file menu with open before new") << QByteArray(xmlBegin + "<Menu name=\"file\" noMerge=\"1\"><Action name=\"file_open\"/><Action name=\"file_new\"/></Menu>" + xmlEnd) << (QStringList() << QStringLiteral("file_open") << QStringLiteral("file_new")) << (QStringList() << QStringLiteral("file")); // TODO check the order of the actions in the menu? how? // Tests for deleted="true" QTest::newRow("deleted file menu, implicit settings menu") << QByteArray(xmlBegin + "<Menu name=\"file\" deleted=\"true\"/>\n" + xmlEnd) << (QStringList() << QStringLiteral("file_open") << QStringLiteral("options_configure_toolbars")) << (QStringList() << QStringLiteral("settings")); QTest::newRow("deleted file menu, file_open moved elsewhere") << QByteArray(xmlBegin + "<Menu name=\"file\" deleted=\"true\"/>\n<Menu name=\"foo\"><Action name=\"file_open\"/></Menu>" + xmlEnd) << (QStringList() << QStringLiteral("file_open")) << (QStringList() << QStringLiteral("foo")); QTest::newRow("deleted file menu with actions (contradiction)") << QByteArray(xmlBegin + "<Menu name=\"file\" deleted=\"true\"><Action name=\"file_open\"/><Action name=\"file_new\"/></Menu>" + xmlEnd) << (QStringList() << QStringLiteral("file_open") << QStringLiteral("file_new")) << (QStringList()); } void KXmlGui_UnitTest::testUiStandardsMerging() { QFETCH(QByteArray, xml); QFETCH(QStringList, actions); QFETCH(QStringList, expectedMenus); TestGuiClient client; client.createActions(actions); client.createGUI(xml, true /*ui_standards.rc*/); const QDomDocument domDocument = client.domDocument(); const QDomElement docElem = domDocument.documentElement(); QCOMPARE(docElem.attribute(QStringLiteral("name")), QStringLiteral("foo")); // not standard_containers from ui_standards.rc QCOMPARE(docElem.attribute(QStringLiteral("version")), QStringLiteral("1")); // not standard_containers from ui_standards.rc KMainWindow mainWindow; KXMLGUIBuilder builder(&mainWindow); KXMLGUIFactory factory(&builder); factory.addClient(&client); const QStringList containerNames = collectMenuNames(factory); //qDebug() << containerNames; QCOMPARE(containerNames, expectedMenus); factory.removeClient(&client); } void KXmlGui_UnitTest::testActionListAndSeparator() { const QByteArray xml = "<?xml version = '1.0'?>\n" "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n" "<gui version=\"1\" name=\"foo\" >\n" "<MenuBar>\n" " <Menu name=\"groups\"><text>Add to Group</text>\n" " <ActionList name=\"view_groups_list\"/>\n" " <Separator />" " <Action name=\"view_add_to_new_group\" />\n" " <ActionList name=\"second_list\"/>\n" " </Menu>\n" "</MenuBar>\n" "</gui>"; TestGuiClient client(xml); client.createActions(QStringList() << QStringLiteral("view_add_to_new_group")); KMainWindow mainWindow; KXMLGUIBuilder builder(&mainWindow); KXMLGUIFactory factory(&builder); factory.addClient(&client); QWidget *menuW = factory.container(QStringLiteral("groups"), &client); QVERIFY(menuW); QMenu *menu = qobject_cast<QMenu *>(menuW); QVERIFY(menu); //debugActions(menu->actions()); checkActions(menu->actions(), QStringList() << QStringLiteral("separator") // that's ok, QMenuPrivate::filterActions won't show it << QStringLiteral("view_add_to_new_group")); qDebug() << "Now plugging the actionlist"; QAction *action1 = new QAction(this); action1->setObjectName(QStringLiteral("action1")); client.actionCollection()->setDefaultShortcut(action1, QKeySequence(QStringLiteral("Ctrl+2"))); QAction *action2 = new QAction(this); action2->setObjectName(QStringLiteral("action2")); const QList<QAction *> actionList = { action1, action2 }; client.plugActionList(QStringLiteral("view_groups_list"), actionList); QCOMPARE(QKeySequence::listToString(action1->shortcuts()), QStringLiteral("Ctrl+2")); const QStringList expectedActionsOneList = { QStringLiteral("action1"), QStringLiteral("action2"), QStringLiteral("separator"), QStringLiteral("view_add_to_new_group") }; //debugActions(menu->actions()); checkActions(menu->actions(), expectedActionsOneList); QAction *action3 = new QAction(this); action3->setObjectName(QStringLiteral("action3")); const QList<QAction *> secondActionList = { action3 }; client.plugActionList(QStringLiteral("second_list"), secondActionList); QStringList expectedActions = expectedActionsOneList; expectedActions << QStringLiteral("action3"); checkActions(menu->actions(), expectedActions); qDebug() << "Now remove+add gui client"; // While I'm here, what happens with the action list if I remove+add the guiclient, // like KXmlGuiWindow::newToolBarConfig does? factory.removeClient(&client); factory.addClient(&client); // We need to get the container widget again, it was re-created. menuW = factory.container(QStringLiteral("groups"), &client); QVERIFY(menuW); menu = qobject_cast<QMenu *>(menuW); //debugActions(menu->actions()); checkActions(menu->actions(), QStringList() << QStringLiteral("separator") // yep, it removed the actionlist thing... << QStringLiteral("view_add_to_new_group")); qDebug() << "Now plugging the actionlist again"; client.plugActionList(QStringLiteral("second_list"), secondActionList); client.plugActionList(QStringLiteral("view_groups_list"), actionList); checkActions(menu->actions(), expectedActions); factory.removeClient(&client); } void KXmlGui_UnitTest::testHiddenToolBar() { const QByteArray xml = "<?xml version = '1.0'?>\n" "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n" "<gui version=\"1\" name=\"foo\" >\n" "<MenuBar>\n" "</MenuBar>\n" "<ToolBar hidden=\"true\" name=\"mainToolBar\">\n" " <Action name=\"go_up\"/>\n" "</ToolBar>\n" "<ToolBar name=\"visibleToolBar\">\n" " <Action name=\"go_up\"/>\n" "</ToolBar>\n" "<ToolBar hidden=\"true\" name=\"hiddenToolBar\">\n" " <Action name=\"go_up\"/>\n" "</ToolBar>\n" "</gui>\n"; KConfigGroup cg(KSharedConfig::openConfig(), "testHiddenToolBar"); TestXmlGuiWindow mainWindow(xml, "kxmlgui_unittest.rc"); mainWindow.setAutoSaveSettings(cg); mainWindow.createActions(QStringList() << QStringLiteral("go_up")); mainWindow.createGUI(); KToolBar *mainToolBar = mainWindow.toolBarByName(QStringLiteral("mainToolBar")); QVERIFY(mainToolBar->isHidden()); KXMLGUIFactory *factory = mainWindow.guiFactory(); QVERIFY(!factory->container(QStringLiteral("visibleToolBar"), &mainWindow)->isHidden()); KToolBar *hiddenToolBar = qobject_cast<KToolBar *>(factory->container(QStringLiteral("hiddenToolBar"), &mainWindow)); qDebug() << hiddenToolBar; QVERIFY(hiddenToolBar->isHidden()); // Now open KEditToolBar (#105525) KEditToolBar editToolBar(factory); // KEditToolBar loads the stuff in showEvent... QShowEvent ev; qApp->sendEvent(&editToolBar, &ev); clickApply(&editToolBar); QVERIFY(qobject_cast<KToolBar *>(factory->container(QStringLiteral("hiddenToolBar"), &mainWindow))->isHidden()); mainWindow.close(); } // taken from KMainWindow_UnitTest::testAutoSaveSettings() void KXmlGui_UnitTest::testAutoSaveSettings() { const QByteArray xml = "<?xml version = '1.0'?>\n" "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n" "<gui version=\"1\" name=\"foo\" >\n" "<MenuBar>\n" "</MenuBar>\n" "<ToolBar name=\"mainToolBar\">\n" " <Action name=\"go_up\"/>\n" "</ToolBar>\n" "<ToolBar name=\"secondToolBar\">\n" " <Action name=\"go_up\"/>\n" "</ToolBar>\n" "</gui>\n"; { // do not interfere with the "toolbarVisibility" unit test KConfigGroup cg(KSharedConfig::openConfig(), "testAutoSaveSettings"); TestXmlGuiWindow mw(xml, "kxmlgui_unittest.rc"); mw.show(); mw.setAutoSaveSettings(cg); // Test resizing first (like show() does). mw.reallyResize(400, 400); QTest::qWait(200); mw.createActions(QStringList() << QStringLiteral("go_up")); mw.createGUI(); // Resize again, should be saved mw.reallyResize(800, 600); QTest::qWait(200); KToolBar *mainToolBar = mw.toolBarByName(QStringLiteral("mainToolBar")); QCOMPARE(mw.toolBarArea(mainToolBar), Qt::TopToolBarArea); KToolBar *secondToolBar = mw.toolBarByName(QStringLiteral("secondToolBar")); QCOMPARE((int)mw.toolBarArea(secondToolBar), (int)Qt::TopToolBarArea); // REFERENCE #1 (see below) // Move second toolbar to bottom const QPoint oldPos = secondToolBar->pos(); mw.addToolBar(Qt::BottomToolBarArea, secondToolBar); const QPoint newPos = secondToolBar->pos(); QCOMPARE(mw.toolBarArea(secondToolBar), Qt::BottomToolBarArea); // Calling to addToolBar is not enough to trigger the event filter for move events // in KMainWindow, because there is no layouting happening in hidden mainwindows. QMoveEvent moveEvent(newPos, oldPos); QApplication::sendEvent(secondToolBar, &moveEvent); mw.close(); } { KConfigGroup cg(KSharedConfig::openConfig(), "testAutoSaveSettings"); TestXmlGuiWindow mw2(xml, "kxmlgui_unittest.rc"); mw2.show(); mw2.setAutoSaveSettings(cg); QTest::qWait(200); // Check window size was restored QCOMPARE(mw2.size(), QSize(800, 600)); mw2.createActions(QStringList() << QStringLiteral("go_up")); mw2.createGUI(); // Force window layout to happen mw2.reallyResize(800, 600); QTest::qWait(200); // Check toolbar positions were restored KToolBar *mainToolBar = mw2.toolBarByName(QStringLiteral("mainToolBar")); QCOMPARE(mw2.toolBarArea(mainToolBar), Qt::TopToolBarArea); KToolBar *secondToolBar = mw2.toolBarByName(QStringLiteral("secondToolBar")); QCOMPARE(mw2.toolBarArea(secondToolBar), Qt::BottomToolBarArea); mw2.applyMainWindowSettings(mw2.autoSaveConfigGroup()); QCOMPARE(mw2.toolBarArea(secondToolBar), Qt::BottomToolBarArea); } } void KXmlGui_UnitTest::testDeletedContainers() // deleted="true" { const QByteArray xml = "<?xml version = '1.0'?>\n" "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n" "<gui version=\"1\" name=\"foo\" >\n" "<MenuBar>\n" " <Menu deleted=\"true\" name=\"game\"/>\n" "</MenuBar>\n" "<ToolBar deleted=\"true\" name=\"mainToolBar\">\n" " <Action name=\"go_up\"/>\n" "</ToolBar>\n" "<ToolBar name=\"visibleToolBar\">\n" " <Action name=\"go_up\"/>\n" "</ToolBar>\n" "<ToolBar deleted=\"true\" name=\"deletedToolBar\">\n" " <Action name=\"go_up\"/>\n" "</ToolBar>\n" "</gui>\n"; KConfigGroup cg(KSharedConfig::openConfig(), "testDeletedToolBar"); TestXmlGuiWindow mainWindow(xml, "kxmlgui_unittest.rc"); mainWindow.setAutoSaveSettings(cg); mainWindow.createActions(QStringList() << QStringLiteral("go_up") << QStringLiteral("file_new") << QStringLiteral("game_new")); mainWindow.createGUI(); KXMLGUIFactory *factory = mainWindow.guiFactory(); //qDebug() << "containers:" << factory->containers("ToolBar"); QVERIFY(!factory->container(QStringLiteral("mainToolBar"), &mainWindow)); QVERIFY(!factory->container(QStringLiteral("visibleToolBar"), &mainWindow)->isHidden()); QVERIFY(!factory->container(QStringLiteral("deletedToolBar"), &mainWindow)); QVERIFY(factory->container(QStringLiteral("file"), &mainWindow)); // File menu was created QVERIFY(!factory->container(QStringLiteral("game"), &mainWindow)); // Game menu was not created // Now open KEditToolBar, just to check it doesn't crash. KEditToolBar editToolBar(factory); // KEditToolBar loads the stuff in showEvent... QShowEvent ev; qApp->sendEvent(&editToolBar, &ev); clickApply(&editToolBar); QVERIFY(!factory->container(QStringLiteral("mainToolBar"), &mainWindow)); QVERIFY(!factory->container(QStringLiteral("visibleToolBar"), &mainWindow)->isHidden()); QVERIFY(!factory->container(QStringLiteral("deletedToolBar"), &mainWindow)); QVERIFY(factory->container(QStringLiteral("file"), &mainWindow)); QVERIFY(!factory->container(QStringLiteral("game"), &mainWindow)); mainWindow.close(); } void KXmlGui_UnitTest::testTopLevelSeparator() { const QByteArray xml = "<?xml version = '1.0'?>\n" "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n" "<gui version=\"1\" name=\"foo\" >\n" "<MenuBar>\n" " <Menu name=\"before_separator\"><text>Before Separator</text></Menu>\n" " <Separator />\n" " <Menu name=\"after_separator\"><text>After Separator</text></Menu>\n" "</MenuBar>\n" "</gui>"; TestXmlGuiWindow mainWindow(xml, "kxmlgui_unittest.rc"); mainWindow.createGUI(); checkActions(mainWindow.menuBar()->actions(), QStringList() << QStringLiteral("before_separator") << QStringLiteral("separator") << QStringLiteral("after_separator") << QStringLiteral("separator") << QStringLiteral("help")); } // Check that the objectName() of the menus is set from the name in the XML file void KXmlGui_UnitTest::testMenuNames() { const QByteArray xml = "<?xml version = '1.0'?>\n" "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n" "<gui version=\"1\" name=\"foo\" >\n" "<MenuBar>\n" " <Menu name=\"filemenu\"><text>File Menu</text></Menu>\n" "</MenuBar>\n" "</gui>"; TestXmlGuiWindow mainWindow(xml, "kxmlgui_unittest.rc"); mainWindow.createGUI(); checkActions(mainWindow.menuBar()->actions(), QStringList() << QStringLiteral("filemenu") << QStringLiteral("separator") << QStringLiteral("help")); } // Test what happens when the application's rc file isn't found // We want a warning to be printed, but we don't want to see all menus from ui_standards.rc void KXmlGui_UnitTest::testMenusNoXmlFile() { TestXmlGuiWindow mainWindow(QByteArray(), "nolocalfile_either.rc"); mainWindow.createGUIBad(); checkActions(mainWindow.menuBar()->actions(), QStringList() << QStringLiteral("help")); } void KXmlGui_UnitTest::testXMLFileReplacement() { // to differentiate "original" and replacement xml file, one is created with "modified" toolbars QTemporaryFile fileOrig; QVERIFY(fileOrig.open()); createXmlFile(fileOrig, 2, AddToolBars); const QString filenameOrig = fileOrig.fileName(); fileOrig.close(); QTemporaryFile fileReplace; QVERIFY(fileReplace.open()); createXmlFile(fileReplace, 2, AddModifiedToolBars); const QString filenameReplace = fileReplace.fileName(); fileReplace.close(); // finally, our local xml file has <ActionProperties/> QFile fileLocal(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + QStringLiteral("testui.rc")); QVERIFY2(fileLocal.open(QIODevice::WriteOnly), qPrintable(fileLocal.fileName())); createXmlFile(fileLocal, 1, AddActionProperties); const QString filenameLocal = fileLocal.fileName(); fileLocal.close(); TestGuiClient client; // first make sure that the "original" file is loaded, correctly client.setXMLFilePublic(filenameOrig); QString xml = client.domDocument().toString(); //qDebug() << xml; QVERIFY(xml.contains(QStringLiteral("<Action name=\"print\""))); QVERIFY(!xml.contains(QStringLiteral("<Action name=\"home\""))); QVERIFY(!xml.contains(QStringLiteral("<ActionProperties>"))); // now test the replacement (+ local file) client.replaceXMLFile(filenameReplace, filenameLocal); xml = client.domDocument().toString(); QVERIFY(!xml.contains(QStringLiteral("<Action name=\"print\""))); QVERIFY(xml.contains(QStringLiteral("<Action name=\"home\""))); QVERIFY(xml.contains(QStringLiteral("<ActionProperties>"))); // re-check after a reload client.reloadXML(); QString reloadedXml = client.domDocument().toString(); QVERIFY(!reloadedXml.contains(QStringLiteral("<Action name=\"print\""))); QVERIFY(reloadedXml.contains(QStringLiteral("<Action name=\"home\""))); QVERIFY(reloadedXml.contains(QStringLiteral("<ActionProperties>"))); // Check what happens when the local file doesn't exist TestGuiClient client2; QFile::remove(filenameLocal); client2.replaceXMLFile(filenameReplace, filenameLocal); xml = client2.domDocument().toString(); //qDebug() << xml; QVERIFY(!xml.contains(QStringLiteral("<Action name=\"print\""))); QVERIFY(xml.contains(QStringLiteral("<Action name=\"home\""))); // modified toolbars QVERIFY(!xml.contains(QStringLiteral("<ActionProperties>"))); // but no local xml file } void KXmlGui_UnitTest::testClientDestruction() // #170806 { const QByteArray hostXml = "<?xml version = '1.0'?>\n" "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n" "<gui version=\"1\" name=\"foo\" >\n" "<MenuBar>\n" " <Menu name=\"file\"><text>&File</text>\n" " </Menu>\n" " <Merge/>\n" "</MenuBar>\n" "</gui>"; const QByteArray xml = "<?xml version = '1.0'?>\n" "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n" "<gui version=\"1\" name=\"foo\" >\n" "<MenuBar>\n" " <Menu name=\"file\"><text>&File</text>\n" " <Action name=\"file_open\"/>\n" " <Action name=\"file_quit\"/>\n" " </Menu>\n" "</MenuBar>\n" "</gui>"; TestXmlGuiWindow mainWindow(hostXml, "kxmlgui_unittest.rc"); TestGuiClient *client = new TestGuiClient(xml); client->createActions(QStringList() << QStringLiteral("file_open") << QStringLiteral("file_quit")); mainWindow.insertChildClient(client); mainWindow.createGUI(); checkActions(mainWindow.menuBar()->actions(), QStringList() << QStringLiteral("file") << QStringLiteral("separator") << QStringLiteral("help")); QVERIFY(mainWindow.factory()->clients().contains(client)); delete client; QVERIFY(!mainWindow.factory()->clients().contains(client)); // No change, because deletion is fast, it doesn't do manual unplugging. checkActions(mainWindow.menuBar()->actions(), QStringList() << QStringLiteral("file") << QStringLiteral("separator") << QStringLiteral("help")); } void KXmlGui_UnitTest::testShortcuts() { const QByteArray xml = "<?xml version = '1.0'?>\n" "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n" "<gui version=\"1\" name=\"foo\" >\n" "<MenuBar>\n" " <Menu name=\"file\"><text>&File</text>\n" " <Action name=\"file_open\" shortcut=\"Ctrl+O\"/>\n" " <Action name=\"file_quit\" shortcut=\"Ctrl+Q; Ctrl+D\"/>\n" " </Menu>\n" "</MenuBar>\n" "<ActionProperties scheme=\"Default\">\n" " <Action shortcut=\"Ctrl+O\" name=\"file_open\"/>\n" " <Action shortcut=\"Ctrl+Q; Ctrl+D\" name=\"file_quit\"/>\n" "</ActionProperties>\n" "</gui>"; TestGuiClient client; client.createActions(QStringList() << QStringLiteral("file_open") << QStringLiteral("file_quit")); client.createGUI(xml, false /*ui_standards.rc*/); KMainWindow mainWindow; KXMLGUIBuilder builder(&mainWindow); KXMLGUIFactory factory(&builder); factory.addClient(&client); QAction* actionOpen = client.action("file_open"); QAction* actionQuit = client.action("file_quit"); QVERIFY(actionOpen && actionQuit); QCOMPARE(actionOpen->shortcuts(), QList<QKeySequence>() << QKeySequence(QStringLiteral("Ctrl+O"))); // #345411 QCOMPARE(actionQuit->shortcuts(), QList<QKeySequence>() << QKeySequence(QStringLiteral("Ctrl+Q")) << QKeySequence(QStringLiteral("Ctrl+D"))); factory.removeClient(&client); } void KXmlGui_UnitTest::testPopupMenuParent() { const QByteArray xml = "<?xml version = '1.0'?>\n" "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n" "<gui version=\"1\" name=\"foo\" >\n" "<Menu name=\"foo\"><text>Foo</text></Menu>\n" "</gui>"; TestXmlGuiWindow mainWindow(xml, "kxmlgui_unittest.rc"); mainWindow.createGUI(); auto popupMenu = mainWindow.menuByName(QStringLiteral("foo")); QVERIFY(popupMenu); QCOMPARE(popupMenu->parent(), &mainWindow); } void KXmlGui_UnitTest::testSpecificApplicationLanguageQLocale() { const QLocale originalSystemLocale = QLocale::system(); KDEPrivate::setApplicationSpecificLanguage("ru_RU"); KDEPrivate::initializeLanguages(); QCOMPARE(QLocale::system().language(), QLocale::Russian); KDEPrivate::setApplicationSpecificLanguage("wa"); KDEPrivate::initializeLanguages(); QCOMPARE(QLocale::system().language(), QLocale::Walloon); KDEPrivate::setApplicationSpecificLanguage(QByteArray()); KDEPrivate::initializeLanguages(); QCOMPARE(QLocale::system(), originalSystemLocale); } + +void KXmlGui_UnitTest::testSingleModifierQKeySequenceEndsWithPlus() +{ + // Check that native texts of the Meta, Alt, Control, Shift, Keypad modifiers end in "+" + // we depend on that in KKeySequenceWidgetPrivate::updateShortcutDisplay() + QVERIFY(QKeySequence(Qt::MetaModifier).toString(QKeySequence::NativeText).endsWith(QLatin1Char('+'))); + QVERIFY(QKeySequence(Qt::AltModifier).toString(QKeySequence::NativeText).endsWith(QLatin1Char('+'))); + QVERIFY(QKeySequence(Qt::ControlModifier).toString(QKeySequence::NativeText).endsWith(QLatin1Char('+'))); + QVERIFY(QKeySequence(Qt::ShiftModifier).toString(QKeySequence::NativeText).endsWith(QLatin1Char('+'))); + QVERIFY(QKeySequence(Qt::KeypadModifier).toString(QKeySequence::NativeText).endsWith(QLatin1Char('+'))); +} diff --git a/autotests/kxmlgui_unittest.h b/autotests/kxmlgui_unittest.h index 7d4a3de..e1e7666 100644 --- a/autotests/kxmlgui_unittest.h +++ b/autotests/kxmlgui_unittest.h @@ -1,54 +1,55 @@ /* This file is part of the KDE libraries Copyright (c) 2007 David Faure <faure@kde.org> This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KXMLGUI_UNITTEST_H #define KXMLGUI_UNITTEST_H #include <QObject> class KXmlGui_UnitTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testFindVersionNumber_data(); void testFindVersionNumber(); void testVersionHandlerSameVersion(); void testVersionHandlerNewVersionNothingKept(); void testVersionHandlerNewVersionUserChanges(); void testPartMerging(); void testPartMergingSettings(); void testUiStandardsMerging_data(); void testUiStandardsMerging(); void testActionListAndSeparator(); void testHiddenToolBar(); void testDeletedContainers(); void testAutoSaveSettings(); void testXMLFileReplacement(); void testTopLevelSeparator(); void testMenuNames(); void testClientDestruction(); void testMenusNoXmlFile(); void testShortcuts(); void testPopupMenuParent(); void testSpecificApplicationLanguageQLocale(); + void testSingleModifierQKeySequenceEndsWithPlus(); }; #endif diff --git a/src/kkeysequencewidget.cpp b/src/kkeysequencewidget.cpp index 55d3289..333f4b4 100644 --- a/src/kkeysequencewidget.cpp +++ b/src/kkeysequencewidget.cpp @@ -1,861 +1,861 @@ /* This file is part of the KDE libraries Copyright (C) 1998 Mark Donohoe <donohoe@kde.org> Copyright (C) 2001 Ellis Whitehead <ellis@kde.org> Copyright (C) 2007 Andreas Hartmetz <ahartmetz@gmail.com> This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "config-xmlgui.h" #include "kkeysequencewidget.h" #include "kkeysequencewidget_p.h" #include "debug.h" #include <QAction> #include <QKeyEvent> #include <QTimer> #include <QHash> #include <QHBoxLayout> #include <QToolButton> #include <QApplication> #include <klocalizedstring.h> #include <kmessagebox.h> #include <kkeyserver.h> #if HAVE_GLOBALACCEL # include <kglobalaccel.h> #endif #include "kactioncollection.h" class KKeySequenceWidgetPrivate { public: KKeySequenceWidgetPrivate(KKeySequenceWidget *q); void init(); static QKeySequence appendToSequence(const QKeySequence &seq, int keyQt); static bool isOkWhenModifierless(int keyQt); void updateShortcutDisplay(); void startRecording(); /** * Conflicts the key sequence @p seq with a current standard * shortcut? */ bool conflictWithStandardShortcuts(const QKeySequence &seq); /** * Conflicts the key sequence @p seq with a current local * shortcut? */ bool conflictWithLocalShortcuts(const QKeySequence &seq); /** * Conflicts the key sequence @p seq with a current global * shortcut? */ bool conflictWithGlobalShortcuts(const QKeySequence &seq); /** * Get permission to steal the shortcut @seq from the standard shortcut @p std. */ bool stealStandardShortcut(KStandardShortcut::StandardShortcut std, const QKeySequence &seq); bool checkAgainstStandardShortcuts() const { return checkAgainstShortcutTypes & KKeySequenceWidget::StandardShortcuts; } bool checkAgainstGlobalShortcuts() const { return checkAgainstShortcutTypes & KKeySequenceWidget::GlobalShortcuts; } bool checkAgainstLocalShortcuts() const { return checkAgainstShortcutTypes & KKeySequenceWidget::LocalShortcuts; } void controlModifierlessTimout() { if (nKey != 0 && !modifierKeys) { // No modifier key pressed currently. Start the timout modifierlessTimeout.start(600); } else { // A modifier is pressed. Stop the timeout modifierlessTimeout.stop(); } } void cancelRecording() { keySequence = oldKeySequence; doneRecording(); } #if HAVE_GLOBALACCEL bool promptStealShortcutSystemwide( QWidget *parent, const QHash<QKeySequence, QList<KGlobalShortcutInfo> > &shortcuts, const QKeySequence &sequence) { if (shortcuts.isEmpty()) { // Usage error. Just say no return false; } QString clashingKeys; Q_FOREACH (const QKeySequence &seq, shortcuts.keys()) { Q_FOREACH (const KGlobalShortcutInfo &info, shortcuts[seq]) { clashingKeys += i18n("Shortcut '%1' in Application %2 for action %3\n", seq.toString(), info.componentFriendlyName(), info.friendlyName()); } } const int hashSize = shortcuts.size(); QString message = i18ncp("%1 is the number of conflicts (hidden), %2 is the key sequence of the shortcut that is problematic", "The shortcut '%2' conflicts with the following key combination:\n", "The shortcut '%2' conflicts with the following key combinations:\n", hashSize, sequence.toString()); message += clashingKeys; QString title = i18ncp("%1 is the number of shortcuts with which there is a conflict", "Conflict with Registered Global Shortcut", "Conflict with Registered Global Shortcuts", hashSize); return KMessageBox::warningContinueCancel(parent, message, title, KGuiItem(i18n("Reassign"))) == KMessageBox::Continue; } #endif //private slot void doneRecording(bool validate = true); //members KKeySequenceWidget *const q; QHBoxLayout *layout; KKeySequenceButton *keyButton; QToolButton *clearButton; QKeySequence keySequence; QKeySequence oldKeySequence; QTimer modifierlessTimeout; bool allowModifierless; uint nKey; uint modifierKeys; bool isRecording; bool multiKeyShortcutsAllowed; QString componentName; //! Check the key sequence against KStandardShortcut::find() KKeySequenceWidget::ShortcutTypes checkAgainstShortcutTypes; /** * The list of action to check against for conflict shortcut */ QList<QAction *> checkList; // deprecated /** * The list of action collections to check against for conflict shortcut */ QList<KActionCollection *> checkActionCollections; /** * The action to steal the shortcut from. */ QList<QAction *> stealActions; bool stealShortcuts(const QList<QAction *> &actions, const QKeySequence &seq); void wontStealShortcut(QAction *item, const QKeySequence &seq); }; KKeySequenceWidgetPrivate::KKeySequenceWidgetPrivate(KKeySequenceWidget *q) : q(q) , layout(nullptr) , keyButton(nullptr) , clearButton(nullptr) , allowModifierless(false) , nKey(0) , modifierKeys(0) , isRecording(false) , multiKeyShortcutsAllowed(true) , componentName() , checkAgainstShortcutTypes(KKeySequenceWidget::LocalShortcuts | KKeySequenceWidget::GlobalShortcuts) , stealActions() {} bool KKeySequenceWidgetPrivate::stealShortcuts( const QList<QAction *> &actions, const QKeySequence &seq) { const int listSize = actions.size(); QString title = i18ncp("%1 is the number of conflicts", "Shortcut Conflict", "Shortcut Conflicts", listSize); QString conflictingShortcuts; Q_FOREACH (const QAction *action, actions) { conflictingShortcuts += i18n("Shortcut '%1' for action '%2'\n", action->shortcut().toString(QKeySequence::NativeText), KLocalizedString::removeAcceleratorMarker(action->text())); } QString message = i18ncp("%1 is the number of ambigious shortcut clashes (hidden)", "The \"%2\" shortcut is ambiguous with the following shortcut.\n" "Do you want to assign an empty shortcut to this action?\n" "%3", "The \"%2\" shortcut is ambiguous with the following shortcuts.\n" "Do you want to assign an empty shortcut to these actions?\n" "%3", listSize, seq.toString(QKeySequence::NativeText), conflictingShortcuts); if (KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18n("Reassign"))) != KMessageBox::Continue) { return false; } return true; } void KKeySequenceWidgetPrivate::wontStealShortcut(QAction *item, const QKeySequence &seq) { QString title(i18n("Shortcut conflict")); QString msg(i18n("<qt>The '%1' key combination is already used by the <b>%2</b> action.<br>" "Please select a different one.</qt>", seq.toString(QKeySequence::NativeText), KLocalizedString::removeAcceleratorMarker(item->text()))); KMessageBox::sorry(q, msg, title); } KKeySequenceWidget::KKeySequenceWidget(QWidget *parent) : QWidget(parent), d(new KKeySequenceWidgetPrivate(this)) { d->init(); setFocusProxy(d->keyButton); connect(d->keyButton, &KKeySequenceButton::clicked, this, &KKeySequenceWidget::captureKeySequence); connect(d->clearButton, &QToolButton::clicked, this, &KKeySequenceWidget::clearKeySequence); connect(&d->modifierlessTimeout, SIGNAL(timeout()), this, SLOT(doneRecording())); //TODO: how to adopt style changes at runtime? /*QFont modFont = d->clearButton->font(); modFont.setStyleHint(QFont::TypeWriter); d->clearButton->setFont(modFont);*/ d->updateShortcutDisplay(); } void KKeySequenceWidgetPrivate::init() { layout = new QHBoxLayout(q); layout->setContentsMargins(0, 0, 0, 0); keyButton = new KKeySequenceButton(this, q); keyButton->setFocusPolicy(Qt::StrongFocus); keyButton->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); keyButton->setToolTip(i18n("Click on the button, then enter the shortcut like you would in the program.\nExample for Ctrl+A: hold the Ctrl key and press A.")); layout->addWidget(keyButton); clearButton = new QToolButton(q); layout->addWidget(clearButton); if (qApp->isLeftToRight()) { clearButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-rtl"))); } else { clearButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-ltr"))); } } KKeySequenceWidget::~KKeySequenceWidget() { delete d; } KKeySequenceWidget::ShortcutTypes KKeySequenceWidget::checkForConflictsAgainst() const { return d->checkAgainstShortcutTypes; } void KKeySequenceWidget::setComponentName(const QString &componentName) { d->componentName = componentName; } bool KKeySequenceWidget::multiKeyShortcutsAllowed() const { return d->multiKeyShortcutsAllowed; } void KKeySequenceWidget::setMultiKeyShortcutsAllowed(bool allowed) { d->multiKeyShortcutsAllowed = allowed; } void KKeySequenceWidget::setCheckForConflictsAgainst(ShortcutTypes types) { d->checkAgainstShortcutTypes = types; } void KKeySequenceWidget::setModifierlessAllowed(bool allow) { d->allowModifierless = allow; } bool KKeySequenceWidget::isKeySequenceAvailable(const QKeySequence &keySequence) const { if (keySequence.isEmpty()) { return true; } return !(d->conflictWithLocalShortcuts(keySequence) || d->conflictWithGlobalShortcuts(keySequence) || d->conflictWithStandardShortcuts(keySequence)); } bool KKeySequenceWidget::isModifierlessAllowed() { return d->allowModifierless; } void KKeySequenceWidget::setClearButtonShown(bool show) { d->clearButton->setVisible(show); } #ifndef KXMLGUI_NO_DEPRECATED void KKeySequenceWidget::setCheckActionList(const QList<QAction *> &checkList) // deprecated { d->checkList = checkList; Q_ASSERT(d->checkActionCollections.isEmpty()); // don't call this method if you call setCheckActionCollections! } #endif void KKeySequenceWidget::setCheckActionCollections(const QList<KActionCollection *> &actionCollections) { d->checkActionCollections = actionCollections; } //slot void KKeySequenceWidget::captureKeySequence() { d->startRecording(); } QKeySequence KKeySequenceWidget::keySequence() const { return d->keySequence; } //slot void KKeySequenceWidget::setKeySequence(const QKeySequence &seq, Validation validate) { // oldKeySequence holds the key sequence before recording started, if setKeySequence() // is called while not recording then set oldKeySequence to the existing sequence so // that the keySequenceChanged() signal is emitted if the new and previous key // sequences are different if (!d->isRecording) { d->oldKeySequence = d->keySequence; } d->keySequence = seq; d->doneRecording(validate == Validate); } //slot void KKeySequenceWidget::clearKeySequence() { setKeySequence(QKeySequence()); } //slot void KKeySequenceWidget::applyStealShortcut() { QSet<KActionCollection *> changedCollections; Q_FOREACH (QAction *stealAction, d->stealActions) { // Stealing a shortcut means setting it to an empty one. stealAction->setShortcuts(QList<QKeySequence>()); // The following code will find the action we are about to // steal from and save it's actioncollection. KActionCollection *parentCollection = nullptr; foreach (KActionCollection *collection, d->checkActionCollections) { if (collection->actions().contains(stealAction)) { parentCollection = collection; break; } } // Remember the changed collection if (parentCollection) { changedCollections.insert(parentCollection); } } Q_FOREACH (KActionCollection *col, changedCollections) { col->writeSettings(); } d->stealActions.clear(); } void KKeySequenceWidgetPrivate::startRecording() { nKey = 0; modifierKeys = 0; oldKeySequence = keySequence; keySequence = QKeySequence(); isRecording = true; keyButton->grabKeyboard(); if (!QWidget::keyboardGrabber()) { qCWarning(DEBUG_KXMLGUI) << "Failed to grab the keyboard! Most likely qt's nograb option is active"; } keyButton->setDown(true); updateShortcutDisplay(); } void KKeySequenceWidgetPrivate::doneRecording(bool validate) { modifierlessTimeout.stop(); isRecording = false; keyButton->releaseKeyboard(); keyButton->setDown(false); stealActions.clear(); if (keySequence == oldKeySequence) { // The sequence hasn't changed updateShortcutDisplay(); return; } if (validate && !q->isKeySequenceAvailable(keySequence)) { // The sequence had conflicts and the user said no to stealing it keySequence = oldKeySequence; } else { emit q->keySequenceChanged(keySequence); } updateShortcutDisplay(); } bool KKeySequenceWidgetPrivate::conflictWithGlobalShortcuts(const QKeySequence &keySequence) { #ifdef Q_OS_WIN //on windows F12 is reserved by the debugger at all times, so we can't use it for a global shortcut if (KKeySequenceWidget::GlobalShortcuts && keySequence.toString().contains(QStringLiteral("F12"))) { QString title = i18n("Reserved Shortcut"); QString message = i18n("The F12 key is reserved on Windows, so cannot be used for a global shortcut.\n" "Please choose another one."); KMessageBox::sorry(q, message, title); return false; } #endif #if HAVE_GLOBALACCEL if (!(checkAgainstShortcutTypes & KKeySequenceWidget::GlobalShortcuts)) { return false; } // Global shortcuts are on key+modifier shortcuts. They can clash with // each of the keys of a multi key shortcut. QHash<QKeySequence, QList<KGlobalShortcutInfo> > others; for (int i = 0; i < keySequence.count(); ++i) { QKeySequence tmp(keySequence[i]); if (!KGlobalAccel::isGlobalShortcutAvailable(tmp, componentName)) { others.insert(tmp, KGlobalAccel::getGlobalShortcutsByKey(tmp)); } } if (!others.isEmpty() && !promptStealShortcutSystemwide(q, others, keySequence)) { return true; } // The user approved stealing the shortcut. We have to steal // it immediately because KAction::setGlobalShortcut() refuses // to set a global shortcut that is already used. There is no // error it just silently fails. So be nice because this is // most likely the first action that is done in the slot // listening to keySequenceChanged(). for (int i = 0; i < keySequence.count(); ++i) { KGlobalAccel::stealShortcutSystemwide(keySequence[i]); } return false; #else Q_UNUSED(keySequence); return false; #endif } static bool shortcutsConflictWith(const QList<QKeySequence> &shortcuts, const QKeySequence &needle) { if (needle.isEmpty()) { return false; } foreach (const QKeySequence &sequence, shortcuts) { if (sequence.isEmpty()) { continue; } if (sequence.matches(needle) != QKeySequence::NoMatch || needle.matches(sequence) != QKeySequence::NoMatch) { return true; } } return false; } bool KKeySequenceWidgetPrivate::conflictWithLocalShortcuts(const QKeySequence &keySequence) { if (!(checkAgainstShortcutTypes & KKeySequenceWidget::LocalShortcuts)) { return false; } // We have actions both in the deprecated checkList and the // checkActionCollections list. Add all the actions to a single list to // be able to process them in a single loop below. // Note that this can't be done in setCheckActionCollections(), because we // keep pointers to the action collections, and between the call to // setCheckActionCollections() and this function some actions might already be // removed from the collection again. QList<QAction *> allActions; allActions += checkList; foreach (KActionCollection *collection, checkActionCollections) { allActions += collection->actions(); } // Because of multikey shortcuts we can have clashes with many shortcuts. // // Example 1: // // Application currently uses 'CTRL-X,a', 'CTRL-X,f' and 'CTRL-X,CTRL-F' // and the user wants to use 'CTRL-X'. 'CTRL-X' will only trigger as // 'activatedAmbiguously()' for obvious reasons. // // Example 2: // // Application currently uses 'CTRL-X'. User wants to use 'CTRL-X,CTRL-F'. // This will shadow 'CTRL-X' for the same reason as above. // // Example 3: // // Some weird combination of Example 1 and 2 with three shortcuts using // 1/2/3 key shortcuts. I think you can imagine. QList<QAction *> conflictingActions; //find conflicting shortcuts with existing actions foreach (QAction *qaction, allActions) { if (shortcutsConflictWith(qaction->shortcuts(), keySequence)) { // A conflict with a KAction. If that action is configurable // ask the user what to do. If not reject this keySequence. if (checkActionCollections.first()->isShortcutsConfigurable(qaction)) { conflictingActions.append(qaction); } else { wontStealShortcut(qaction, keySequence); return true; } } } if (conflictingActions.isEmpty()) { // No conflicting shortcuts found. return false; } if (stealShortcuts(conflictingActions, keySequence)) { stealActions = conflictingActions; // Announce that the user // agreed Q_FOREACH (QAction *stealAction, stealActions) { emit q->stealShortcut( keySequence, stealAction); } return false; } else { return true; } } bool KKeySequenceWidgetPrivate::conflictWithStandardShortcuts(const QKeySequence &keySequence) { if (!(checkAgainstShortcutTypes & KKeySequenceWidget::StandardShortcuts)) { return false; } KStandardShortcut::StandardShortcut ssc = KStandardShortcut::find(keySequence); if (ssc != KStandardShortcut::AccelNone && !stealStandardShortcut(ssc, keySequence)) { return true; } return false; } bool KKeySequenceWidgetPrivate::stealStandardShortcut(KStandardShortcut::StandardShortcut std, const QKeySequence &seq) { QString title = i18n("Conflict with Standard Application Shortcut"); QString message = i18n("The '%1' key combination is also used for the standard action " "\"%2\" that some applications use.\n" "Do you really want to use it as a global shortcut as well?", seq.toString(QKeySequence::NativeText), KStandardShortcut::label(std)); if (KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18n("Reassign"))) != KMessageBox::Continue) { return false; } return true; } void KKeySequenceWidgetPrivate::updateShortcutDisplay() { //empty string if no non-modifier was pressed QString s = keySequence.toString(QKeySequence::NativeText); s.replace(QLatin1Char('&'), QStringLiteral("&&")); if (isRecording) { if (modifierKeys) { if (!s.isEmpty()) { s.append(QLatin1Char(',')); } if (modifierKeys & Qt::MetaModifier) { - s += KKeyServer::modToStringUser(Qt::MetaModifier) + QLatin1Char('+'); + s += QKeySequence(Qt::MetaModifier).toString(QKeySequence::NativeText); } #if defined(Q_OS_MAC) if (modifierKeys & Qt::AltModifier) { - s += KKeyServer::modToStringUser(Qt::AltModifier) + QLatin1Char('+'); + s += QKeySequence(Qt::AltModifier).toString(QKeySequence::NativeText); } if (modifierKeys & Qt::ControlModifier) { - s += KKeyServer::modToStringUser(Qt::ControlModifier) + QLatin1Char('+'); + s += QKeySequence(Qt::ControlModifier).toString(QKeySequence::NativeText); } #else if (modifierKeys & Qt::ControlModifier) { - s += KKeyServer::modToStringUser(Qt::ControlModifier) + QLatin1Char('+'); + s += QKeySequence(Qt::ControlModifier).toString(QKeySequence::NativeText); } if (modifierKeys & Qt::AltModifier) { - s += KKeyServer::modToStringUser(Qt::AltModifier) + QLatin1Char('+'); + s += QKeySequence(Qt::AltModifier).toString(QKeySequence::NativeText); } #endif if (modifierKeys & Qt::ShiftModifier) { - s += KKeyServer::modToStringUser(Qt::ShiftModifier) + QLatin1Char('+'); + s += QKeySequence(Qt::ShiftModifier).toString(QKeySequence::NativeText); } if (modifierKeys & Qt::KeypadModifier) { - s += KKeyServer::modToStringUser(Qt::KeypadModifier) + QLatin1Char('+'); + s += QKeySequence(Qt::KeypadModifier).toString(QKeySequence::NativeText); } } else if (nKey == 0) { s = i18nc("What the user inputs now will be taken as the new shortcut", "Input"); } //make it clear that input is still going on s.append(QStringLiteral(" ...")); } if (s.isEmpty()) { s = i18nc("No shortcut defined", "None"); } s.prepend(QLatin1Char(' ')); s.append(QLatin1Char(' ')); keyButton->setText(s); } KKeySequenceButton::~KKeySequenceButton() { } //prevent Qt from special casing Tab and Backtab bool KKeySequenceButton::event(QEvent *e) { if (d->isRecording && e->type() == QEvent::KeyPress) { keyPressEvent(static_cast<QKeyEvent *>(e)); return true; } // The shortcut 'alt+c' ( or any other dialog local action shortcut ) // ended the recording and triggered the action associated with the // action. In case of 'alt+c' ending the dialog. It seems that those // ShortcutOverride events get sent even if grabKeyboard() is active. if (d->isRecording && e->type() == QEvent::ShortcutOverride) { e->accept(); return true; } if (d->isRecording && e->type() == QEvent::ContextMenu) { // is caused by Qt::Key_Menu e->accept(); return true; } return QPushButton::event(e); } void KKeySequenceButton::keyPressEvent(QKeyEvent *e) { int keyQt = e->key(); if (keyQt == -1) { // Qt sometimes returns garbage keycodes, I observed -1, if it doesn't know a key. // We cannot do anything useful with those (several keys have -1, indistinguishable) // and QKeySequence.toString() will also yield a garbage string. KMessageBox::sorry(this, i18n("The key you just pressed is not supported by Qt."), i18n("Unsupported Key")); d->cancelRecording(); return; } uint newModifiers = e->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier | Qt::KeypadModifier); //don't have the return or space key appear as first key of the sequence when they //were pressed to start editing - catch and them and imitate their effect if (!d->isRecording && ((keyQt == Qt::Key_Return || keyQt == Qt::Key_Space))) { d->startRecording(); d->modifierKeys = newModifiers; d->updateShortcutDisplay(); return; } // We get events even if recording isn't active. if (!d->isRecording) { QPushButton::keyPressEvent(e); return; } e->accept(); d->modifierKeys = newModifiers; switch (keyQt) { case Qt::Key_AltGr: //or else we get unicode salad return; case Qt::Key_Shift: case Qt::Key_Control: case Qt::Key_Alt: case Qt::Key_Meta: case Qt::Key_Super_L: case Qt::Key_Super_R: d->controlModifierlessTimout(); d->updateShortcutDisplay(); break; default: if (d->nKey == 0 && !(d->modifierKeys & ~Qt::SHIFT)) { // It's the first key and no modifier pressed. Check if this is // allowed if (!(KKeySequenceWidgetPrivate::isOkWhenModifierless(keyQt) || d->allowModifierless)) { // No it's not return; } } // We now have a valid key press. if (keyQt) { if ((keyQt == Qt::Key_Backtab) && (d->modifierKeys & Qt::SHIFT)) { keyQt = Qt::Key_Tab | d->modifierKeys; } else if (KKeyServer::isShiftAsModifierAllowed(keyQt)) { keyQt |= d->modifierKeys; } else { keyQt |= (d->modifierKeys & ~Qt::SHIFT); } if (d->nKey == 0) { d->keySequence = QKeySequence(keyQt); } else { d->keySequence = KKeySequenceWidgetPrivate::appendToSequence(d->keySequence, keyQt); } d->nKey++; if ((!d->multiKeyShortcutsAllowed) || (d->nKey >= 4)) { d->doneRecording(); return; } d->controlModifierlessTimout(); d->updateShortcutDisplay(); } } } void KKeySequenceButton::keyReleaseEvent(QKeyEvent *e) { if (e->key() == -1) { // ignore garbage, see keyPressEvent() return; } if (!d->isRecording) { QPushButton::keyReleaseEvent(e); return; } e->accept(); uint newModifiers = e->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier | Qt::KeypadModifier); //if a modifier that belongs to the shortcut was released... if ((newModifiers & d->modifierKeys) < d->modifierKeys) { d->modifierKeys = newModifiers; d->controlModifierlessTimout(); d->updateShortcutDisplay(); } } //static QKeySequence KKeySequenceWidgetPrivate::appendToSequence(const QKeySequence &seq, int keyQt) { switch (seq.count()) { case 0: return QKeySequence(keyQt); case 1: return QKeySequence(seq[0], keyQt); case 2: return QKeySequence(seq[0], seq[1], keyQt); case 3: return QKeySequence(seq[0], seq[1], seq[2], keyQt); default: return seq; } } //static bool KKeySequenceWidgetPrivate::isOkWhenModifierless(int keyQt) { //this whole function is a hack, but especially the first line of code if (QKeySequence(keyQt).toString().length() == 1) { return false; } switch (keyQt) { case Qt::Key_Return: case Qt::Key_Space: case Qt::Key_Tab: case Qt::Key_Backtab: //does this ever happen? case Qt::Key_Backspace: case Qt::Key_Delete: return false; default: return true; } } #include "moc_kkeysequencewidget.cpp"