diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 1f6d3aa..cce85ff 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -1,32 +1,33 @@ add_definitions(-DTRANSLATION_DOMAIN=\"milou\") set (lib_SRCS preview.cpp previewplugin.cpp sourcesmodel.cpp draghelper.cpp + mousehelper.cpp ) add_library(milou SHARED ${lib_SRCS}) set_target_properties(milou PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} ) target_link_libraries (milou Qt5::Qml Qt5::Quick Qt5::Widgets # for QAction... KF5::Service KF5::Plasma KF5::Runner ) install( FILES miloupreviewplugin.desktop DESTINATION ${SERVICETYPES_INSTALL_DIR}) generate_export_header(milou BASE_NAME MILOU EXPORT_FILE_NAME milou_export.h) install(TARGETS milou EXPORT MilouLibraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) add_subdirectory(qml) add_subdirectory(previews) add_subdirectory(test) diff --git a/lib/mousehelper.cpp b/lib/mousehelper.cpp new file mode 100644 index 0000000..d3e8bcf --- /dev/null +++ b/lib/mousehelper.cpp @@ -0,0 +1,40 @@ +/* + * Copyright 2017 Aetf + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 . + * + */ + +#include "mousehelper.h" + +#include + +using namespace Milou; + +MouseHelper::MouseHelper(QObject* parent) + : QObject(parent) +{ +} + +MouseHelper::~MouseHelper() +{ +} + +QPointF MouseHelper::globalMousePosition() const +{ + return QCursor::pos(); +} diff --git a/lib/mousehelper.h b/lib/mousehelper.h new file mode 100644 index 0000000..f3e18c0 --- /dev/null +++ b/lib/mousehelper.h @@ -0,0 +1,44 @@ +/* + * Copyright 2017 Aetf + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 . + * + */ + +#ifndef MOUSEHELPER_H +#define MOUSEHELPER_H + +#include +#include + +#include "milou_export.h" + +namespace Milou { + +class MILOU_EXPORT MouseHelper : public QObject +{ + Q_OBJECT + +public: + explicit MouseHelper(QObject* parent = nullptr); + ~MouseHelper(); + + Q_INVOKABLE QPointF globalMousePosition() const; +}; + +} +#endif // MOUSEHELPER_H diff --git a/lib/qml/ResultDelegate.qml b/lib/qml/ResultDelegate.qml index 6fe6549..1820e88 100644 --- a/lib/qml/ResultDelegate.qml +++ b/lib/qml/ResultDelegate.qml @@ -1,248 +1,256 @@ /* * This file is part of the KDE Milou Project * Copyright (C) 2013-2014 Vishesh Handa * Copyright (C) 2015-2016 Kai Uwe Broulik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ import QtQuick 2.1 import QtQuick.Layouts 1.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.kquickcontrolsaddons 2.0 as QtExtra import "globals.js" as Globals MouseArea { id: resultDelegate property variant theModel: model readonly property bool isCurrent: ListView.isCurrentItem // cannot properly Connect {} to this readonly property bool sectionHasChanged: typeof reversed !== "undefined" && ( (reversed && ListView.section != ListView.nextSection) || (!reversed && ListView.section != ListView.previousSection) ) property int activeAction: -1 property string typeText: sectionHasChanged ? ListView.section : "" property var additionalActions: typeof actions !== "undefined" ? actions : [] property bool __pressed: false property int __pressX: -1 property int __pressY: -1 onIsCurrentChanged: { if (!isCurrent) { activeAction = -1 } } function activateNextAction() { if (activeAction === actionsRepeater.count - 1) { // last action, do nothing return false } ++activeAction return true } function activatePreviousAction() { if (activeAction < 0) { // no action, do nothing return false } --activeAction return true } function activateLastAction() { activeAction = actionsRepeater.count - 1 } width: listItem.implicitWidth height: listItem.implicitHeight acceptedButtons: Qt.LeftButton hoverEnabled: true - onEntered: { - listView.currentIndex = index - } - onPressed: { __pressed = true; __pressX = mouse.x; __pressY = mouse.y; } onReleased: { if (__pressed) { listView.currentIndex = model.index listView.runCurrentIndex() } __pressed = false; __pressX = -1; __pressY = -1; } onPositionChanged: { if (__pressX != -1 && typeof dragHelper !== "undefined" && dragHelper.isDrag(__pressX, __pressY, mouse.x, mouse.y)) { var mimeData = ListView.view.model.getMimeData(index); if (mimeData) { dragHelper.startDrag(root, mimeData, model.decoration); __pressed = false; __pressX = -1; __pressY = -1; } } + + if (!listView.moved && listView.mouseMovedGlobally()) { + listView.moved = true + listView.currentIndex = index + } } onContainsMouseChanged: { if (!containsMouse) { __pressed = false; __pressX = -1; __pressY = -1; + } else { + if (listView.moved) { + listView.currentIndex = index + } else if (listView.mouseMovedGlobally()) { + listView.moved = true + listView.currentIndex = index + } } } PlasmaComponents.Label { id: typeText text: resultDelegate.typeText color: theme.textColor opacity: 0.5 horizontalAlignment: Text.AlignRight verticalAlignment: Text.AlignVCenter elide: Text.ElideRight width: Globals.CategoryWidth - Globals.CategoryRightMargin anchors { left: parent.left verticalCenter: listItem.verticalCenter } } PlasmaComponents.ListItem { id: listItem // fake pressed look checked: resultDelegate.pressed separatorVisible: resultDelegate.sectionHasChanged && !resultDelegate.isCurrent && (index === 0 || resultDelegate.ListView.view.currentIndex !== (index - 1)) Item { id: labelWrapper anchors { left: parent.left right: parent.right leftMargin: Globals.CategoryWidth } height: Math.max(typePixmap.height, displayLabel.height, subtextLabel.height) RowLayout { anchors { left: parent.left right: actionsRow.left rightMargin: units.smallSpacing } PlasmaCore.IconItem { id: typePixmap Layout.preferredWidth: Globals.IconSize Layout.preferredHeight: Globals.IconSize Layout.fillHeight: true source: model.decoration usesPlasmaTheme: false animated: false } PlasmaComponents.Label { id: displayLabel text: String(typeof modelData !== "undefined" ? modelData : model.display) height: undefined elide: Text.ElideMiddle wrapMode: Text.NoWrap maximumLineCount: 1 verticalAlignment: Text.AlignVCenter Layout.maximumWidth: labelWrapper.width - typePixmap.width - actionsRow.width } PlasmaComponents.Label { id: subtextLabel text: model.isDuplicate > 1 || resultDelegate.isCurrent ? String(model.subtext || "") : "" color: theme.textColor opacity: 0.3 height: undefined elide: Text.ElideMiddle wrapMode: Text.NoWrap maximumLineCount: 1 verticalAlignment: Text.AlignVCenter Layout.fillWidth: true } } Row { id: actionsRow anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter visible: resultDelegate.isCurrent Repeater { id: actionsRepeater model: resultDelegate.additionalActions PlasmaComponents.ToolButton { width: height height: listItem.height visible: modelData.visible || true enabled: modelData.enabled || true tooltip: { var text = modelData.text || "" if (index === 0) { // Shift+Return will invoke first action text = i18nc("placeholder is action e.g. run in terminal, in parenthesis is shortcut", "%1 (Shift+Return)", text) } return text } checkable: checked checked: resultDelegate.activeAction === index PlasmaCore.IconItem { anchors.centerIn: parent width: Globals.IconSize height: Globals.IconSize // ToolButton cannot cope with QIcon source: modelData.icon || "" active: parent.hovered || parent.checked } onClicked: resultDelegate.ListView.view.runAction(index) } } } } } } diff --git a/lib/qml/ResultsView.qml b/lib/qml/ResultsView.qml index 8a9c85c..38a5cb9 100644 --- a/lib/qml/ResultsView.qml +++ b/lib/qml/ResultsView.qml @@ -1,160 +1,169 @@ /* * This file is part of the KDE Milou Project * Copyright (C) 2013-2014 Vishesh Handa * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ import QtQuick 2.1 import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.milou 0.2 as Milou import "globals.js" as Globals ListView { id: listView property alias queryString: resultModel.queryString property alias runner: resultModel.runner property alias runnerName: resultModel.runnerName property alias runnerIcon: resultModel.runnerIcon property bool reversed signal activated signal updateQueryString(string text, int cursorPosition) // NOTE this also flips increment/decrementCurrentIndex (Bug 360789) verticalLayoutDirection: reversed ? ListView.BottomToTop : ListView.TopToBottom keyNavigationWraps: true highlight: PlasmaComponents.Highlight {} highlightMoveDuration: 0 section { criteria: ViewSection.FullString property: "type" } // This is used to keep track if the user has pressed enter before // the first result has been shown, in the case the first result should // be run when the model is populated property bool runAutomatically + // This is used to disable mouse selection if the user interacts only with keyboard + property bool moved: false + property point savedMousePosition: Milou.MouseHelper.globalMousePosition() + function mouseMovedGlobally() { + return savedMousePosition != Milou.MouseHelper.globalMousePosition(); + } + Milou.DragHelper { id: dragHelper dragIconSize: units.iconSizes.medium } model: Milou.SourcesModel { id: resultModel queryLimit: 20 // Internally when the query string changes, the model is reset // and the results are presented onModelReset: { listView.currentIndex = 0 + listView.moved = false + listView.savedMousePosition = Milou.MouseHelper.globalMousePosition() if (runAutomatically) { runCurrentIndex(); } } onUpdateSearchTerm: listView.updateQueryString(text, pos) } delegate: ResultDelegate { id: resultDelegate width: listView.width } // // vHanda: Ideally this should have gotten handled in the delagte's onReturnPressed // code, but the ListView doesn't seem forward keyboard events to the delgate when // it is not in activeFocus. Even manually adding Keys.forwardTo: resultDelegate // doesn't make any difference! Keys.onReturnPressed: runCurrentIndex(event); Keys.onEnterPressed: runCurrentIndex(event); function runCurrentIndex(event) { if (!currentItem) { runAutomatically = true return; } else { // If user presses Shift+Return to invoke an action, invoke the first runner action if (event && event.modifiers === Qt.ShiftModifier && currentItem.additionalActions && currentItem.additionalActions.length > 0) { runAction(0) return } if (currentItem.activeAction > -1) { runAction(currentItem.activeAction) return } if (resultModel.run(currentIndex)) { activated() } runAutomatically = false } } function runAction(index) { if (resultModel.runAction(currentIndex, index)) { activated() } } Keys.onTabPressed: { if (!currentItem || !currentItem.activateNextAction()) { if (reversed) { decrementCurrentIndex() } else { incrementCurrentIndex() } } } Keys.onBacktabPressed: { if (!currentItem || !currentItem.activatePreviousAction()) { if (reversed) { incrementCurrentIndex() } else { decrementCurrentIndex() } // activate previous action cannot know whether we want to back tab from an action // to the main result or back tab from another search result, so we explicitly highlight // the last action here to provide a consistent navigation experience if (currentItem) { currentItem.activateLastAction() } } } Keys.onUpPressed: reversed ? incrementCurrentIndex() : decrementCurrentIndex(); Keys.onDownPressed: reversed ? decrementCurrentIndex() : incrementCurrentIndex(); boundsBehavior: Flickable.StopAtBounds function loadSettings() { resultModel.loadSettings() } function setQueryString(string) { resultModel.queryString = string runAutomatically = false } } diff --git a/lib/qml/qmlplugins.cpp b/lib/qml/qmlplugins.cpp index b10f635..8d05990 100644 --- a/lib/qml/qmlplugins.cpp +++ b/lib/qml/qmlplugins.cpp @@ -1,41 +1,46 @@ /* * This file is part of the KDE Milou Project * Copyright (C) 2013 Vishesh Handa * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #include "qmlplugins.h" #include "sourcesmodel.h" #include "preview.h" #include "draghelper.h" +#include "mousehelper.h" #include void QmlPlugins::initializeEngine(QQmlEngine *, const char *) { } void QmlPlugins::registerTypes(const char *uri) { qmlRegisterType (uri, 0, 1, "SourcesModel"); qmlRegisterType (uri, 0, 1, "Preview"); qmlRegisterType (uri, 0, 2, "DragHelper"); + qmlRegisterSingletonType (uri, 0, 1, "MouseHelper", + [](QQmlEngine*, QJSEngine*) -> QObject* { + return new Milou::MouseHelper(); + }); }