diff --git a/krunner/view.cpp b/krunner/view.cpp index 3ff2aad0a..b0569b63a 100644 --- a/krunner/view.cpp +++ b/krunner/view.cpp @@ -1,446 +1,448 @@ /* * Copyright 2014 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "view.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "appadaptor.h" View::View(QWindow *) : PlasmaQuick::Dialog(), m_offset(.5), m_floating(false), m_plasmaShell(nullptr) { initWayland(); setClearBeforeRendering(true); setColor(QColor(Qt::transparent)); setFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); KCrash::setFlags(KCrash::AutoRestart); + //used only by screen readers + setTitle(i18n("KRunner")); m_config = KConfigGroup(KSharedConfig::openConfig(QStringLiteral("krunnerrc")), "General"); setFreeFloating(m_config.readEntry("FreeFloating", false)); reloadConfig(); new AppAdaptor(this); QDBusConnection::sessionBus().registerObject(QStringLiteral("/App"), this); QAction *a = new QAction(0); QObject::connect(a, &QAction::triggered, this, &View::displayOrHide); a->setText(i18n("Run Command")); a->setObjectName(QStringLiteral("run command")); a->setProperty("componentDisplayName", i18nc("Name for krunner shortcuts category", "Run Command")); KGlobalAccel::self()->setDefaultShortcut(a, QList() << QKeySequence(Qt::ALT + Qt::Key_Space), KGlobalAccel::NoAutoloading); KGlobalAccel::self()->setShortcut(a, QList() << QKeySequence(Qt::ALT + Qt::Key_Space) << QKeySequence(Qt::ALT + Qt::Key_F2) << Qt::Key_Search); a = new QAction(0); QObject::connect(a, &QAction::triggered, this, &View::displayWithClipboardContents); a->setText(i18n("Run Command on clipboard contents")); a->setObjectName(QStringLiteral("run command on clipboard contents")); a->setProperty("componentDisplayName", i18nc("Name for krunner shortcuts category", "Run Command")); KGlobalAccel::self()->setDefaultShortcut(a, QList() << QKeySequence(Qt::ALT+Qt::SHIFT+Qt::Key_F2)); KGlobalAccel::self()->setShortcut(a, QList() << QKeySequence(Qt::ALT+Qt::SHIFT+Qt::Key_F2)); m_qmlObj = new KDeclarative::QmlObject(this); m_qmlObj->setInitializationDelayed(true); connect(m_qmlObj, &KDeclarative::QmlObject::finished, this, &View::objectIncubated); KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel")); KConfigGroup cg(KSharedConfig::openConfig(QStringLiteral("kdeglobals")), "KDE"); const QString packageName = cg.readEntry("LookAndFeelPackage", QString()); if (!packageName.isEmpty()) { package.setPath(packageName); } m_qmlObj->setSource(QUrl::fromLocalFile(package.filePath("runcommandmainscript"))); m_qmlObj->engine()->rootContext()->setContextProperty(QStringLiteral("runnerWindow"), this); m_qmlObj->completeInitialization(); auto screenRemoved = [this](QScreen* screen) { if (screen == this->screen()) { setScreen(qGuiApp->primaryScreen()); hide(); } }; auto screenAdded = [this](QScreen* screen) { connect(screen, &QScreen::geometryChanged, this, &View::screenGeometryChanged); screenGeometryChanged(); }; foreach(QScreen* s, QGuiApplication::screens()) screenAdded(s); connect(qGuiApp, &QGuiApplication::screenAdded, this, screenAdded); connect(qGuiApp, &QGuiApplication::screenRemoved, this, screenRemoved); connect(KWindowSystem::self(), &KWindowSystem::workAreaChanged, this, &View::resetScreenPos); connect(this, &View::visibleChanged, this, &View::resetScreenPos); KDirWatch::self()->addFile(m_config.name()); // Catch both, direct changes to the config file ... connect(KDirWatch::self(), &KDirWatch::dirty, this, &View::reloadConfig); connect(KDirWatch::self(), &KDirWatch::created, this, &View::reloadConfig); if (m_floating) { setLocation(Plasma::Types::Floating); } else { setLocation(Plasma::Types::TopEdge); } connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, &View::slotFocusWindowChanged); } View::~View() { } void View::initWayland() { if (!KWindowSystem::isPlatformWayland()) { return; } using namespace KWayland::Client; auto connection = ConnectionThread::fromApplication(this); if (!connection) { return; } Registry *registry = new Registry(this); registry->create(connection); QObject::connect(registry, &Registry::interfacesAnnounced, this, [registry, this] { const auto interface = registry->interface(Registry::Interface::PlasmaShell); if (interface.name != 0) { m_plasmaShell = registry->createPlasmaShell(interface.name, interface.version, this); } } ); registry->setup(); connection->roundtrip(); } void View::objectIncubated() { connect(m_qmlObj->rootObject(), SIGNAL(widthChanged()), this, SLOT(resetScreenPos())); setMainItem(qobject_cast(m_qmlObj->rootObject())); } void View::slotFocusWindowChanged() { if (!QGuiApplication::focusWindow()) { setVisible(false); } } bool View::freeFloating() const { return m_floating; } void View::setFreeFloating(bool floating) { if (m_floating == floating) { return; } m_floating = floating; if (m_floating) { setLocation(Plasma::Types::Floating); } else { setLocation(Plasma::Types::TopEdge); } positionOnScreen(); } void View::reloadConfig() { m_config.config()->reparseConfiguration(); setFreeFloating(m_config.readEntry("FreeFloating", false)); const QStringList history = m_config.readEntry("history", QStringList()); if (m_history != history) { m_history = history; emit historyChanged(); } } bool View::event(QEvent *event) { // QXcbWindow overwrites the state in its show event. There are plans // to fix this in 5.4, but till then we must explicitly overwrite it // each time. const bool retval = Dialog::event(event); bool setState = event->type() == QEvent::Show; if (event->type() == QEvent::PlatformSurface) { if (auto e = dynamic_cast(event)) { setState = e->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated; } } if (setState) { KWindowSystem::setState(winId(), NET::SkipTaskbar | NET::SkipPager); } if (m_plasmaShell && event->type() == QEvent::Expose) { using namespace KWayland::Client; auto ee = static_cast(event); if (ee->region().isNull()) { return retval; } if (!m_plasmaShellSurface && isVisible()) { Surface *s = Surface::fromWindow(this); if (!s) { return retval; } m_plasmaShellSurface = m_plasmaShell->createSurface(s, this); m_plasmaShellSurface->setPanelBehavior(PlasmaShellSurface::PanelBehavior::WindowsGoBelow); m_plasmaShellSurface->setPanelTakesFocus(true); m_plasmaShellSurface->setRole(PlasmaShellSurface::Role::Panel); //this should be on showEvent, but it was too soon so none of those had any effect KWindowSystem::setOnAllDesktops(winId(), true); positionOnScreen(); requestActivate(); //positionOnScreen tried to position it in the position it already had, so no moveevent happens and we need to manually posiyion the surface m_plasmaShellSurface->setPosition(position()); } } else if (event->type() == QEvent::Hide) { delete m_plasmaShellSurface; } else if (m_plasmaShellSurface && event->type() == QEvent::Move) { QMoveEvent *me = static_cast(event); m_plasmaShellSurface->setPosition(me->pos()); } return retval; } void View::resizeEvent(QResizeEvent *event) { if (event->oldSize().width() != event->size().width()) { positionOnScreen(); } } void View::showEvent(QShowEvent *event) { KWindowSystem::setOnAllDesktops(winId(), true); Dialog::showEvent(event); positionOnScreen(); requestActivate(); } void View::screenGeometryChanged() { if (isVisible()) { positionOnScreen(); } } void View::resetScreenPos() { if (isVisible() && !m_floating) { positionOnScreen(); } } void View::positionOnScreen() { QScreen *shownOnScreen = QGuiApplication::primaryScreen(); Q_FOREACH (QScreen* screen, QGuiApplication::screens()) { if (screen->geometry().contains(QCursor::pos(screen))) { shownOnScreen = screen; break; } } setScreen(shownOnScreen); const QRect r = shownOnScreen->availableGeometry(); if (m_floating && !m_customPos.isNull()) { int x = qBound(r.left(), m_customPos.x(), r.right() - width()); int y = qBound(r.top(), m_customPos.y(), r.bottom() - height()); setPosition(x, y); show(); return; } const int w = width(); int x = r.left() + (r.width() * m_offset) - (w / 2); int y = r.top(); if (m_floating) { y += r.height() / 3; } x = qBound(r.left(), x, r.right() - width()); y = qBound(r.top(), y, r.bottom() - height()); setPosition(x, y); if (m_floating) { KWindowSystem::setOnDesktop(winId(), KWindowSystem::currentDesktop()); KWindowSystem::setType(winId(), NET::Normal); //Turn the sliding effect off KWindowEffects::slideWindow(winId(), KWindowEffects::NoEdge, 0); } else { KWindowSystem::setOnAllDesktops(winId(), true); KWindowEffects::slideWindow(winId(), KWindowEffects::TopEdge, 0); } KWindowSystem::forceActiveWindow(winId()); //qDebug() << "moving to" << m_screenPos[screen]; } void View::displayOrHide() { if (isVisible() && !QGuiApplication::focusWindow()) { KWindowSystem::forceActiveWindow(winId()); return; } setVisible(!isVisible()); } void View::display() { setVisible(true); } void View::displaySingleRunner(const QString &runnerName) { setVisible(true); m_qmlObj->rootObject()->setProperty("runner", runnerName); m_qmlObj->rootObject()->setProperty("query", QString()); } void View::displayWithClipboardContents() { setVisible(true); m_qmlObj->rootObject()->setProperty("runner", QString()); m_qmlObj->rootObject()->setProperty("query", QGuiApplication::clipboard()->text(QClipboard::Selection)); } void View::query(const QString &term) { setVisible(true); m_qmlObj->rootObject()->setProperty("runner", QString()); m_qmlObj->rootObject()->setProperty("query", term); } void View::querySingleRunner(const QString &runnerName, const QString &term) { setVisible(true); m_qmlObj->rootObject()->setProperty("runner", runnerName); m_qmlObj->rootObject()->setProperty("query", term); } void View::switchUser() { QDBusConnection::sessionBus().asyncCall( QDBusMessage::createMethodCall(QStringLiteral("org.kde.ksmserver"), QStringLiteral("/KSMServer"), QStringLiteral("org.kde.KSMServerInterface"), QStringLiteral("openSwitchUserDialog")) ); } void View::displayConfiguration() { QProcess::startDetached(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("plasmasearch")); } QStringList View::history() const { return m_history; } void View::addToHistory(const QString &item) { if (item == QLatin1String("SESSIONS")) { return; } if (!KAuthorized::authorize(QStringLiteral("lineedit_text_completion"))) { return; } m_history.removeOne(item); m_history.prepend(item); while (m_history.count() > 50) { // make configurable? m_history.removeLast(); } emit historyChanged(); writeHistory(); m_config.sync(); } void View::removeFromHistory(int index) { if (index < 0 || index >= m_history.count()) { return; } m_history.removeAt(index); emit historyChanged(); writeHistory(); } void View::writeHistory() { m_config.writeEntry("history", m_history); } diff --git a/lookandfeel/contents/runcommand/RunCommand.qml b/lookandfeel/contents/runcommand/RunCommand.qml index 5f4cc4adc..e2133e4fb 100644 --- a/lookandfeel/contents/runcommand/RunCommand.qml +++ b/lookandfeel/contents/runcommand/RunCommand.qml @@ -1,197 +1,238 @@ /* * Copyright 2014 Marco Martin * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import QtQuick 2.0 +import QtQuick 2.6 import QtQuick.Layouts 1.1 import QtQuick.Window 2.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.milou 0.1 as Milou ColumnLayout { id: root property string query property string runner property bool showHistory: false LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft LayoutMirroring.childrenInherit: true onQueryChanged: { queryField.text = query; } Connections { target: runnerWindow onVisibleChanged: { if (runnerWindow.visible) { queryField.forceActiveFocus(); listView.currentIndex = -1 } else { root.query = ""; root.runner = "" root.showHistory = false } } } RowLayout { Layout.alignment: Qt.AlignTop PlasmaComponents.ToolButton { iconSource: "configure" onClicked: { runnerWindow.visible = false runnerWindow.displayConfiguration() } Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Configure") Accessible.description: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Configure Search Plugins") } PlasmaComponents.TextField { id: queryField property bool allowCompletion: false clearButtonShown: true Layout.minimumWidth: units.gridUnit * 25 activeFocusOnPress: true placeholderText: results.runnerName ? i18ndc("plasma_lookandfeel_org.kde.lookandfeel", "Textfield placeholder text, query specific KRunner", "Search '%1'...", results.runnerName) : i18ndc("plasma_lookandfeel_org.kde.lookandfeel", "Textfield placeholder text", "Search...") onTextChanged: { root.query = queryField.text if (allowCompletion && length > 0) { var history = runnerWindow.history // search the first item in the history rather than the shortest matching one // this way more recently used entries take precedence over older ones (Bug 358985) for (var i = 0, j = history.length; i < j; ++i) { var item = history[i] if (item.toLowerCase().indexOf(text.toLowerCase()) === 0) { var oldText = text text = text + item.substr(oldText.length) select(text.length, oldText.length) break } } } } Keys.onPressed: allowCompletion = (event.key !== Qt.Key_Backspace && event.key !== Qt.Key_Delete) Keys.onUpPressed: { if (length === 0) { - root.showHistory = true + root.showHistory = true; + listView.forceActiveFocus(); + } else { + results.forceActiveFocus(); } } Keys.onDownPressed: { if (length === 0) { - root.showHistory = true + root.showHistory = true; + listView.forceActiveFocus(); + } else { + results.forceActiveFocus(); } } + Keys.onEnterPressed: results.runCurrentIndex() + Keys.onReturnPressed: results.runCurrentIndex() Keys.onEscapePressed: { runnerWindow.visible = false } - Keys.forwardTo: [listView, results] } PlasmaComponents.ToolButton { iconSource: "window-close" onClicked: runnerWindow.visible = false Accessible.name: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Close") Accessible.description: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Close Search") } } PlasmaExtras.ScrollArea { Layout.alignment: Qt.AlignTop visible: results.count > 0 enabled: visible Layout.fillWidth: true Layout.preferredHeight: Math.min(Screen.height, results.contentHeight) Milou.ResultsView { id: results queryString: root.query runner: root.runner + Keys.onPressed: { + if (event.text != "") { + queryField.text += event.text; + queryField.focus = true; + } + } + onActivated: { runnerWindow.addToHistory(queryString) runnerWindow.visible = false } onUpdateQueryString: { queryField.text = text queryField.cursorPosition = cursorPosition } } } PlasmaExtras.ScrollArea { Layout.alignment: Qt.AlignTop Layout.fillWidth: true visible: root.query.length === 0 && listView.count > 0 // don't accept keyboard input when not visible so the keys propagate to the other list enabled: visible Layout.preferredHeight: Math.min(Screen.height, listView.contentHeight) ListView { id: listView // needs this id so the delegate can access it keyNavigationWraps: true highlight: PlasmaComponents.Highlight {} highlightMoveDuration: 0 + activeFocusOnTab: true // we store 50 entries in the history but only show 20 in the UI so it doesn't get too huge model: root.showHistory ? runnerWindow.history.slice(0, 20) : [] delegate: Milou.ResultDelegate { id: resultDelegate width: listView.width typeText: index === 0 ? i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Recent Queries") : "" additionalActions: [{ icon: "list-remove", text: i18nd("plasma_lookandfeel_org.kde.lookandfeel", "Remove") }] + Accessible.description: i18n("in category recent queries") } + onActiveFocusChanged: { + if (!activeFocus && currentIndex == listView.count-1) { + currentIndex = 0; + } + } Keys.onReturnPressed: runCurrentIndex() Keys.onEnterPressed: runCurrentIndex() - - Keys.onTabPressed: incrementCurrentIndex() - Keys.onBacktabPressed: decrementCurrentIndex() + + Keys.onTabPressed: { + if (currentIndex == listView.count-1) { + listView.nextItemInFocusChain(true).forceActiveFocus(); + } else { + incrementCurrentIndex() + } + } + Keys.onBacktabPressed: { + if (currentIndex == 0) { + listView.nextItemInFocusChain(false).forceActiveFocus(); + } else { + decrementCurrentIndex() + } + } + Keys.onPressed: { + if (event.text != "") { + queryField.text += event.text; + queryField.focus = true; + } + } + Keys.onUpPressed: decrementCurrentIndex() Keys.onDownPressed: incrementCurrentIndex() function runCurrentIndex() { var entry = runnerWindow.history[currentIndex] if (entry) { queryField.text = entry + queryField.forceActiveFocus(); } } function runAction(actionIndex) { if (actionIndex === 0) { // QStringList changes just reset the model, so we'll remember the index and set it again var currentIndex = listView.currentIndex runnerWindow.removeFromHistory(currentIndex) listView.currentIndex = currentIndex } } } } }