diff --git a/lib/search/CMakeLists.txt b/lib/search/CMakeLists.txt index f64eaa9..b884b5a 100644 --- a/lib/search/CMakeLists.txt +++ b/lib/search/CMakeLists.txt @@ -16,6 +16,8 @@ target_link_libraries(${PROJECT} Qt6::Core) set_target_properties(${PROJECT} PROPERTIES VERSION 0.1 SOVERSION 0) +add_definitions( -DINSTALL_LIBDIR="${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") +add_subdirectory(plugins) install(TARGETS ${PROJECT} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/) diff --git a/lib/search/glaciersearchplugin.h b/lib/search/glaciersearchplugin.h index cb74602..0193086 100644 --- a/lib/search/glaciersearchplugin.h +++ b/lib/search/glaciersearchplugin.h @@ -28,9 +28,18 @@ class GLACIER_EXPORT GlacierSearchPlugin : public QObject { Q_OBJECT public: + struct SearchResult + { + QString iconTitle; + QString iconSource; + QString category; + QString extraCaption; + QVariant action; + }; + virtual void search(QString searchString) = 0; signals: - void searchResultReady(QMap results); + void searchResultReady(QList results); }; Q_DECLARE_INTERFACE(GlacierSearchPlugin, "GlacierHome.SearchPlugin") diff --git a/lib/search/plugins/CMakeLists.txt b/lib/search/plugins/CMakeLists.txt new file mode 100644 index 0000000..7983944 --- /dev/null +++ b/lib/search/plugins/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(application) diff --git a/lib/search/plugins/application/CMakeLists.txt b/lib/search/plugins/application/CMakeLists.txt new file mode 100644 index 0000000..af78e66 --- /dev/null +++ b/lib/search/plugins/application/CMakeLists.txt @@ -0,0 +1,19 @@ +SET(PLUGINNAME application) + +set(SRC ${PLUGINNAME}searchplugin.cpp) +SET(HEADERS ${PLUGINNAME}searchplugin.h) + +include_directories(${CMAKE_SOURCE_DIR}/lib) +set(CMAKE_AUTOMOC ON) +add_definitions(-DQT_PLUGIN) + +add_library(${PLUGINNAME} MODULE ${SRC} ${HEADERS}) + +target_link_libraries(${PLUGINNAME} PUBLIC + Qt6::Core + Qt6::DBus + GlacierHome::Search + PkgConfig::LIPSTICK) + +install(TARGETS ${PLUGINNAME} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/glacier-home/plugins/search) diff --git a/lib/search/plugins/application/applicationsearchplugin.cpp b/lib/search/plugins/application/applicationsearchplugin.cpp new file mode 100644 index 0000000..1a5932b --- /dev/null +++ b/lib/search/plugins/application/applicationsearchplugin.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 Chupligin Sergey + * + * 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 "applicationsearchplugin.h" + +ApplicationSearchPlugin::ApplicationSearchPlugin(QObject *parent) + :m_launchModel(new LauncherModel) +{ +} + +void ApplicationSearchPlugin::search(QString searchString) +{ + qDebug() << Q_FUNC_INFO << searchString; + m_searchResults.clear(); + + for(int i = 0; i < m_launchModel.itemCount(); i++) { + QObject* item = m_launchModel.get(i); + if(item->property("title").toString().toLower().indexOf(searchString) != -1){ + SearchResult result; + result.iconTitle = item->property("title").toString(); + + QString iconSource = item->property("iconId").toString(); + if(iconSource.isEmpty()) { + iconSource = "/usr/share/glacier-home/qml/theme/default-icon.png"; + } else { + if(iconSource.startsWith("/")) { + iconSource = "file://" + iconSource; + } else if(!iconSource.startsWith("file:///")) { + iconSource = "image://theme/" + iconSource; + } + } + result.iconSource = iconSource; + + result.category = tr("Application"); + result.extraCaption = tr("installed on your device"); + + m_searchResults.push_back(result); + } + } + + if(!m_searchResults.isEmpty()) { + emit searchResultReady(m_searchResults); + } +} diff --git a/lib/search/plugins/application/applicationsearchplugin.h b/lib/search/plugins/application/applicationsearchplugin.h new file mode 100644 index 0000000..631294e --- /dev/null +++ b/lib/search/plugins/application/applicationsearchplugin.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 Chupligin Sergey + * + * 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 APPLICATIONSEARCHPLUGIN_H +#define APPLICATIONSEARCHPLUGIN_H + +#include +#include + +class ApplicationSearchPlugin : public GlacierSearchPlugin +{ + Q_OBJECT + Q_INTERFACES(GlacierSearchPlugin) + Q_PLUGIN_METADATA(IID "GlacierHome.SearchPlugin") +public: + explicit ApplicationSearchPlugin(QObject *parent = nullptr); + void search(QString searchString); + +private: + LauncherModel m_launchModel; + QList m_searchResults; +}; + +#endif // APPLICATIONSEARCHPLUGIN_H diff --git a/lib/search/searchpluginmanager.cpp b/lib/search/searchpluginmanager.cpp index 19f461d..edb415d 100644 --- a/lib/search/searchpluginmanager.cpp +++ b/lib/search/searchpluginmanager.cpp @@ -22,6 +22,10 @@ #include #include +#ifndef INSTALL_LIBDIR + #error INTALLINSTALL_LIBDIR is not set! +#endif + SearchPluginManager::SearchPluginManager(QObject* parent) : QObject { parent } { @@ -31,26 +35,45 @@ SearchPluginManager::SearchPluginManager(QObject* parent) SearchPluginManager::~SearchPluginManager() { foreach (const GlacierSearchPlugin* plugin, m_pluginList) { - disconnect(plugin, &GlacierSearchPlugin::searchResultReady, this, &SearchPluginManager::searchResultReady); + disconnect(plugin, &GlacierSearchPlugin::searchResultReady, this, &SearchPluginManager::searchResultPluginHandler); delete plugin; } } +void SearchPluginManager::search(QString searchString) +{ + m_searchResults.clear(); + + foreach (GlacierSearchPlugin* plugin, m_pluginList) { + plugin->search(searchString); + } +} + void SearchPluginManager::loadSearchPlugins() { - QDir pluginsDir("/usr/lib/glacier-home/plugins/search"); + QDir pluginsDir(QString::fromUtf8(INSTALL_LIBDIR) + "/glacier-home/plugins/search"); QList pluginsLibList = pluginsDir.entryList(QDir::Files); for (const QString& file : qAsConst(pluginsLibList)) { - QPluginLoader pluginLoader(pluginsDir.path() + file); + QPluginLoader pluginLoader(pluginsDir.path() + "/" + file); QObject* plugin = pluginLoader.instance(); if (plugin) { - GlacierSearchPlugin* sourcePlugin = qobject_cast(plugin); - if (sourcePlugin != nullptr) { - m_pluginList.push_back(sourcePlugin); - connect(sourcePlugin, &GlacierSearchPlugin::searchResultReady, this, &SearchPluginManager::searchResultReady); + GlacierSearchPlugin* searchPlugin = qobject_cast(plugin); + if (searchPlugin != nullptr) { + m_pluginList.push_back(searchPlugin); + connect(searchPlugin, &GlacierSearchPlugin::searchResultReady, this, &SearchPluginManager::searchResultReady); + } else { + qWarning() << "CANT CAST PLIUGIN FROM" << pluginsDir.path() + "/" + file; } + } else { + delete plugin; } } } + +void SearchPluginManager::searchResultPluginHandler(QList results) +{ + m_searchResults.append(results); + emit searchResultReady(m_searchResults); +} diff --git a/lib/search/searchpluginmanager.h b/lib/search/searchpluginmanager.h index 446415b..914192c 100644 --- a/lib/search/searchpluginmanager.h +++ b/lib/search/searchpluginmanager.h @@ -31,14 +31,18 @@ class SearchPluginManager : public QObject { explicit SearchPluginManager(QObject* parent = nullptr); virtual ~SearchPluginManager(); + void search(QString searchString); + signals: - void searchResultReady(QMap results); + void searchResultReady(QList results); private slots: void loadSearchPlugins(); + void searchResultPluginHandler(QList results); private: QList m_pluginList; + QList m_searchResults; }; #endif // SEARCHPLUGINMANAGER_H diff --git a/src/models/searchmodel.cpp b/src/models/searchmodel.cpp index bb26ce5..d219954 100644 --- a/src/models/searchmodel.cpp +++ b/src/models/searchmodel.cpp @@ -28,19 +28,60 @@ SearchModel::SearchModel(QObject* parent) m_hash.insert(Qt::UserRole + 2, QByteArray("category")); m_hash.insert(Qt::UserRole + 3, QByteArray("extraCaption")); m_hash.insert(Qt::UserRole + 4, QByteArray("action")); + + connect(m_manager, &SearchPluginManager::searchResultReady, this, &SearchModel::searchResultHandler); } int SearchModel::rowCount(const QModelIndex& parent) const { - return -1; + return m_searchResults.count(); } QVariant SearchModel::data(const QModelIndex& index, int role) const { + if (!index.isValid()) { + return QVariant(); + } + + if (index.row() >= m_searchResults.size()) { + return QVariant(); + } + + GlacierSearchPlugin::SearchResult result = m_searchResults.at(index.row()); + if(role == Qt::UserRole) { + return result.iconTitle; + } + if(role == Qt::UserRole+1) { + return result.iconSource; + } + if(role == Qt::UserRole+2) { + return result.category; + } + if(role == Qt::UserRole+3) { + return result.extraCaption; + } + if(role == Qt::UserRole+4) { + return result.action; + } + return QVariant(); } void SearchModel::search(QString searchString) { - qDebug() << Q_FUNC_INFO << searchString; + m_manager->search(searchString); +} + +void SearchModel::searchResultHandler(QList results) +{ + beginResetModel(); + m_searchResults.clear(); + m_searchResults = results; + endResetModel(); + emit countChanged(); +} + +int SearchModel::count() const +{ + return m_searchResults.count(); } diff --git a/src/models/searchmodel.h b/src/models/searchmodel.h index 755f059..0c66ac9 100644 --- a/src/models/searchmodel.h +++ b/src/models/searchmodel.h @@ -27,6 +27,8 @@ class SearchModel : public QAbstractListModel { Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged FINAL) + public: explicit SearchModel(QObject* parent = nullptr); @@ -35,10 +37,18 @@ class SearchModel : public QAbstractListModel { QHash roleNames() const { return m_hash; } Q_INVOKABLE void search(QString searchString); + int count() const; + +signals: + void countChanged(); + +private slots: + void searchResultHandler(QList results); private: QHash m_hash; SearchPluginManager* m_manager; + QList m_searchResults; }; #endif // SEARCHMODEL_H diff --git a/src/qml/AppLauncher.qml b/src/qml/AppLauncher.qml index 137a39f..14961e1 100644 --- a/src/qml/AppLauncher.qml +++ b/src/qml/AppLauncher.qml @@ -41,7 +41,7 @@ Flickable{ width: parent.width height: desktop.height property var switcher: null - property string searchString + property alias searchString: searchListView.searchString ConfigurationValue { id: alwaysShowSearch diff --git a/src/qml/applauncher/SearchListView.qml b/src/qml/applauncher/SearchListView.qml index 75b9393..55a36bd 100644 --- a/src/qml/applauncher/SearchListView.qml +++ b/src/qml/applauncher/SearchListView.qml @@ -40,6 +40,7 @@ Item { height: calculateHeight() anchors.bottomMargin:Theme.itemSpacingHuge property alias searchField: searchField + property alias searchString: searchField.text property int oldHeight GlacierSearchModel{ @@ -171,36 +172,26 @@ Item { } ListView { - id:listView + id: searchResultListView clip: true width: parent.width height:contentHeight anchors{ top: searchRow.bottom - topMargin: listModel.count > 0 ? Theme.itemSpacingSmall : 0 + topMargin: searchModel.count > 0 ? Theme.itemSpacingSmall : 0 } - visible: searchString.length>0 + visible: searchModel.count > 0 section.property: 'category' section.delegate: Component{ id: sectionHeading Rectangle { - width: listView.width + width: searchResultListView.width height: Theme.itemHeightMedium color: "transparent" Text { id: sectionText - text: { - switch (section) { - case 'Application': - return qsTr("Application") - case 'Contact': - return qsTr("Contact") - default: - return qsTr("Content") - } - } - + text: section font.capitalization: Font.AllUppercase font.pixelSize: Theme.fontSizeSmall color: Theme.textColor @@ -215,7 +206,7 @@ Item { id: line height: 1 color: Theme.textColor - width: listView.width-sectionText.width-Theme.itemHeightExtraSmall + width: searchResultListView.width-sectionText.width-Theme.itemHeightExtraSmall anchors{ left: sectionText.right leftMargin: Theme.itemSpacingSmall @@ -231,130 +222,15 @@ Item { Connections { target: appLauncher - function onSearchStringChanged() { listView.update() } + function onSearchStringChanged() { searchResultListView.update() } } - model: ListModel { - id: listModel - } - - //Orginal function ** Copyright (C) 2013 Jolla Ltd. ** Contact: Joona Petrell **BSD - //Function has been modified - function update() { - if(searchString.length<1) { - listModel.clear() - } else { - var iconTitle - var category - var extraCaption - var iconId - var found - var i - - var titles = [] - var contacts = [] - for (i = 0; i < searchLauncherModel.itemCount; ++i) { - if (searchLauncherModel.get(i).type === LauncherModel.Folder) { - for(var j = 0; j< searchLauncherModel.get(i).itemCount; ++j ) { - titles.push({ - 'iconTitle':searchLauncherModel.get(i).get(j).title, - 'iconSource':searchLauncherModel.get(i).get(j).iconId, - 'id':i, - 'folderId':j, - 'category':qsTr("Application"), - 'extraCaption': qsTr("installed on your device") - }) - } - } else { - titles.push({ - 'iconTitle':searchLauncherModel.get(i).title, - 'iconSource':searchLauncherModel.get(i).iconId, - 'id':i, - 'folderId':-1, - 'category':qsTr("Application"), - 'extraCaption': qsTr("installed on your device") - }) - } - } - for (i = 0; i < peopleModel.count; ++i) { - if(peopleModel.get(i).firstName && peopleModel.get(i).lastName) { - contacts.push({ - 'title':(peopleModel.get(i).firstName + " " + peopleModel.get(i).lastName), - 'iconSource':peopleModel.get(i).avatarUrl.toString(), - 'extraCaption':peopleModel.get(i).phoneNumbers, - 'category':qsTr("Contact") - }) - } - } - var filteredTitles = titles.filter(function (icon) { - return icon.iconTitle.toLowerCase().indexOf(searchString) !== -1 - }) - // helper objects that can be quickly accessed - var filteredTitleObject = new Object - for (i = 0; i < filteredTitles.length; ++i) { - filteredTitleObject[filteredTitles[i].iconTitle] = true - } - var existingTitleObject = new Object - for (i = 0; i < count; ++i) { - iconTitle = listModel.get(i).title - existingTitleObject[iconTitle] = true - } - - // remove items no longer in filtered set - i = 0 - while (i < count) { - iconTitle = listModel.get(i).title - found = filteredTitleObject.hasOwnProperty(iconTitle) - if (!found) { - listModel.remove(i) - } else { - i++ - } - } - // add new items - for (i = 0; i < filteredTitles.length; ++i) { - iconTitle = filteredTitles[i].iconTitle - iconId = filteredTitles[i].iconSource - var id = filteredTitles[i].id - var folderId = filteredTitles[i].folderId - category = filteredTitles[i].category - found = existingTitleObject.hasOwnProperty(iconTitle) - if (!found) { - // for simplicity, just adding to end instead of corresponding position in original list - listModel.append({'title':iconTitle, 'iconSource':iconId, 'id':id, 'folderId':folderId, 'category':category, 'extraCaption': ""}) - } - } - for (i = 0; i < contacts.length; ++i) { - iconTitle = contacts[i].title - iconId = contacts[i].iconSource - extraCaption = contacts[i].extraCaption[0] - category = contacts[i].category - listModel.append({'title':iconTitle, 'iconSource':iconId, 'extraCaption':extraCaption, 'category':category}) - } - } - } + model: searchModel delegate: Item { - width: listView.width + width: searchResultListView.width height:Theme.itemHeightExtraLarge*1.2 - property string iconCaption: model.title - property string iconSource: { - if(model.iconSource) { - if (model.iconSource.indexOf("file:///") == 0) { - return model.iconSource - } else { - if( model.iconSource.indexOf("/") == 0) { - return "file://" + model.iconSource - } else { - return "image://theme/" + model.iconSource - } - } - } else { - return "/usr/share/glacier-home/qml/theme/default-icon.png" - } - } - Rectangle { anchors.fill: parent color: "#11ffffff" @@ -381,16 +257,8 @@ Item { } width: height height: parent.height - Theme.itemSpacingHuge - enabled: { - if(searchLauncherModel.get(model.id).type === LauncherModel.Application) { - if(searchLauncherModel.get(model.id).isLaunching) - return switcher.switchModel.getWindowIdForTitle(model.title) == 0 - } else if (searchLauncherModel.get(model.id).type === LauncherModel.Folder && model.folderId > -1) { - if (searchLauncherModel.get(model.id).get(model.folderId).isLaunching) - return switcher.switchModel.getWindowIdForTitle(model.title) == 0 - } - return false - } + enabled: false + Connections { target: Lipstick.compositor function onWindowAdded(window) { @@ -412,7 +280,7 @@ Item { height: labelWrapper.childrenRect.height Label { id:mainLabel - text:iconCaption + text:iconTitle anchors { left: parent.left right: parent.right @@ -438,30 +306,7 @@ Item { MouseArea { id:mouse anchors.fill: parent - onClicked: { - switch (category ) { - case "Application": - var winId - if (searchLauncherModel.get(model.id).type !== LauncherModel.Folder) { - winId = switcher.switchModel.getWindowIdForTitle(model.title) - if (winId == 0 && !searchLauncherModel.get(model.id).isLaunching) - searchLauncherModel.get(model.id).launchApplication() - else - Lipstick.compositor.windowToFront(winId) - } else if (searchLauncherModel.get(model.id).type === LauncherModel.Folder && model.folderId > -1) { - winId = switcher.switchModel.getWindowIdForTitle(model.title) - if (winId == 0 && !searchLauncherModel.get(model.id).get(model.folderId).isLaunching) - searchLauncherModel.get(model.id).get(model.folderId).launchApplication() - else - Lipstick.compositor.windowToFront(winId) - } - - break - case "Contact": - console.log("Call to person. Or open contextmenu where sms and call") - break - } - } + onClicked: console.log("Call to item") } } }