diff --git a/app-search/app-search.c b/app-search/app-search.c new file mode 100644 index 0000000000000000000000000000000000000000..f26140def9ba206cbf79b2cbee7ab8b760db1449 --- /dev/null +++ b/app-search/app-search.c @@ -0,0 +1,367 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-app-search" + +#include +#include + +#include "gnome-shell-search-provider.h" + + +#define PHOSH_TYPE_APP_SEARCH_APPLICATION (phosh_app_search_application_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshAppSearchApplication, phosh_app_search_application, PHOSH, APP_SEARCH_APPLICATION, GApplication) + + +struct _PhoshAppSearchApplication { + GApplication parent; + + PhoshDBusSearchProvider2 *provider; +}; + + +G_DEFINE_TYPE (PhoshAppSearchApplication, phosh_app_search_application, G_TYPE_APPLICATION) + + +static void +phosh_app_search_application_dispose (GObject *object) +{ + PhoshAppSearchApplication *self = PHOSH_APP_SEARCH_APPLICATION (object); + + g_clear_object (&self->provider); + + G_OBJECT_CLASS (phosh_app_search_application_parent_class)->dispose (object); +} + + +static void +phosh_app_search_application_activate (GApplication *application) +{ + /* GApplication complains if we don't provide an activate */ +} + + +static gboolean +phosh_app_search_application_dbus_register (GApplication *application, + GDBusConnection *connection, + const gchar *object_path, + GError **error) +{ + PhoshAppSearchApplication *self = PHOSH_APP_SEARCH_APPLICATION (application); + + if (!G_APPLICATION_CLASS (phosh_app_search_application_parent_class)->dbus_register (application, + connection, + object_path, + error)) { + return FALSE; + } + + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->provider), + connection, + object_path, + error)) { + g_critical ("Can’t export search provider: %s", (*error)->message); + } + + return TRUE; +} + + +static void +phosh_app_search_application_dbus_unregister (GApplication *application, + GDBusConnection *connection, + const gchar *object_path) +{ + PhoshAppSearchApplication *self = PHOSH_APP_SEARCH_APPLICATION (application); + + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->provider)); + + G_APPLICATION_CLASS (phosh_app_search_application_parent_class)->dbus_unregister (application, + connection, + object_path); +} + + +static void +phosh_app_search_application_class_init (PhoshAppSearchApplicationClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GApplicationClass *app_class = G_APPLICATION_CLASS (klass); + + object_class->dispose = phosh_app_search_application_dispose; + + app_class->activate = phosh_app_search_application_activate; + app_class->dbus_register = phosh_app_search_application_dbus_register; + app_class->dbus_unregister = phosh_app_search_application_dbus_unregister; +} + + +static GStrv +do_search (GStrv terms) +{ + GPtrArray *strv_builder = g_ptr_array_new_full (10, g_free); + g_autofree char *search_string = NULL; + g_autofree char ***search_results = NULL; + + search_string = g_strjoinv (" ", (GStrv) terms); + search_results = g_desktop_app_info_search (search_string); + + for (int i = 0; search_results[i]; i++) { + for (int j = 0; search_results[i][j]; j++) { + g_autoptr (GDesktopAppInfo) info = NULL; + + info = g_desktop_app_info_new (search_results[i][j]); + + if (G_UNLIKELY (info == NULL)) { + g_warning ("Unknown desktop-id: “%s”", search_results[i][j]); + + continue; + } + + if (G_LIKELY (g_app_info_should_show (G_APP_INFO (info)))) { + g_ptr_array_add (strv_builder, + g_strdup (g_app_info_get_id (G_APP_INFO (info)))); + } + } + } + + g_ptr_array_add (strv_builder, NULL); + + for (int i = 0; search_results[i]; i++) { + g_strfreev (search_results[i]); + } + + return (GStrv) g_ptr_array_free (strv_builder, FALSE); +} + + +static gboolean +get_initial_result_set (PhoshDBusSearchProvider2 *interface, + GDBusMethodInvocation *invocation, + const char *const *terms, + PhoshAppSearchApplication *self) +{ + g_auto (GStrv) results = NULL; + + g_application_hold (G_APPLICATION (self)); + + results = do_search ((GStrv) terms); + + phosh_dbus_search_provider2_complete_get_initial_result_set (interface, + invocation, + (const char *const *) results); + + g_application_release (G_APPLICATION (self)); + + return TRUE; +} + + +static gboolean +get_subsearch_result_set (PhoshDBusSearchProvider2 *interface, + GDBusMethodInvocation *invocation, + const char *const *previous_results, + const char *const *terms, + PhoshAppSearchApplication *self) +{ + g_auto (GStrv) results = NULL; + + g_application_hold (G_APPLICATION (self)); + + results = do_search ((GStrv) terms); + + phosh_dbus_search_provider2_complete_get_subsearch_result_set (interface, + invocation, + (const char *const *) results); + + g_application_release (G_APPLICATION (self)); + + return TRUE; +} + + +static gboolean +get_result_metas (PhoshDBusSearchProvider2 *interface, + GDBusMethodInvocation *invocation, + const char *const *results, + PhoshAppSearchApplication *self) +{ + GVariantBuilder builder; + + g_application_hold (G_APPLICATION (self)); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}")); + + for (int i = 0; results[i]; i++) { + g_autoptr (GDesktopAppInfo) info = NULL; + g_autofree char *title = NULL; + g_autofree char *description = NULL; + GIcon *icon = NULL; + + info = g_desktop_app_info_new (results[i]); + + if (G_UNLIKELY (info == NULL)) { + g_warning ("Unknown desktop-id: “%s”", results[i]); + + continue; + } + + g_variant_builder_open (&builder, G_VARIANT_TYPE ("a{sv}")); + + g_variant_builder_add (&builder, + "{sv}", + "id", + g_variant_new_string (results[i])); + + title = g_markup_escape_text (g_app_info_get_name (G_APP_INFO (info)), -1); + g_variant_builder_add (&builder, + "{sv}", + "name", + g_variant_new_string (title)); + + description = g_markup_escape_text (g_app_info_get_description (G_APP_INFO (info)), -1); + g_variant_builder_add (&builder, + "{sv}", + "description", + g_variant_new_string (description)); + + icon = g_app_info_get_icon (G_APP_INFO (info)); + g_variant_builder_add (&builder, "{sv}", "icon", g_icon_serialize (icon)); + + g_variant_builder_close (&builder); + } + + phosh_dbus_search_provider2_complete_get_result_metas (interface, + invocation, + g_variant_builder_end (&builder)); + + g_application_release (G_APPLICATION (self)); + + return TRUE; +} + + +static gboolean +activate_result (PhoshDBusSearchProvider2 *interface, + GDBusMethodInvocation *invocation, + const char *id, + const char *const *terms, + guint timestamp, + PhoshAppSearchApplication *self) +{ + g_autoptr (GDesktopAppInfo) info = NULL; + g_autoptr (GError) error = NULL; + + g_application_hold (G_APPLICATION (self)); + + info = g_desktop_app_info_new (id); + + if (!info) { + g_warning ("Unknown desktop-id: “%s”", id); + + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Unknown desktop-id: “%s”", + id); + + g_application_release (G_APPLICATION (self)); + + return TRUE; + } + + g_app_info_launch (G_APP_INFO (info), NULL, NULL, &error); + + if (error) { + g_warning ("Failed to launch app “%s”: %s", + id, + error->message); + + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Failed to launch app “%s”: %s", + id, + error->message); + + g_application_release (G_APPLICATION (self)); + + return TRUE; + } + + phosh_dbus_search_provider2_complete_activate_result (interface, invocation); + + g_application_release (G_APPLICATION (self)); + + return TRUE; +} + + +static gboolean +launch_search (PhoshDBusSearchProvider2 *interface, + GDBusMethodInvocation *invocation, + const char *const *terms, + guint timestamp, + PhoshAppSearchApplication *self) +{ + g_application_hold (G_APPLICATION (self)); + + phosh_dbus_search_provider2_complete_launch_search (interface, invocation); + + g_application_release (G_APPLICATION (self)); + + return TRUE; +} + + +static void +phosh_app_search_application_init (PhoshAppSearchApplication *self) +{ + self->provider = phosh_dbus_search_provider2_skeleton_new (); + + g_signal_connect (self->provider, + "handle-get-initial-result-set", + G_CALLBACK (get_initial_result_set), + self); + g_signal_connect (self->provider, + "handle-get-subsearch-result-set", + G_CALLBACK (get_subsearch_result_set), + self); + g_signal_connect (self->provider, + "handle-get-result-metas", + G_CALLBACK (get_result_metas), + self); + g_signal_connect (self->provider, + "handle-activate-result", + G_CALLBACK (activate_result), + self); + g_signal_connect (self->provider, + "handle-launch-search", + G_CALLBACK (launch_search), + self); +} + + +#ifdef TESTING_APP_SEARCH +static int +real_main (int argc, char **argv) +#else +int +main (int argc, char **argv) +#endif +{ + g_autoptr (GApplication) app = NULL; + + app = g_object_new (PHOSH_TYPE_APP_SEARCH_APPLICATION, + "application-id", "sm.puri.Phosh.AppSearch", + "flags", G_APPLICATION_IS_SERVICE, + NULL); + + return g_application_run (app, argc, argv); +} diff --git a/app-search/meson.build b/app-search/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..90d7cf1963be93dd2727b5c0db7468a34d95a9dd --- /dev/null +++ b/app-search/meson.build @@ -0,0 +1,5 @@ +executable('phosh-app-search', ['app-search.c'], + include_directories: [root_inc], + dependencies: phoshsearch_dep, + install_dir: libexecdir, + install: true) diff --git a/data/meson.build b/data/meson.build index 5a002d48224246ebdfa1054a97f4e186487403c5..2a0fc3ae8fb26c16f19c90d59bd04e0748b97125 100644 --- a/data/meson.build +++ b/data/meson.build @@ -143,3 +143,30 @@ install_data('wayland-sessions/phosh.desktop', install_data('phosh.portal', install_dir : join_paths(datadir, 'xdg-desktop-portal', 'portals')) install_data('phosh-portals.conf', install_dir : join_paths(datadir, 'xdg-desktop-portal')) + +# Search +desktop_file = configure_file(input: 'sm.puri.Phosh.AppSearch.desktop.in', + output: 'sm.puri.Phosh.AppSearch.desktop', + configuration: desktopconf, + install_dir: desktopdir) + +desktop_utils = find_program('desktop-file-validate', required: false) +if desktop_utils.found() and get_option('tests') + test('Validate desktop file', desktop_utils, + args: [desktop_file] + ) +endif + +servicedir = join_paths([datadir, 'dbus-1', 'services']) + +configure_file( + input: 'sm.puri.Phosh.Search.service.in', + output: 'sm.puri.Phosh.Search.service', + configuration: desktopconf, + install_dir: servicedir) + +configure_file( + input: 'sm.puri.Phosh.AppSearch.service.in', + output: 'sm.puri.Phosh.AppSearch.service', + configuration: desktopconf, + install_dir: servicedir) diff --git a/data/sm.puri.Phosh.AppSearch.desktop.in b/data/sm.puri.Phosh.AppSearch.desktop.in new file mode 100644 index 0000000000000000000000000000000000000000..65cb005a00940ea7a05ba66debc18e8484c80ab4 --- /dev/null +++ b/data/sm.puri.Phosh.AppSearch.desktop.in @@ -0,0 +1,7 @@ +[Desktop Entry] +Name=App Search +Comment=App Search Provider +Exec=@libexecdir@/phosh-app-search +NoDisplay=true +Type=Application +DBusActivatable=true diff --git a/data/sm.puri.Phosh.AppSearch.service.in b/data/sm.puri.Phosh.AppSearch.service.in new file mode 100644 index 0000000000000000000000000000000000000000..9b1b68d211a9810fe52207417c0885a798d9f0db --- /dev/null +++ b/data/sm.puri.Phosh.AppSearch.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=sm.puri.Phosh.AppSearch +Exec=@libexecdir@/phosh-app-search diff --git a/data/sm.puri.Phosh.Search.service.in b/data/sm.puri.Phosh.Search.service.in new file mode 100644 index 0000000000000000000000000000000000000000..078aca67b1b536ae1cdee648bae1a47071b8d5c0 --- /dev/null +++ b/data/sm.puri.Phosh.Search.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=sm.puri.Phosh.Search +Exec=@libexecdir@/phosh-searchd --gapplication-service diff --git a/meson.build b/meson.build index 0c7818aa38f2c8e0c88d294a1714e1a7099ea9ad..b83a5ecae49cb725555edf90e07f2b0de2366180 100644 --- a/meson.build +++ b/meson.build @@ -16,6 +16,7 @@ desktopdir = join_paths(datadir, 'applications') sessiondir = join_paths(datadir, 'gnome-session') pkgdatadir = join_paths(datadir, meson.project_name()) pkglibdir = join_paths(libdir, meson.project_name()) +servicedir = join_paths([datadir, 'dbus-1', 'services']) systemddir = join_paths(prefix, 'lib/systemd') systemduserdir = join_paths(systemddir, 'user') plugins_dir = join_paths(prefix, libdir, 'phosh', 'plugins') @@ -237,6 +238,8 @@ subdir('po') subdir('protocol') subdir('src') subdir('plugins') +subdir('app-search') +subdir('searchd') subdir('tests') subdir('tools') subdir('docs') diff --git a/searchd/meson.build b/searchd/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..978fb5efdf0fa02c3e058494e57fc2f52c72c747 --- /dev/null +++ b/searchd/meson.build @@ -0,0 +1,11 @@ +searchd_args = [ + '-DG_LOG_USE_STRUCTURED', + '-DG_LOG_DOMAIN="sm.puri.Phosh.Search"' +] + +executable('phosh-searchd', ['searchd.c', 'search-provider.c'], + include_directories: [root_inc], + dependencies: phoshsearch_dep, + c_args: searchd_args, + install_dir: libexecdir, + install: true) diff --git a/searchd/search-provider.c b/searchd/search-provider.c new file mode 100644 index 0000000000000000000000000000000000000000..6118846518f031f659c249d8b1a75014cb22266b --- /dev/null +++ b/searchd/search-provider.c @@ -0,0 +1,663 @@ +/* + * Copyright © 2019 Zander Brown + * + * Based on search.js / remoteSearch.js: + * https://gitlab.gnome.org/GNOME/gnome-shell/blob/2d2824b947754abf0ddadd9c1ba9b9f16b0745d3/js/ui/search.js + * https://gitlab.gnome.org/GNOME/gnome-shell/blob/0a7e717e0e125248bace65e170a95ae12e3cdf38/js/ui/remoteSearch.js + * + * SPDX-License-Identifier: GPL-3.0+ + * + * Author: Zander Brown + */ + +#include + +#include "search-provider.h" +#include "search-result-meta.h" +#include "gnome-shell-search-provider.h" + + +typedef struct _PhoshSearchProviderPrivate PhoshSearchProviderPrivate; +struct _PhoshSearchProviderPrivate { + GAppInfo *info; + PhoshDBusSearchProvider2 *proxy; + GCancellable *cancellable; + GCancellable *parent_cancellable; + gulong parent_cancellable_handler; + GDBusProxyFlags proxy_flags; + char *bus_name; + char *bus_path; + gboolean autostart; + gboolean default_disabled; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshSearchProvider, phosh_search_provider, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_APP_INFO, + PROP_BUS_NAME, + PROP_BUS_PATH, + PROP_AUTOSTART, + PROP_DEFAULT_DISABLED, + PROP_PARENT_CANCELLABLE, + LAST_PROP +}; +static GParamSpec *pspecs[LAST_PROP] = { NULL, }; + +enum { + READY, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +static void +got_proxy (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr (PhoshSearchProvider) self = PHOSH_SEARCH_PROVIDER (user_data); + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + g_autoptr (GError) error = NULL; + + priv->proxy = phosh_dbus_search_provider2_proxy_new_for_bus_finish (res, &error); + + if (error) { + g_warning ("[%s]: Unable to create proxy: %s", + priv->bus_path, + error->message); + return; + } + + g_debug ("[%s]: Got proxy", priv->bus_path); + + g_signal_emit (self, signals[READY], 0); +} + +static void +phosh_search_provider_constructed (GObject *object) +{ + PhoshSearchProvider *self = PHOSH_SEARCH_PROVIDER (object); + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + G_OBJECT_CLASS (phosh_search_provider_parent_class)->constructed (object); + + phosh_dbus_search_provider2_proxy_new_for_bus (G_BUS_TYPE_SESSION, + priv->proxy_flags, + priv->bus_name, + priv->bus_path, + priv->cancellable, + got_proxy, + g_object_ref (self)); +} + +static void +phosh_search_provider_finalize (GObject *object) +{ + PhoshSearchProvider *self = PHOSH_SEARCH_PROVIDER (object); + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + g_cancellable_cancel (priv->cancellable); + g_clear_object (&priv->cancellable); + + g_cancellable_disconnect (priv->parent_cancellable, + priv->parent_cancellable_handler); + + g_clear_object (&priv->info); + g_clear_object (&priv->proxy); + g_clear_object (&priv->parent_cancellable); + + g_clear_pointer (&priv->bus_name, g_free); + g_clear_pointer (&priv->bus_path, g_free); + + G_OBJECT_CLASS (phosh_search_provider_parent_class)->finalize (object); +} + +static void +parent_canceled (GCancellable *cancellable, + PhoshSearchProvider *self) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + g_debug ("Provider %s cancelling", priv->bus_name); + + g_cancellable_cancel (priv->cancellable); + g_clear_object (&priv->cancellable); + priv->cancellable = g_cancellable_new (); +} + +static void +phosh_search_provider_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshSearchProvider *self = PHOSH_SEARCH_PROVIDER (object); + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + switch (property_id) { + case PROP_APP_INFO: + priv->info = g_value_dup_object (value); + break; + case PROP_PARENT_CANCELLABLE: + priv->parent_cancellable = g_value_dup_object (value); + if (priv->parent_cancellable) { + priv->parent_cancellable_handler = g_cancellable_connect (priv->parent_cancellable, + G_CALLBACK (parent_canceled), + self, + NULL); + } + break; + case PROP_BUS_NAME: + priv->bus_name = g_value_dup_string (value); + break; + case PROP_BUS_PATH: + priv->bus_path = g_value_dup_string (value); + break; + case PROP_AUTOSTART: + priv->autostart = g_value_get_boolean (value); + if (priv->autostart) { + // Delay autostart to avoid spawing everything at once + priv->proxy_flags |= G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START_AT_CONSTRUCTION; + } else { + // Don't attempt to autostart + priv->proxy_flags |= G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START; + } + break; + case PROP_DEFAULT_DISABLED: + priv->default_disabled = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +phosh_search_provider_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshSearchProvider *self = PHOSH_SEARCH_PROVIDER (object); + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + switch (property_id) { + case PROP_APP_INFO: + g_value_set_object (value, priv->info); + break; + case PROP_PARENT_CANCELLABLE: + g_value_set_object (value, priv->parent_cancellable); + break; + case PROP_BUS_NAME: + g_value_set_string (value, priv->bus_name); + break; + case PROP_BUS_PATH: + g_value_set_string (value, priv->bus_path); + break; + case PROP_AUTOSTART: + g_value_set_boolean (value, priv->autostart); + break; + case PROP_DEFAULT_DISABLED: + g_value_set_boolean (value, priv->default_disabled); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +phosh_search_provider_class_init (PhoshSearchProviderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_search_provider_constructed; + object_class->finalize = phosh_search_provider_finalize; + object_class->set_property = phosh_search_provider_set_property; + object_class->get_property = phosh_search_provider_get_property; + + pspecs[PROP_APP_INFO] = + g_param_spec_object ("app-info", "", "", + G_TYPE_APP_INFO, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + pspecs[PROP_PARENT_CANCELLABLE] = + g_param_spec_object ("parent-cancellable", "", "", + G_TYPE_CANCELLABLE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + pspecs[PROP_BUS_NAME] = + g_param_spec_string ("bus-name", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + pspecs[PROP_BUS_PATH] = + g_param_spec_string ("bus-path", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + pspecs[PROP_AUTOSTART] = + g_param_spec_boolean ("autostart", "", "", + TRUE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + pspecs[PROP_DEFAULT_DISABLED] = + g_param_spec_boolean ("default-disabled", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, LAST_PROP, pspecs); + + signals[READY] = g_signal_new ("ready", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +phosh_search_provider_init (PhoshSearchProvider *self) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + priv->cancellable = g_cancellable_new (); + priv->proxy_flags = G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES; + priv->autostart = TRUE; +} + +PhoshSearchProvider * +phosh_search_provider_new (const char *desktop_app_id, + GCancellable *parent_cancellable, + const char *bus_path, + const char *bus_name, + gboolean autostart, + gboolean default_disabled) +{ + return g_object_new (PHOSH_TYPE_SEARCH_PROVIDER, + "app-info", g_desktop_app_info_new (desktop_app_id), + "parent-cancellable", parent_cancellable, + "bus-path", bus_path, + "bus-name", bus_name, + "autostart", autostart, + "default-disabled", default_disabled, + NULL); +} + +GPtrArray * +phosh_search_provider_limit_results (GStrv results, + int max) +{ + g_autoptr (GPtrArray) normal = NULL; + g_autoptr (GPtrArray) special = NULL; + GPtrArray *array; + int i = 0; + + g_return_val_if_fail (results != NULL, NULL); + + normal = g_ptr_array_new_full (max * 1.5, g_free); + special = g_ptr_array_new_full (max * 1.5, g_free); + array = g_ptr_array_new_full (max * 1.5, g_free); + + while (results[i]) { + if (G_UNLIKELY (g_str_has_prefix (results[i], "special:"))) { + g_ptr_array_add (special, g_strdup (results[i])); + } else { + g_ptr_array_add (normal, g_strdup (results[i])); + } + + if (special->len == max && normal->len == max) { + break; + } + + i++; + } + + for (i = 0; i < max &&i < normal->len; i++) + g_ptr_array_add (array, g_ptr_array_steal_index (normal, i)); + + for (i = 0; i < max &&i < special->len; i++) + g_ptr_array_add (array, g_ptr_array_steal_index (special, i)); + + return array; +} + +static void +result_activated (GObject *source, + GAsyncResult *res, + gpointer data) +{ + g_autoptr (GError) error = NULL; + + phosh_dbus_search_provider2_call_activate_result_finish (PHOSH_DBUS_SEARCH_PROVIDER2 (source), + res, + &error); + + if (error) + g_warning ("Failed to activate result: %s", error->message); +} + +void +phosh_search_provider_activate_result (PhoshSearchProvider *self, + const char *result, + const char *const *terms, + guint timestamp) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + phosh_dbus_search_provider2_call_activate_result (PHOSH_DBUS_SEARCH_PROVIDER2 (priv->proxy), + result, + terms, + timestamp, + priv->cancellable, + result_activated, + NULL); +} + +static void +search_launched (GObject *source, + GAsyncResult *res, + gpointer data) +{ + g_autoptr (GError) error = NULL; + + phosh_dbus_search_provider2_call_launch_search_finish (PHOSH_DBUS_SEARCH_PROVIDER2 (source), + res, + &error); + + if (error) { + g_warning ("Failed to launch search: %s", error->message); + } +} + +void +phosh_search_provider_launch (PhoshSearchProvider *self, + const char *const *terms, + guint timestamp) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + phosh_dbus_search_provider2_call_launch_search (PHOSH_DBUS_SEARCH_PROVIDER2 (priv->proxy), + terms, + timestamp, + priv->cancellable, + search_launched, + NULL); +} + +static GIcon * +get_result_icon (PhoshSearchProvider *self, + GVariantDict *result_meta) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + g_autoptr (GError) error = NULL; + g_autoptr (GVariant) variant = NULL; + g_autofree char *icon_data = NULL; + GIcon *icon = NULL; + int width = 0; + int height = 0; + int row_stride = 0; + int has_alpha = 0; + int sample_size = 0; + int channels = 0; + + if (g_variant_dict_lookup (result_meta, "icon", "v", &variant)) { + icon = g_icon_deserialize (variant); + } else if (g_variant_dict_lookup (result_meta, "gicon", "s", &icon_data)) { + icon = g_icon_new_for_string (icon_data, &error); + + if (error) { + g_warning ("[%s]: bad icon: %s", priv->bus_path, error->message); + } + } else if (g_variant_dict_lookup (result_meta, "icon-data", "iiibiiay", + &width, &height, &row_stride, &has_alpha, + &sample_size, &channels, &icon_data)) { + icon = G_ICON (gdk_pixbuf_new_from_data ((guchar *) icon_data, + GDK_COLORSPACE_RGB, + has_alpha, + sample_size, + width, + height, + row_stride, + (GdkPixbufDestroyNotify) g_free, + NULL)); + } + + return icon; +} + +struct GetMetaData { + PhoshSearchProvider *self; + GTask *task; +}; + +static void +got_result_meta (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + g_autoptr (GVariant) metas = NULL; + g_autoptr (GPtrArray) results = NULL; + struct GetMetaData *data = user_data; + GVariant *val = NULL; + GVariantIter iter; + g_autofree char *bus_path = NULL; + + g_object_get (data->self, "bus-path", &bus_path, NULL); + + phosh_dbus_search_provider2_call_get_result_metas_finish (PHOSH_DBUS_SEARCH_PROVIDER2 (source), + &metas, + res, + &error); + + if (error) { + g_warning ("[%s]: Failed get result meta: %s", bus_path, error->message); + } + + results = g_ptr_array_new_full (100, (GDestroyNotify) phosh_search_result_meta_unref); + + // Some providers decide to provide NULL instead of an empty array + // thus we do what JS does and map NULL to an empty array + if (metas == NULL) { + metas = g_variant_new ("aa{sv}", NULL); + } + + g_variant_iter_init (&iter, metas); + while (g_variant_iter_loop (&iter, "@a{sv}", &val)) { + g_auto (GVariantDict) dict; + g_autofree char *id = NULL; + g_autofree char *name = NULL; + g_autofree char *desc = NULL; + g_autofree char *clipboard = NULL; + g_autoptr (GIcon) icon = NULL; + PhoshSearchResultMeta *meta; + + g_variant_dict_init (&dict, val); + + if (!g_variant_dict_lookup (&dict, "id", "s", &id)) { + g_warning ("Result is missing a result id"); + continue; + } + + if (!g_variant_dict_lookup (&dict, "name", "s", &name)) { + g_warning ("Result %s is missing a name!", id); + continue; + } + + g_variant_dict_lookup (&dict, "description", "s", &desc); + + // Isn't consistent naming great? + g_variant_dict_lookup (&dict, "clipboardText", "s", &clipboard); + + icon = get_result_icon (data->self, &dict); + + meta = phosh_search_result_meta_new (id, name, desc, icon, clipboard); + + g_ptr_array_add (results, meta); + } + + g_task_return_pointer (G_TASK (data->task), + g_ptr_array_ref (results), + (GDestroyNotify) g_ptr_array_unref); + + g_object_unref (data->self); + g_free (data); +} + +void +phosh_search_provider_get_result_meta (PhoshSearchProvider *self, + GStrv results, + GAsyncReadyCallback callback, + gpointer callback_data) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + struct GetMetaData *data; + GTask *task; + + task = g_task_new (self, priv->cancellable, callback, callback_data); + + data = g_new (struct GetMetaData, 1); + data->self = g_object_ref (self); + data->task = task; + + phosh_dbus_search_provider2_call_get_result_metas (PHOSH_DBUS_SEARCH_PROVIDER2 (priv->proxy), + (const gchar * const*) results, + priv->cancellable, + got_result_meta, + data); +} + +GPtrArray * +phosh_search_provider_get_result_meta_finish (PhoshSearchProvider *self, + GAsyncResult *res, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (res, self), NULL); + + return g_task_propagate_pointer (G_TASK (res), error); +} + + +struct GetResultsData { + gboolean inital; + GTask *task; + PhoshSearchProvider *self; +}; + + +static void +got_results (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + struct GetResultsData *data = user_data; + GStrv results = NULL; + + if (data->inital) { + phosh_dbus_search_provider2_call_get_initial_result_set_finish (PHOSH_DBUS_SEARCH_PROVIDER2 (source), + &results, + res, + &error); + } else { + phosh_dbus_search_provider2_call_get_subsearch_result_set_finish (PHOSH_DBUS_SEARCH_PROVIDER2 (source), + &results, + res, + &error); + } + + if (error) { + g_task_return_error (data->task, g_steal_pointer (&error)); + } else { + g_task_return_pointer (G_TASK (data->task), + results, + (GDestroyNotify) g_strfreev); + } + + g_object_unref (data->self); + g_free (data); +} + +void +phosh_search_provider_get_initial (PhoshSearchProvider *self, + const char *const *terms, + GAsyncReadyCallback callback, + gpointer callback_data) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + struct GetResultsData *data; + GTask *task; + + task = g_task_new (self, priv->cancellable, callback, callback_data); + + data = g_new (struct GetResultsData, 1); + data->inital = TRUE; + data->task = task; + data->self = g_object_ref (self); + + phosh_dbus_search_provider2_call_get_initial_result_set (PHOSH_DBUS_SEARCH_PROVIDER2 (priv->proxy), + terms, + priv->cancellable, + got_results, + data); +} + +GStrv +phosh_search_provider_get_initial_finish (PhoshSearchProvider *self, + GAsyncResult *res, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (res, self), NULL); + + return g_task_propagate_pointer (G_TASK (res), error); +} + +void +phosh_search_provider_get_subsearch (PhoshSearchProvider *self, + const char *const *results, + const char *const *terms, + GAsyncReadyCallback callback, + gpointer callback_data) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + struct GetResultsData *data; + GTask *task; + + task = g_task_new (self, priv->cancellable, callback, callback_data); + + data = g_new (struct GetResultsData, 1); + data->inital = FALSE; + data->task = task; + data->self = g_object_ref (self); + + phosh_dbus_search_provider2_call_get_subsearch_result_set (PHOSH_DBUS_SEARCH_PROVIDER2 (priv->proxy), + results, + terms, + priv->cancellable, + got_results, + data); +} + +GStrv +phosh_search_provider_get_subsearch_finish (PhoshSearchProvider *self, + GAsyncResult *res, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (res, self), NULL); + + return g_task_propagate_pointer (G_TASK (res), error); +} + + +gboolean +phosh_search_provider_get_ready (PhoshSearchProvider *self) +{ + PhoshSearchProviderPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SEARCH_PROVIDER (self), FALSE); + + priv = phosh_search_provider_get_instance_private (self); + + return priv->proxy != NULL; +} diff --git a/searchd/search-provider.h b/searchd/search-provider.h new file mode 100644 index 0000000000000000000000000000000000000000..7dd2f602d1611194d048cc5f7f69b5235b5f86d0 --- /dev/null +++ b/searchd/search-provider.h @@ -0,0 +1,63 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0+ + * + * Author: Zander Brown + */ + +#include +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SEARCH_PROVIDER phosh_search_provider_get_type() +G_DECLARE_DERIVABLE_TYPE (PhoshSearchProvider, phosh_search_provider, PHOSH, SEARCH_PROVIDER, GObject) + +struct _PhoshSearchProviderClass +{ + GObjectClass parent_class; +}; + +PhoshSearchProvider *phosh_search_provider_new (const char *desktop_app_id, + GCancellable *parent_cancellable, + const char *bus_path, + const char *bus_name, + gboolean autostart, + gboolean default_disabled); +GPtrArray *phosh_search_provider_limit_results (GStrv results, + int max); +void phosh_search_provider_activate_result (PhoshSearchProvider *self, + const char *result, + const char *const *terms, + guint timestamp); +void phosh_search_provider_launch (PhoshSearchProvider *self, + const char *const *terms, + guint timestamp); +void phosh_search_provider_get_result_meta (PhoshSearchProvider *self, + GStrv results, + GAsyncReadyCallback callback, + gpointer callback_data); +GPtrArray *phosh_search_provider_get_result_meta_finish (PhoshSearchProvider *self, + GAsyncResult *res, + GError **error); +void phosh_search_provider_get_initial (PhoshSearchProvider *self, + const char *const *terms, + GAsyncReadyCallback callback, + gpointer callback_data); +GStrv phosh_search_provider_get_initial_finish (PhoshSearchProvider *self, + GAsyncResult *res, + GError **error); +void phosh_search_provider_get_subsearch (PhoshSearchProvider *self, + const char *const *results, + const char *const *terms, + GAsyncReadyCallback callback, + gpointer callback_data); +GStrv phosh_search_provider_get_subsearch_finish (PhoshSearchProvider *self, + GAsyncResult *res, + GError **error); +gboolean phosh_search_provider_get_ready (PhoshSearchProvider *self); + +G_END_DECLS diff --git a/searchd/searchd.c b/searchd/searchd.c new file mode 100644 index 0000000000000000000000000000000000000000..3fe8ff29a9422ebf92a6ef8de60acacb708b431b --- /dev/null +++ b/searchd/searchd.c @@ -0,0 +1,744 @@ +/* + * Phosh Search Daemon + * + * Copyright (C) 2019 Zander Brown + * + * Based on gnome-shell's original js implementation + * https://gitlab.gnome.org/GNOME/gnome-shell/blob/2d2824b947754abf0ddadd9c1ba9b9f16b0745d3/js/ui/search.js + * https://gitlab.gnome.org/GNOME/gnome-shell/blob/0a7e717e0e125248bace65e170a95ae12e3cdf38/js/ui/remoteSearch.js + * + * SPDX-License-Identifier: GPL-3.0+ + * + * Authors: Zander Brown + * Guido Günther + */ + +#include "phosh-searchd.h" +#include "searchd.h" +#include "search-provider.h" + +#include +#include + +#define GROUP_NAME "Shell Search Provider" +#define SEARCH_PROVIDERS_SCHEMA "org.gnome.desktop.search-providers" + + +typedef struct _PhoshSearchApplicationPrivate PhoshSearchApplicationPrivate; +struct _PhoshSearchApplicationPrivate { + PhoshSearchDBusSearch *object; + + GSettings *settings; + + /* element-type: Phosh.SearchSource */ + GList *sources; + /* element-type: Phosh.SearchProvider */ + GHashTable *providers; + + /* key: char * (object path), value: char ** (results) */ + GHashTable *last_results; + gboolean doing_subsearch; + + char *query; + GStrv query_parts; + + GCancellable *cancellable; + + gulong search_timeout; + + GRegex *splitter; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshSearchApplication, phosh_search_application, G_TYPE_APPLICATION) + + +static void +phosh_search_application_finalize (GObject *object) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (object); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + + g_clear_object (&priv->object); + + g_cancellable_cancel (priv->cancellable); + + g_clear_object (&priv->cancellable); + g_clear_object (&priv->settings); + g_clear_pointer (&priv->last_results, g_hash_table_destroy); + + g_clear_pointer (&priv->query, g_free); + g_clear_pointer (&priv->query_parts, g_strfreev); + + g_list_free_full (priv->sources, (GDestroyNotify) phosh_search_source_unref); + g_clear_pointer (&priv->providers, g_hash_table_destroy); + + G_OBJECT_CLASS (phosh_search_application_parent_class)->finalize (object); +} + + +static gboolean +phosh_search_application_dbus_register (GApplication *app, + GDBusConnection *connection, + const gchar *object_path, + GError **error) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (app); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (priv->object), + connection, + object_path, + error)) { + g_clear_object (&priv->object); + return FALSE; + } + + return G_APPLICATION_CLASS (phosh_search_application_parent_class)->dbus_register (app, + connection, + object_path, + error); +} + + +static void +phosh_search_application_dbus_unregister (GApplication *app, + GDBusConnection *connection, + const gchar *object_path) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (app); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + + if (priv->object) { + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (priv->object)); + + g_clear_object (&priv->object); + } + + G_APPLICATION_CLASS (phosh_search_application_parent_class)->dbus_unregister (app, + connection, + object_path); +} + + +static void +phosh_search_application_class_init (PhoshSearchApplicationClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GApplicationClass *app_class = G_APPLICATION_CLASS (klass); + + object_class->finalize = phosh_search_application_finalize; + + app_class->dbus_register = phosh_search_application_dbus_register; + app_class->dbus_unregister = phosh_search_application_dbus_unregister; +} + + +static gboolean +launch_source (PhoshSearchDBusSearch *interface, + GDBusMethodInvocation *invocation, + const gchar *source_id, + guint timestamp, + gpointer user_data) +{ + phosh_search_dbus_search_complete_launch_source (interface, invocation); + + return TRUE; +} + + +static gboolean +activate_result (PhoshSearchDBusSearch *interface, + GDBusMethodInvocation *invocation, + const gchar *source_id, + const gchar *result_id, + guint timestamp, + gpointer user_data) +{ + g_message ("activate %s - %s at %i", source_id, result_id, timestamp); + + phosh_search_dbus_search_complete_activate_result (interface, invocation); + + return TRUE; +} + + +static gboolean +get_sources (PhoshSearchDBusSearch *interface, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (user_data); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + GVariantBuilder builder; + g_autoptr (GVariant) result = NULL; + GList *list; + + list = priv->sources; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ssu)")); + + while (list) { + g_variant_builder_add_value (&builder, phosh_search_source_serialise (list->data)); + + list = g_list_next (list); + } + + result = g_variant_builder_end (&builder); + + phosh_search_dbus_search_complete_get_sources (interface, + invocation, + g_variant_ref (result)); + + return TRUE; +} + + +static void +got_metas (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + GPtrArray *metas = phosh_search_provider_get_result_meta_finish (PHOSH_SEARCH_PROVIDER (source), res, &error); + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (user_data); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + GVariantBuilder builder; + g_autoptr (GVariant) result = NULL; + char *bus_path; + + g_object_get (source, "bus-path", &bus_path, NULL); + + if (error) { + g_critical ("Failed to load results %s", error->message); + return; + } + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}")); + + for (int i = 0; i < metas->len; i++) { + g_variant_builder_add_value (&builder, + phosh_search_result_meta_serialise (g_ptr_array_index (metas, i))); + } + + result = g_variant_builder_end (&builder); + + phosh_search_dbus_search_emit_source_results_changed (priv->object, + bus_path, + g_variant_ref (result)); +} + + +struct GotResultsData { + gboolean initial; + PhoshSearchApplication *self; +}; + + +static void +got_results (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + PhoshSearchApplicationPrivate *priv; + struct GotResultsData *data = user_data; + g_autoptr (GError) error = NULL; + GStrv results = NULL; + g_autoptr (GPtrArray) sub_res = NULL; + char *bus_path = NULL; + GStrv sub_res_strv = NULL; + + priv = phosh_search_application_get_instance_private (data->self); + + g_object_get (source, "bus-path", &bus_path, NULL); + + if (data->initial) { + results = phosh_search_provider_get_initial_finish (PHOSH_SEARCH_PROVIDER (source), res, &error); + } else { + results = phosh_search_provider_get_subsearch_finish (PHOSH_SEARCH_PROVIDER (source), res, &error); + } + + if (error) { + g_warning ("[%s]: %s", bus_path, error->message); + } else if (results) { + sub_res = phosh_search_provider_limit_results (results, 5); + + sub_res_strv = g_new (char *, sub_res->len + 1); + + for (int i = 0; i < sub_res->len; i++) { + sub_res_strv[i] = g_ptr_array_index (sub_res, i); + } + sub_res_strv[sub_res->len] = NULL; + + phosh_search_provider_get_result_meta (PHOSH_SEARCH_PROVIDER (source), + sub_res_strv, + got_metas, + data->self); + + g_hash_table_insert (priv->last_results, bus_path, results); + } + + g_object_unref (data->self); + g_free (data); +} + + +static void +search (PhoshSearchApplication *self) +{ + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, priv->providers); + while (g_hash_table_iter_next (&iter, &key, &value)) { + PhoshSearchProvider *provider = PHOSH_SEARCH_PROVIDER (value); + char *bus_path; + struct GotResultsData *data; + + g_object_get (provider, "bus-path", &bus_path, NULL); + + if (!phosh_search_provider_get_ready (provider)) { + g_warning ("[%s]: not ready", bus_path); + continue; + } + + data = g_new (struct GotResultsData, 1); + data->self = g_object_ref (self); + + if (priv->doing_subsearch && + g_hash_table_contains (priv->last_results, bus_path)) { + data->initial = FALSE; + phosh_search_provider_get_subsearch (provider, + g_hash_table_lookup (priv->last_results, bus_path), + (const char * const*) priv->query_parts, + got_results, + data); + } else { + data->initial = TRUE; + + phosh_search_provider_get_initial (provider, + (const char * const*) priv->query_parts, + got_results, + data); + } + } + + g_hash_table_remove_all (priv->last_results); + + if (priv->search_timeout != 0) { + g_source_remove (priv->search_timeout); + priv->search_timeout = 0; + } +} + + +static gboolean +search_timeout (gpointer user_data) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (user_data); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + + priv->search_timeout = 0; + + search (self); + + return G_SOURCE_REMOVE; +} + + +static gboolean +query (PhoshSearchDBusSearch *interface, + GDBusMethodInvocation *invocation, + const char *query, + gpointer user_data) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (user_data); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + g_autofree char *striped = NULL; + g_auto (GStrv) parts = NULL; + int len = 0; + + striped = g_strstrip (g_strdup (query)); + parts = g_regex_split (priv->splitter, striped, 0); + + len = parts ? g_strv_length (parts) : 0; + if (priv->query_parts && g_strv_equal ((const char *const *) priv->query_parts, + (const char *const *) parts)) { + phosh_search_dbus_search_complete_query (interface, invocation, FALSE); + return TRUE; + } + + g_cancellable_cancel (priv->cancellable); + /* FIXME: don't reuse cancellable */ + g_cancellable_reset (priv->cancellable); + + if (len == 0) { + g_clear_pointer (&priv->query, g_free); + g_clear_pointer (&priv->query_parts, g_strfreev); + + priv->doing_subsearch = FALSE; + + phosh_search_dbus_search_complete_query (interface, invocation, FALSE); + + return TRUE; + } + + if (priv->query != NULL) { + priv->doing_subsearch = g_str_has_prefix (query, priv->query); + } else { + priv->doing_subsearch = FALSE; + } + + g_clear_pointer (&priv->query_parts, g_strfreev); + priv->query_parts = g_strdupv (parts); + priv->query = g_strdup (query); + + if (priv->search_timeout == 0) { + priv->search_timeout = g_timeout_add (150, search_timeout, self); + } + + phosh_search_dbus_search_complete_query (interface, invocation, TRUE); + + return TRUE; +} + + +// Sort algorithm taken straight from remoteSearch.js, comments and all +static int +sort_sources (gconstpointer a, gconstpointer b, gpointer user_data) +{ + GAppInfo *app_a = NULL; + GAppInfo *app_b = NULL; + const char *app_id_a = NULL; + const char *app_id_b = NULL; + GStrv order = user_data; + int idx_a = -1; + int idx_b = -1; + int i = 0; + + g_return_val_if_fail (a != NULL, -1); + g_return_val_if_fail (b != NULL, -1); + + app_a = phosh_search_source_get_app_info ((PhoshSearchSource *) a); + app_b = phosh_search_source_get_app_info ((PhoshSearchSource *) b); + + g_return_val_if_fail (G_IS_APP_INFO (app_a), -1); + g_return_val_if_fail (G_IS_APP_INFO (app_b), -1); + + app_id_a = g_app_info_get_id (app_a); + app_id_b = g_app_info_get_id (app_b); + + while ((order[i])) { + if (idx_a == -1 && g_strcmp0 (order[i], app_id_a) == 0) { + idx_a = i; + } + + if (idx_b == -1 && g_strcmp0 (order[i], app_id_b) == 0) { + idx_b = i; + } + + if (idx_a != -1 && idx_b != -1) { + break; + } + + i++; + } + + // if no provider is found in the order, use alphabetical order + if ((idx_a == -1) && (idx_b == -1)) { + return g_utf8_collate (g_app_info_get_name (app_a), + g_app_info_get_name (app_b)); + } + + // if providerA isn't found, it's sorted after providerB + if (idx_a == -1) + return 1; + + // if providerB isn't found, it's sorted after providerA + if (idx_b == -1) + return -1; + + // finally, if both providers are found, return their order in the list + return (idx_a - idx_b); +} + + +static void +reload_providers (PhoshSearchApplication *self) +{ + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + const char *const *data_dirs = g_get_system_data_dirs (); + const char *data_dir = NULL; + g_autolist (PhoshSearchSource) sources = NULL; + // These two skip the normal sorting + g_autoptr (PhoshSearchSource) apps = NULL; + g_autoptr (PhoshSearchSource) settings = NULL; + g_auto (GStrv) enabled = NULL; + g_auto (GStrv) disabled = NULL; + g_auto (GStrv) sort_order = NULL; + GList *list; + int i = 0; + + if (g_settings_get_boolean (priv->settings, "disable-external")) { + g_list_free_full (priv->sources, (GDestroyNotify) phosh_search_source_unref); + } + + enabled = g_settings_get_strv (priv->settings, "enabled"); + disabled = g_settings_get_strv (priv->settings, "disabled"); + sort_order = g_settings_get_strv (priv->settings, "sort-order"); + + while ((data_dir = data_dirs[i])) { + g_autofree char *dir = NULL; + g_autoptr (GError) error = NULL; + g_autoptr (GDir) contents = NULL; + const char* name = NULL; + + i++; + + dir = g_build_filename (data_dir, "gnome-shell", "search-providers", NULL); + contents = g_dir_open (dir, 0, &error); + + if (error) { + if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { + g_warning ("Can't look for in %s: %s", dir, error->message); + } + g_clear_error (&error); + continue; + } + + while ((name = g_dir_read_name (contents))) { + g_autofree char *provider = NULL; + g_autofree char *bus_path = NULL; + g_autofree char *bus_name = NULL; + g_autofree char *desktop_id = NULL; + g_autoptr (GKeyFile) data = NULL; + g_autoptr (PhoshSearchProvider) provider_object = NULL; + g_autoptr (PhoshSearchSource) source = NULL; + int version = 0; + gboolean autostart = TRUE; + gboolean autostart_tmp = FALSE; + gboolean default_disabled = FALSE; + gboolean default_disabled_tmp = FALSE; + + provider = g_build_filename (dir, name, NULL); + data = g_key_file_new (); + + g_key_file_load_from_file (data, provider, G_KEY_FILE_NONE, &error); + + if (error) { + g_warning ("Can't read %s: %s", provider, error->message); + g_clear_error (&error); + continue; + } + + if (!g_key_file_has_group (data, GROUP_NAME)) { + g_warning ("%s doesn't define a search provider", provider); + continue; + } + + + version = g_key_file_get_integer (data, GROUP_NAME, "Version", &error); + + if (error) { + g_warning ("Failed to fetch provider version %s: %s", provider, error->message); + g_clear_error (&error); + continue; + } + + if (version < 2) { + g_warning ("Provider %s implements version %i but we only support version 2 and up", provider, version); + continue; + } + + + desktop_id = g_key_file_get_string (data, GROUP_NAME, "DesktopId", &error); + if (error) { + g_warning ("Failed to fetch provider desktop id %s: %s", provider, error->message); + g_clear_error (&error); + continue; + } + if (!desktop_id) { + g_warning ("Provider %s doesn't specify a desktop id", provider); + continue; + } + + + bus_name = g_key_file_get_string (data, GROUP_NAME, "BusName", &error); + if (error) { + g_warning ("Failed to fetch provider bus name %s: %s", provider, error->message); + g_clear_error (&error); + continue; + } + if (!bus_name) { + g_warning ("Provider %s doesn't specify a bus name", provider); + continue; + } + + + bus_path = g_key_file_get_string (data, GROUP_NAME, "ObjectPath", &error); + if (error) { + g_warning ("Failed to fetch provider bus path %s: %s", provider, error->message); + g_clear_error (&error); + continue; + } + if (!bus_path) { + g_warning ("Provider %s doesn't specify a bus path", provider); + continue; + } + + if (g_hash_table_contains (priv->providers, bus_path)) { + g_debug ("We already have a provider for %s, ignoring %s", bus_path, provider); + continue; + } + + autostart_tmp = g_key_file_get_boolean (data, GROUP_NAME, "AutoStart", &error); + + if (G_LIKELY (error)) { + g_clear_error (&error); + } else { + autostart = autostart_tmp; + } + + default_disabled_tmp = g_key_file_get_boolean (data, GROUP_NAME, "DefaultDisabled", &error); + if (G_LIKELY (error)) { + g_clear_error (&error); + } else { + default_disabled = default_disabled_tmp; + } + + if (!default_disabled) { + if (g_strv_contains ((const char * const*) disabled, desktop_id)) { + g_debug ("Provider %s has been disabled", provider); + continue; + } + } else { + if (!g_strv_contains ((const char * const*) enabled, desktop_id)) { + g_debug ("Provider %s hasn't been enabled", provider); + continue; + } + } + + provider_object = phosh_search_provider_new (desktop_id, + priv->cancellable, + bus_path, + bus_name, + autostart, + default_disabled); + + if (G_UNLIKELY (g_str_equal (desktop_id, "sm.puri.Phosh.AppSearch.desktop"))) { + apps = phosh_search_source_new (bus_path, + G_APP_INFO (g_desktop_app_info_new (desktop_id))); + } else if (G_UNLIKELY (g_str_equal (desktop_id, "gnome-control-center.desktop"))) { + settings = phosh_search_source_new (bus_path, + G_APP_INFO (g_desktop_app_info_new (desktop_id))); + } else { + source = phosh_search_source_new (bus_path, + G_APP_INFO (g_desktop_app_info_new (desktop_id))); + + sources = g_list_prepend (sources, phosh_search_source_ref (source)); + } + + g_hash_table_insert (priv->providers, + g_strdup (bus_path), + g_object_ref (provider_object)); + } + } + + sources = g_list_sort_with_data (sources, sort_sources, sort_order); + + if (settings) { + sources = g_list_prepend (sources, phosh_search_source_ref (settings)); + } + + if (apps) { + sources = g_list_prepend (sources, phosh_search_source_ref (apps)); + } + + list = sources; + i = 0; + + while (list) { + phosh_search_source_set_position (list->data, i); + + i++; + list = g_list_next (list); + } + + g_list_free_full (priv->sources, (GDestroyNotify) phosh_search_source_unref); + priv->sources = g_list_copy_deep (sources, (GCopyFunc) phosh_search_source_ref, NULL); +} + + +static void +reload_providers_apps_changed (GAppInfoMonitor *monitor, + PhoshSearchApplication *self) +{ + reload_providers (self); +} + + +static void +phosh_search_application_init (PhoshSearchApplication *self) +{ + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + g_autoptr (GError) error = NULL; + + priv->doing_subsearch = FALSE; + priv->search_timeout = 0; + + priv->splitter = g_regex_new ("\\s+", + G_REGEX_CASELESS | G_REGEX_MULTILINE, + 0, + &error); + if (error) + g_error ("Bad Regex: %s", error->message); + + priv->last_results = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) g_strfreev); + priv->providers = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) g_object_unref); + + priv->cancellable = g_cancellable_new (); + + priv->settings = g_settings_new (SEARCH_PROVIDERS_SCHEMA); + g_object_connect (priv->settings, + "swapped-object-signal::changed::disabled", reload_providers, self, + "swapped-object-signal::changed::enabled", reload_providers, self, + "swapped-object-signal::changed::disable-external", reload_providers, self, + "swapped-object-signal::changed::sort-order", reload_providers, self, + NULL); + + g_signal_connect (g_app_info_monitor_get (), + "changed", + G_CALLBACK (reload_providers_apps_changed), self); + + priv->object = phosh_search_dbus_search_skeleton_new (); + g_object_connect (priv->object, + "object-signal::handle-launch-source", launch_source, self, + "object-signal::handle-activate-result", activate_result, self, + "object-signal::handle-get-sources", get_sources, self, + "object-signal::handle-query", query, self, + NULL); + + reload_providers (self); + + g_application_hold (G_APPLICATION (self)); +} + + +int +main (int argc, char **argv) +{ + g_autoptr (GApplication) app = NULL; + + app = g_object_new (PHOSH_TYPE_SEARCH_APPLICATION, + "application-id", "sm.puri.Phosh.Search", + "flags", G_APPLICATION_IS_SERVICE, + NULL); + + return g_application_run (app, argc, argv); +} diff --git a/searchd/searchd.h b/searchd/searchd.h new file mode 100644 index 0000000000000000000000000000000000000000..11fc6535e6fb7f4fe219bf910e802bfd335b7b5b --- /dev/null +++ b/searchd/searchd.h @@ -0,0 +1,23 @@ +/* + * Phosh Search Daemon + * + * Copyright (C) 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0+ + * + * Author: Zander Brown + */ + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SEARCH_APPLICATION phosh_search_application_get_type() +G_DECLARE_DERIVABLE_TYPE (PhoshSearchApplication, phosh_search_application, PHOSH, SEARCH_APPLICATION, GApplication) + +struct _PhoshSearchApplicationClass +{ + GApplicationClass parent_class; +}; + +G_END_DECLS diff --git a/src/app-grid.c b/src/app-grid.c index 26866b1c3713dc10a5a127cd2ca6c94c3f0cb89f..ae6c38a760b28d2ae1ca0372b6700dfe0c3bd712 100644 --- a/src/app-grid.c +++ b/src/app-grid.c @@ -357,7 +357,7 @@ static void phosh_app_grid_init (PhoshAppGrid *self) { PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); - GtkSortListModel *sorted; + g_autoptr (GtkSortListModel) sorted = NULL; PhoshFavoriteListModel *favorites; g_autoptr (GAction) action = NULL; @@ -382,7 +382,6 @@ phosh_app_grid_init (PhoshAppGrid *self) search_apps, self, NULL); - g_object_unref (sorted); gtk_flow_box_bind_model (GTK_FLOW_BOX (priv->apps), G_LIST_MODEL (priv->model), create_launcher, self, NULL); diff --git a/src/dbus/meson.build b/src/dbus/meson.build index 17a9c23a337197ed89486850d856eada65c4d839..f518ea57fd5dfdf9712a8a28e70b28fdc928b0b2 100644 --- a/src/dbus/meson.build +++ b/src/dbus/meson.build @@ -134,3 +134,17 @@ generated_dbus_sources += gnome.gdbus_codegen('phosh-gtk-mountoperation-dbus', namespace: dbus_prefix) dbus_doc_dep = declare_dependency(sources: dbus_doc_targets) + +# Shell Search +phosh_dbus_inc = include_directories('.') +generated_search_dbus_sources = [] + +generated_search_dbus_sources += gnome.gdbus_codegen('gnome-shell-search-provider', + 'org.gnome.Shell.SearchProvider2.xml', + interface_prefix: 'org.gnome.Shell', + namespace: dbus_prefix) + +generated_search_dbus_sources += gnome.gdbus_codegen('phosh-searchd', + 'sm.puri.Phosh.Search.xml', + interface_prefix: 'sm.puri.Phosh', + namespace: 'PhoshSearchDBus') diff --git a/src/dbus/org.gnome.Shell.SearchProvider2.xml b/src/dbus/org.gnome.Shell.SearchProvider2.xml new file mode 100644 index 0000000000000000000000000000000000000000..9502340e4633e2885c9beaec31248161ee46000b --- /dev/null +++ b/src/dbus/org.gnome.Shell.SearchProvider2.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/sm.puri.Phosh.Search.xml b/src/dbus/sm.puri.Phosh.Search.xml new file mode 100644 index 0000000000000000000000000000000000000000..d268eee9aef489e2cbf4705483ca8ecdd679dbf1 --- /dev/null +++ b/src/dbus/sm.puri.Phosh.Search.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/meson.build b/src/meson.build index cdcd786a07c4bd66220d6d52ebd41c178f59e7d5..1a16f50da0a183a8ac5f07816e03caeb241a4d8f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -2,6 +2,7 @@ subdir('dbus') subdir('monitor') subdir('notifications') subdir('wwan') +subdir('search') subdir('settings') subdir('gtk-list-models') @@ -188,6 +189,7 @@ libphosh_tool_sources = files( phosh_layer_surface_sources, phosh_monitor_sources, phosh_notifications_sources, + phosh_search_sources, ] # Symbols from these are not available in tools and unit tests @@ -322,6 +324,7 @@ phosh_deps = [ network_agent_dep, upower_glib_dep, wayland_client_dep, + phoshsearch_dep, cc.find_library('pam', required: true), cc.find_library('m', required: false), cc.find_library('rt', required: false), diff --git a/src/phosh.gresources.xml b/src/phosh.gresources.xml index 57744fd9254dbd43d1bee012876401d4b455be70..26e7d39de3bd83f296ec40da99e211a14ec3bb2f 100644 --- a/src/phosh.gresources.xml +++ b/src/phosh.gresources.xml @@ -25,6 +25,9 @@ ui/osd-window.ui ui/power-menu.ui ui/run-command-dialog.ui + ui/search-result-app-frame.ui + ui/search-result-content.ui + ui/search-result-frame.ui ui/settings.ui ui/system-modal-dialog.ui ui/splash.ui diff --git a/src/search/meson.build b/src/search/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..1c1ca79ba8956c37f623e091666c31cb58ce5b67 --- /dev/null +++ b/src/search/meson.build @@ -0,0 +1,42 @@ +phosh_search_sources = [ + 'search/search-result.c', + 'search/search-result.h', + 'search/search-result-app-frame.c', + 'search/search-result-app-frame.h', + 'search/search-result-content.c', + 'search/search-result-content.h', + 'search/search-result-frame.c', + 'search/search-result-frame.h', + 'search/search-result-list.c', + 'search/search-result-list.h', + 'search/search-result-source.c', + 'search/search-result-source.h', +] + +phoshsearch_sources = [ + 'search-client.c', + 'search-client.h', + 'search-result-meta.c', + 'search-result-meta.h', + 'search-source.c', + 'search-source.h', + generated_search_dbus_sources, +] + +phoshsearch_deps = [ + gio_dep, + gio_unix_dep, + dependency('gdk-pixbuf-2.0'), +] + +phoshsearch_lib = library('phoshsearch', phoshsearch_sources, + include_directories: [root_inc, phosh_dbus_inc], + dependencies: phoshsearch_deps, + install_dir: pkglibdir, + install: true) +phoshsearch_inc = include_directories('.') + +phoshsearch_dep = declare_dependency(sources: generated_search_dbus_sources, + include_directories: [phosh_dbus_inc, phoshsearch_inc], + link_with: phoshsearch_lib, + dependencies: phoshsearch_deps) diff --git a/src/search/search-client.c b/src/search/search-client.c new file mode 100644 index 0000000000000000000000000000000000000000..7bcd6994b0d242b7f6dd9da749d2605f8949e307 --- /dev/null +++ b/src/search/search-client.c @@ -0,0 +1,343 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0+ + * + * Author: Zander Brown + */ + +#include "search-client.h" +#include "search-result-meta.h" +#include "search-source.h" +#include "phosh-searchd.h" + +enum State { + CREATED, + STARTING, + READY, +}; + +typedef struct _PhoshSearchClientPrivate PhoshSearchClientPrivate; +struct _PhoshSearchClientPrivate { + PhoshSearchDBusSearch *server; + + enum State state; + + GRegex *highlight; + GRegex *splitter; + + GCancellable *cancellable; +}; + +static void async_iface_init (GAsyncInitableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshSearchClient, phosh_search_client, G_TYPE_OBJECT, + G_ADD_PRIVATE (PhoshSearchClient) + G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_iface_init)) + + +enum { + SOURCE_RESULTS_CHANGED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +static void +phosh_search_client_finalize (GObject *object) +{ + PhoshSearchClient *self = PHOSH_SEARCH_CLIENT (object); + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + + g_cancellable_cancel (priv->cancellable); + + g_clear_object (&priv->server); + g_clear_object (&priv->cancellable); + + g_clear_pointer (&priv->highlight, g_regex_unref); + g_clear_pointer (&priv->splitter, g_regex_unref); + + G_OBJECT_CLASS (phosh_search_client_parent_class)->finalize (object); +} + + +static void +phosh_search_client_class_init (PhoshSearchClientClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = phosh_search_client_finalize; + + signals[SOURCE_RESULTS_CHANGED] = g_signal_new ("source-results-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 2, + G_TYPE_STRING, + G_TYPE_PTR_ARRAY); +} + + +static void +results (PhoshSearchDBusSearch *server, + const char *source_id, + GVariant *variant, + PhoshSearchClient *self) +{ + GVariantIter iter; + GVariant *item; + GPtrArray *results = NULL; + + results = g_ptr_array_new_with_free_func ((GDestroyNotify) phosh_search_result_meta_unref); + + g_variant_iter_init (&iter, variant); + while ((item = g_variant_iter_next_value (&iter))) { + g_autoptr (PhoshSearchResultMeta) result = NULL; + + result = phosh_search_result_meta_deserialise (item); + + g_ptr_array_add (results, phosh_search_result_meta_ref (result)); + + g_clear_pointer (&item, g_variant_unref); + } + + g_signal_emit (self, + signals[SOURCE_RESULTS_CHANGED], + g_quark_from_string (source_id), + source_id, + results); +} + + +struct InitData { + PhoshSearchClient *self; + GTask *task; +}; + +static void +got_search (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + g_autofree struct InitData *data = user_data; + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (data->self); + + priv->server = phosh_search_dbus_search_proxy_new_finish (result, &error); + + priv->state = READY; + + if (error) { + g_task_return_error (data->task, error); + return; + } + + g_signal_connect (priv->server, + "source-results-changed", + G_CALLBACK (results), + data->self); + + g_task_return_boolean (data->task, TRUE); +} + + +static void +phosh_search_client_init_async (GAsyncInitable *initable, + int io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + PhoshSearchClient *self = PHOSH_SEARCH_CLIENT (initable); + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + GTask* task = NULL; + struct InitData *data; + + switch (priv->state) { + case CREATED: + priv->state = STARTING; + + task = g_task_new (initable, cancellable, callback, user_data); + + data = g_new0 (struct InitData, 1); + data->self = self; + data->task = task; + + phosh_search_dbus_search_proxy_new_for_bus (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + "sm.puri.Phosh.Search", + "/sm/puri/Phosh/Search", + cancellable, + got_search, + data); + break; + case STARTING: + g_critical ("Already initialising"); + break; + case READY: + g_critical ("Already initialised"); + break; + default: + g_assert_not_reached (); + } +} + + +static gboolean +phosh_search_client_init_finish (GAsyncInitable *initable, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, initable), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + + +static void +async_iface_init (GAsyncInitableIface *iface) +{ + iface->init_async = phosh_search_client_init_async; + iface->init_finish = phosh_search_client_init_finish; +} + + +static void +phosh_search_client_init (PhoshSearchClient *self) +{ + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + g_autoptr (GError) error = NULL; + + priv->highlight = NULL; + priv->splitter = g_regex_new ("\\s+", + G_REGEX_CASELESS | G_REGEX_MULTILINE, + 0, + &error); + priv->state = CREATED; + priv->cancellable = g_cancellable_new (); + + if (error) { + g_error ("Bad Regex: %s", error->message); + } +} + + +static void +got_query (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + PhoshSearchClient *self = PHOSH_SEARCH_CLIENT (user_data); + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + g_autoptr (GError) error = NULL; + + phosh_search_dbus_search_call_query_finish (priv->server, + NULL, + result, + &error); + + if (error) { + g_critical ("Unable to send search: %s", error->message); + } +} + + +void +phosh_search_client_query (PhoshSearchClient *self, + const char *str) +{ + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + g_autofree char *regex_terms = NULL; + g_autofree char *regex = NULL; + g_autofree char *striped = NULL; + g_auto (GStrv) parts = NULL; + g_auto (GStrv) escaped = NULL; + g_autoptr (GError) error = NULL; + int len = 0; + int i = 0; + + phosh_search_dbus_search_call_query (priv->server, + str, + priv->cancellable, + got_query, + self); + + striped = g_strstrip (g_strdup (str)); + parts = g_regex_split (priv->splitter, striped, 0); + + len = parts ? g_strv_length (parts) : 0; + + escaped = g_new0 (char *, len + 1); + + while (parts[i]) { + escaped[i] = g_regex_escape_string (parts[i], -1); + + i++; + } + escaped[len] = NULL; + + regex_terms = g_strjoinv ("|", escaped); + regex = g_strconcat ("(", regex_terms, ")", NULL); + priv->highlight = g_regex_new (regex, + G_REGEX_CASELESS | G_REGEX_MULTILINE, + 0, + &error); + + if (error) { + g_warning ("Unable to prepare highlighter: %s", error->message); + g_clear_error (&error); + priv->highlight = NULL; + } +} + + +const char * +phosh_search_client_markup_string (PhoshSearchClient *self, + const char *string) +{ + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + g_autoptr (GError) error = NULL; + char *marked = NULL; + + if (string == NULL) { + return NULL; + } + + marked = g_regex_replace (priv->highlight, + string, -1, 0, + "\\1", + 0, + &error); + + if (error) { + return g_strdup (string); + } + + return marked; +} + +void +phosh_search_client_new (GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_async_initable_new_async (PHOSH_TYPE_SEARCH_CLIENT, + G_PRIORITY_DEFAULT, + cancellable, + callback, + user_data, + NULL); +} + + +PhoshSearchClient * +phosh_search_client_new_finish (GObject *source, + GAsyncResult *result, + GError **error) +{ + return PHOSH_SEARCH_CLIENT (g_async_initable_new_finish (G_ASYNC_INITABLE (source), + result, + error)); +} diff --git a/src/search/search-client.h b/src/search/search-client.h new file mode 100644 index 0000000000000000000000000000000000000000..14718eb1129732212d12241938387c9941facd9c --- /dev/null +++ b/src/search/search-client.h @@ -0,0 +1,34 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0+ + * + * Author: Zander Brown + */ + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SEARCH_CLIENT phosh_search_client_get_type() +G_DECLARE_DERIVABLE_TYPE (PhoshSearchClient, phosh_search_client, PHOSH, SEARCH_CLIENT, GObject) + +struct _PhoshSearchClientClass +{ + GObjectClass parent_class; +}; + +const char *phosh_search_client_markup_string (PhoshSearchClient *self, + const char *string); +void phosh_search_client_query (PhoshSearchClient *self, + const char *query); +void phosh_search_client_new (GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +PhoshSearchClient *phosh_search_client_new_finish (GObject *source, + GAsyncResult *result, + GError **error); + +G_END_DECLS diff --git a/src/search/search-result-app-frame.c b/src/search/search-result-app-frame.c new file mode 100644 index 0000000000000000000000000000000000000000..f0e978223ee7584686cd3fa8e831d34e1558fea6 --- /dev/null +++ b/src/search/search-result-app-frame.c @@ -0,0 +1,149 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-search-result-app-frame" + +#include "config.h" + +#include "search-result-app-frame.h" +#include "search-result-source.h" +#include "app-grid-button.h" + +#include + + +/** + * SECTION:search-result-app-frame + * @short_description: A frame containing one or more #PhoshAppGridButton + * @Title: PhoshSearchResultAppFrame + * + * An alternative to #PhoshSearchResultFrame using #PhoshAppGridButtons + * generated from the result #PhoshSearchResult:id + */ + + +struct _PhoshSearchResultAppFrame { + GtkListBoxRow parent; + + PhoshSearchResultSource *source; + + GtkFlowBox *apps; +}; +typedef struct _PhoshSearchResultAppFrame PhoshSearchResultAppFrame; + + +G_DEFINE_TYPE (PhoshSearchResultAppFrame, phosh_search_result_app_frame, GTK_TYPE_LIST_BOX_ROW) + + +static void +phosh_search_result_app_frame_dispose (GObject *object) +{ + PhoshSearchResultAppFrame *self = PHOSH_SEARCH_RESULT_APP_FRAME (object); + + // Don't clear bindings, they are unref'd automatically + + g_clear_object (&self->source); + + G_OBJECT_CLASS (phosh_search_result_app_frame_parent_class)->dispose (object); +} + + +static void +item_activated (GtkFlowBox *box, + GtkFlowBoxChild *row, + PhoshSearchResultAppFrame *self) +{ + g_return_if_fail (PHOSH_IS_SEARCH_RESULT_APP_FRAME (self)); + + /* TODO */ +} + + +static void +phosh_search_result_app_frame_class_init (PhoshSearchResultAppFrameClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = phosh_search_result_app_frame_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/phosh/ui/search-result-app-frame.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshSearchResultAppFrame, apps); + + gtk_widget_class_bind_template_callback (widget_class, item_activated); + + gtk_widget_class_set_css_name (widget_class, "phosh-search-app-result-frame"); +} + + +static void +phosh_search_result_app_frame_init (PhoshSearchResultAppFrame *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +/** + * phosh_search_result_app_frame_new: + * + * Create a #PhoshSearchResultAppFrame, use + * phosh_search_result_app_frame_bind_source() to populate it + * + * Returns: (transfer float): a new #PhoshSearchResultAppFrame + */ +GtkWidget * +phosh_search_result_app_frame_new (void) +{ + return g_object_new (PHOSH_TYPE_SEARCH_RESULT_APP_FRAME, NULL); +} + + +static GtkWidget * +create_row (gpointer item, gpointer data) +{ + PhoshSearchResult *result = item; + GDesktopAppInfo *info = NULL; + const char *desktop_id = NULL; + + desktop_id = phosh_search_result_get_id (result); + info = g_desktop_app_info_new (desktop_id); + + if (G_UNLIKELY (info == NULL)) { + g_warning ("Unknown desktop-id: “%s”", desktop_id); + + return NULL; + } + + return phosh_app_grid_button_new (G_APP_INFO (info)); +} + + +/** + * phosh_search_result_app_frame_bind_source: + * @self: the #PhoshSearchResultAppFrame + * @source: the #PhoshSearchResultSource to bind to + * + * Bind @self to a #PhoshSearchResultSource, note @self can be reused for + * different #PhoshSearchResultSources + */ +void +phosh_search_result_app_frame_bind_source (PhoshSearchResultAppFrame *self, + PhoshSearchResultSource *source) +{ + g_return_if_fail (PHOSH_IS_SEARCH_RESULT_APP_FRAME (self)); + g_return_if_fail (PHOSH_IS_SEARCH_RESULT_SOURCE (source)); + + g_set_object (&self->source, source); + + gtk_flow_box_bind_model (GTK_FLOW_BOX (self->apps), + G_LIST_MODEL (source), + create_row, + self, + NULL); +} diff --git a/src/search/search-result-app-frame.h b/src/search/search-result-app-frame.h new file mode 100644 index 0000000000000000000000000000000000000000..9ac4e91330fc1047e74a15cdee79559901869112 --- /dev/null +++ b/src/search/search-result-app-frame.h @@ -0,0 +1,28 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#pragma once + +#include + +#include "search-result-source.h" + +G_BEGIN_DECLS + + +#define PHOSH_TYPE_SEARCH_RESULT_APP_FRAME phosh_search_result_app_frame_get_type () + + +G_DECLARE_FINAL_TYPE (PhoshSearchResultAppFrame, phosh_search_result_app_frame, PHOSH, SEARCH_RESULT_APP_FRAME, GtkListBoxRow) + + +GtkWidget *phosh_search_result_app_frame_new (void); +void phosh_search_result_app_frame_bind_source (PhoshSearchResultAppFrame *self, + PhoshSearchResultSource *source); + +G_END_DECLS diff --git a/src/search/search-result-content.c b/src/search/search-result-content.c new file mode 100644 index 0000000000000000000000000000000000000000..f6a81b4ff39ddf644a13d912f9bbf4c94410cb79 --- /dev/null +++ b/src/search/search-result-content.c @@ -0,0 +1,195 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-search-result-content" + +#include "search-result-content.h" + + +/** + * SECTION:search-result-content + * @short_description: Shows a #PhoshSearchResult + * @Title: PhoshSearchResultContent + * + * A #GtkListBoxRow used to represent a #PhoshSearchResult + */ + +enum { + PROP_0, + PROP_RESULT, + LAST_PROP +}; +static GParamSpec *props[LAST_PROP]; + + +struct _PhoshSearchResultContent { + GtkListBoxRow parent; + + PhoshSearchResult *result; + + GtkWidget *icon_img; + GtkWidget *title_lbl; + GtkWidget *desc_lbl; +}; +typedef struct _PhoshSearchResultContent PhoshSearchResultContent; + + +G_DEFINE_TYPE (PhoshSearchResultContent, phosh_search_result_content, GTK_TYPE_LIST_BOX_ROW) + + +static void +phosh_search_result_content_set_result (PhoshSearchResultContent *self, + PhoshSearchResult *result) +{ + g_return_if_fail (PHOSH_IS_SEARCH_RESULT_CONTENT (self)); + g_return_if_fail (PHOSH_IS_SEARCH_RESULT (result)); + + g_set_object (&self->result, result); + + // Use the "transform" function to show/hide when set/unset + g_object_bind_property (self->result, "icon", + self->icon_img, "gicon", + G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->result, "title", + self->title_lbl, "label", + G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->result, "description", + self->desc_lbl, "label", + G_BINDING_SYNC_CREATE); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_RESULT]); +} + + +static void +phosh_search_result_content_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshSearchResultContent *self = PHOSH_SEARCH_RESULT_CONTENT (object); + + switch (property_id) { + case PROP_RESULT: + phosh_search_result_content_set_result (self, + g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_search_result_content_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshSearchResultContent *self = PHOSH_SEARCH_RESULT_CONTENT (object); + + switch (property_id) { + case PROP_RESULT: + g_value_set_object (value, self->result); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_search_result_content_dispose (GObject *object) +{ + PhoshSearchResultContent *self = PHOSH_SEARCH_RESULT_CONTENT (object); + + g_clear_object (&self->result); + + G_OBJECT_CLASS (phosh_search_result_content_parent_class)->dispose (object); +} + + +static void +phosh_search_result_content_class_init (PhoshSearchResultContentClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = phosh_search_result_content_dispose; + object_class->set_property = phosh_search_result_content_set_property; + object_class->get_property = phosh_search_result_content_get_property; + + /** + * PhoshSearchResultContent:result: + * @self: the #PhoshSearchResultContent + * + * The #PhoshSearchResult shown in @self + */ + props[PROP_RESULT] = + g_param_spec_object ("result", + "Result", + "Search result being shown", + PHOSH_TYPE_SEARCH_RESULT, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/phosh/ui/search-result-content.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshSearchResultContent, icon_img); + gtk_widget_class_bind_template_child (widget_class, PhoshSearchResultContent, title_lbl); + gtk_widget_class_bind_template_child (widget_class, PhoshSearchResultContent, desc_lbl); + + gtk_widget_class_set_css_name (widget_class, "phosh-search-result-content"); +} + + +static void +phosh_search_result_content_init (PhoshSearchResultContent *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +/** + * phosh_search_result_content_new: + * @result: (transfer none): the #PhoshSearchResult to show + * + * Create a #PhoshSearchResultContent to represent a #PhoshSearchResult + * + * Returns: (transfer float): a new #PhoshSearchResultContent + */ +GtkWidget * +phosh_search_result_content_new (PhoshSearchResult *result) +{ + return g_object_new (PHOSH_TYPE_SEARCH_RESULT_CONTENT, + "result", result, + NULL); +} + + +/** + * phosh_search_result_content_get_result: + * + * Get the #PhoshSearchResult currently in @self + * + * Returns: (transfer none): the current #PhoshSearchResult in @self + */ +PhoshSearchResult * +phosh_search_result_content_get_result (PhoshSearchResultContent *self) +{ + g_return_val_if_fail (PHOSH_IS_SEARCH_RESULT_CONTENT (self), NULL); + + return self->result; +} diff --git a/src/search/search-result-content.h b/src/search/search-result-content.h new file mode 100644 index 0000000000000000000000000000000000000000..98f735f79d43a91b53039780bd4b482f39c70558 --- /dev/null +++ b/src/search/search-result-content.h @@ -0,0 +1,28 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#pragma once + +#include + +#include "search-result.h" + +G_BEGIN_DECLS + + +#define PHOSH_TYPE_SEARCH_RESULT_CONTENT (phosh_search_result_content_get_type ()) + + +G_DECLARE_FINAL_TYPE (PhoshSearchResultContent, phosh_search_result_content, PHOSH, SEARCH_RESULT_CONTENT, GtkListBoxRow) + + +GtkWidget *phosh_search_result_content_new (PhoshSearchResult *result); +PhoshSearchResult *phosh_search_result_content_get_result (PhoshSearchResultContent *self); + + +G_END_DECLS diff --git a/src/search/search-result-frame.c b/src/search/search-result-frame.c new file mode 100644 index 0000000000000000000000000000000000000000..c063a988d6c248c2596cc6bdef67cab57aec9961 --- /dev/null +++ b/src/search/search-result-frame.c @@ -0,0 +1,231 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-search-result-frame" + +#include "config.h" + +#include "search-result-frame.h" +#include "search-result-source.h" +#include "search-result-content.h" + +#include + + +/** + * SECTION:search-result-frame + * @short_description: A frame containing one or more #PhoshSearchResultContent + * @Title: PhoshSearchResultFrame + * + * A #PhoshSearchResultFrame is the view of the #PhoshSearchResultSource model + * containing a #PhoshSearchResultContent for each of a + * #PhoshSearchResultSource's #PhoshSearchResults + */ + + +struct _PhoshSearchResultFrame { + GtkListBoxRow parent; + + PhoshSearchResultSource *source; + + GBinding *bind_name; + GBinding *bind_icon; + GBinding *bind_extra; + + GtkWidget *items; + GtkWidget *name_lbl; + GtkWidget *extra_lbl; + GtkWidget *icon_img; +}; +typedef struct _PhoshSearchResultFrame PhoshSearchResultFrame; + + +G_DEFINE_TYPE (PhoshSearchResultFrame, phosh_search_result_frame, GTK_TYPE_LIST_BOX_ROW) + + +static void +phosh_search_result_frame_dispose (GObject *object) +{ + PhoshSearchResultFrame *self = PHOSH_SEARCH_RESULT_FRAME (object); + + // Don't clear bindings, they are unref'd automatically + + g_clear_object (&self->source); + + G_OBJECT_CLASS (phosh_search_result_frame_parent_class)->dispose (object); +} + + +static void +item_activated (GtkListBox *list, + GtkListBoxRow *row, + PhoshSearchResultFrame *self) +{ + g_return_if_fail (PHOSH_IS_SEARCH_RESULT_FRAME (self)); + + /* TODO */ +} + + +static void +phosh_search_result_frame_class_init (PhoshSearchResultFrameClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = phosh_search_result_frame_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/phosh/ui/search-result-frame.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshSearchResultFrame, items); + gtk_widget_class_bind_template_child (widget_class, PhoshSearchResultFrame, name_lbl); + gtk_widget_class_bind_template_child (widget_class, PhoshSearchResultFrame, extra_lbl); + gtk_widget_class_bind_template_child (widget_class, PhoshSearchResultFrame, icon_img); + + gtk_widget_class_bind_template_callback (widget_class, item_activated); + + gtk_widget_class_set_css_name (widget_class, "phosh-search-result-frame"); +} + + +static void +phosh_search_result_frame_init (PhoshSearchResultFrame *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +/** + * phosh_search_result_frame_new: + * + * Create a #PhoshSearchResultFrame, use + * phosh_search_result_frame_bind_source() to populate it + * + * Returns: (transfer float): a new #PhoshSearchResultFrame + */ +GtkWidget * +phosh_search_result_frame_new (void) +{ + return g_object_new (PHOSH_TYPE_SEARCH_RESULT_FRAME, NULL); +} + + +static GtkWidget * +create_row (gpointer item, gpointer data) +{ + PhoshSearchResult *result = item; + + return phosh_search_result_content_new (result); +} + + +static gboolean +set_name (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + GAppInfo *info = g_value_get_object (from_value); + + g_value_set_string (to_value, g_app_info_get_name (info)); + + return TRUE; +} + + +static gboolean +set_extra (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + PhoshSearchResultFrame *self = user_data; + guint total = g_value_get_uint (from_value); + char *extra = NULL; + int n = total - g_list_model_get_n_items (G_LIST_MODEL (self->source)); + + if (n > 0) { + extra = g_strdup_printf (_("%i more"), n); + + g_value_take_string (to_value, extra); + gtk_widget_show (self->extra_lbl); + } else { + g_value_set_string (to_value, NULL); + gtk_widget_hide (self->extra_lbl); + } + + return TRUE; +} + + +static gboolean +set_icon (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + GAppInfo *info = g_value_get_object (from_value); + + g_value_set_object (to_value, g_app_info_get_icon (info)); + + return TRUE; +} + + +/** + * phosh_search_result_frame_bind_source: + * @self: the #PhoshSearchResultFrame + * @source: the #PhoshSearchResultSource to bind to + * + * Bind @self to a #PhoshSearchResultSource, note @self can be reused for + * different #PhoshSearchResultSources + */ +void +phosh_search_result_frame_bind_source (PhoshSearchResultFrame *self, + PhoshSearchResultSource *source) +{ + g_return_if_fail (PHOSH_IS_SEARCH_RESULT_FRAME (self)); + g_return_if_fail (PHOSH_IS_SEARCH_RESULT_SOURCE (source)); + + g_clear_object (&self->bind_name); + g_clear_object (&self->bind_icon); + g_clear_object (&self->bind_extra); + + g_set_object (&self->source, source); + + gtk_list_box_bind_model (GTK_LIST_BOX (self->items), + G_LIST_MODEL (source), + create_row, + self, + NULL); + + // Bind to the new one + self->bind_name = g_object_bind_property_full (source, "app-info", + self->name_lbl, "label", + G_BINDING_SYNC_CREATE, + set_name, + NULL, + self, + NULL); + + self->bind_extra = g_object_bind_property_full (source, "total-results", + self->extra_lbl, "label", + G_BINDING_SYNC_CREATE, + set_extra, + NULL, + self, + NULL); + + self->bind_icon = g_object_bind_property_full (source, "app-info", + self->icon_img, "gicon", + G_BINDING_SYNC_CREATE, + set_icon, + NULL, + self, + NULL); +} diff --git a/src/search/search-result-frame.h b/src/search/search-result-frame.h new file mode 100644 index 0000000000000000000000000000000000000000..87df248bc770144e51a87d84df2240cae6c502de --- /dev/null +++ b/src/search/search-result-frame.h @@ -0,0 +1,28 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#pragma once + +#include + +#include "search-result-source.h" + +G_BEGIN_DECLS + + +#define PHOSH_TYPE_SEARCH_RESULT_FRAME (phosh_search_result_frame_get_type ()) + + +G_DECLARE_FINAL_TYPE (PhoshSearchResultFrame, phosh_search_result_frame, PHOSH, SEARCH_RESULT_FRAME, GtkListBoxRow) + + +GtkWidget *phosh_search_result_frame_new (void); +void phosh_search_result_frame_bind_source (PhoshSearchResultFrame *self, + PhoshSearchResultSource *source); + +G_END_DECLS diff --git a/src/search/search-result-list.c b/src/search/search-result-list.c new file mode 100644 index 0000000000000000000000000000000000000000..841feb4945e9c87788dfde7c81493bf8d9f29614 --- /dev/null +++ b/src/search/search-result-list.c @@ -0,0 +1,178 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-search-result-list" + +#include "search-result-list.h" +#include "search-result-source.h" + +#include + +/** + * SECTION:search-result-list + * @short_description: A #GListModel containing one or + * more #PhoshSearchResultSource + * @Title: PhoshSearchResultList + * + * A list of #PhoshSearchSource containing #PhoshSearchResults + */ + + +enum { + SIGNAL_EMPTY, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +typedef struct _PhoshSearchResultList { + GObject parent; + + GListStore *list; +} PhoshSearchResultList; + + +static void list_iface_init (GListModelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshSearchResultList, phosh_search_result_list, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_iface_init)) + + +static void +phosh_search_result_list_dispose (GObject *object) +{ + PhoshSearchResultList *self = PHOSH_SEARCH_RESULT_LIST (object); + + g_clear_object (&self->list); + + G_OBJECT_CLASS (phosh_search_result_list_parent_class)->dispose (object); +} + + +static void +phosh_search_result_list_class_init (PhoshSearchResultListClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = phosh_search_result_list_dispose; + + /** + * PhoshSearchResultList::empty: + * @self: the #PhoshSearchResultList + * + * The last item has been removed, @self is not empty + */ + signals[SIGNAL_EMPTY] = g_signal_new ("empty", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); +} + + +static GType +list_get_item_type (GListModel *list) +{ + return PHOSH_TYPE_SEARCH_RESULT_SOURCE; +} + + +static gpointer +list_get_item (GListModel *list, guint position) +{ + PhoshSearchResultList *self = PHOSH_SEARCH_RESULT_LIST (list); + + return g_list_model_get_item (G_LIST_MODEL (self->list), position); +} + + +static unsigned int +list_get_n_items (GListModel *list) +{ + PhoshSearchResultList *self = PHOSH_SEARCH_RESULT_LIST (list); + + return g_list_model_get_n_items (G_LIST_MODEL (self->list)); +} + + +static void +list_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = list_get_item_type; + iface->get_item = list_get_item; + iface->get_n_items = list_get_n_items; +} + + +static void +items_changed (GListModel *list, + guint position, + guint removed, + guint added, + PhoshSearchResultList *self) +{ + g_autoptr (PhoshSearchResultSource) item = NULL; + + g_return_if_fail (PHOSH_IS_SEARCH_RESULT_LIST (self)); + + g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); + + item = g_list_model_get_item (G_LIST_MODEL (self->list), 0); + + if (item == NULL) { + g_signal_emit (self, signals[SIGNAL_EMPTY], 0); + } +} + + +static void +phosh_search_result_list_init (PhoshSearchResultList *self) +{ + self->list = g_list_store_new (PHOSH_TYPE_SEARCH_RESULT_SOURCE); + + g_signal_connect (self->list, + "items-changed", + G_CALLBACK (items_changed), + self); +} + + +/** + * phosh_search_result_list_new: + * + * Creates a new #PhoshSearchResultList that will hold various + * #PhoshSearchResultSources + * + * Returns: (transfer full): a new #PhoshSearchResultList + */ +PhoshSearchResultList * +phosh_search_result_list_new (void) +{ + return g_object_new (PHOSH_TYPE_SEARCH_RESULT_LIST, NULL); +} + + +/** + * phosh_search_result_list_add: + * @self: the #PhoshSearchResultList + * @source: (transfer none): the #PhoshSearchResultSource to add to @self + * + * Adds a #PhoshSearchResultSource to the list in @self + */ +void +phosh_search_result_list_add (PhoshSearchResultList *self, + PhoshSearchResultSource *source) +{ + g_return_if_fail (PHOSH_IS_SEARCH_RESULT_LIST (self)); + + g_list_store_append (self->list, source); +} diff --git a/src/search/search-result-list.h b/src/search/search-result-list.h new file mode 100644 index 0000000000000000000000000000000000000000..9108828410a8ed2d58b7b55082e5f4da285e18cb --- /dev/null +++ b/src/search/search-result-list.h @@ -0,0 +1,28 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#pragma once + +#include + +#include "search-result-source.h" + +G_BEGIN_DECLS + + +#define PHOSH_TYPE_SEARCH_RESULT_LIST (phosh_search_result_list_get_type ()) + + +G_DECLARE_FINAL_TYPE (PhoshSearchResultList, phosh_search_result_list, PHOSH, SEARCH_RESULT_LIST, GObject) + + +PhoshSearchResultList *phosh_search_result_list_new (void); +void phosh_search_result_list_add (PhoshSearchResultList *self, + PhoshSearchResultSource *source); + +G_END_DECLS diff --git a/src/search/search-result-meta.c b/src/search/search-result-meta.c new file mode 100644 index 0000000000000000000000000000000000000000..4fec250a3384c720b3b0c0698ee5a21765aedee3 --- /dev/null +++ b/src/search/search-result-meta.c @@ -0,0 +1,295 @@ +/* + * Copyright © 2019-2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-search-result-meta" + +#include "search-result-meta.h" + + +/** + * SECTION:search-result-meta + * @short_description: Information about a search result + * @Title: PhoshSearchResultMeta + * + * A #PhoshSearchResultMeta is a low level immutable representation of a + * search result + * + * It is a ref-counted ( g_rc_box_alloc0() ) #GBoxed type (rather than a full + * #GObject) as a large number may be created and destroyed during a search, + * when the data is immutable the overhead of #GObject is unnecessary + * + * A #PhoshSearchResultMeta can be serialised to a #GVariant for transport + * over dbus + * + * #PhoshSearchResult provides a #GObject wrapper + */ + + +struct _PhoshSearchResultMeta { + char *id; + char *title; + char *desc; + char *clipboard_text; + GIcon *icon; +}; + + +/** + * phosh_search_result_meta_ref: + * @self: the #PhoshSearchResultMeta + * + * Increase the reference count of @self + * + * Returns: (transfer full): @self + */ +PhoshSearchResultMeta * +phosh_search_result_meta_ref (PhoshSearchResultMeta *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return g_rc_box_acquire (self); +} + + +static void +result_free (gpointer source) +{ + PhoshSearchResultMeta *self = source; + + g_clear_pointer (&self->id, g_free); + g_clear_pointer (&self->title, g_free); + g_clear_pointer (&self->desc, g_free); + g_clear_object (&self->icon); + g_clear_pointer (&self->clipboard_text, g_free); +} + + +/** + * phosh_search_result_meta_unref: + * @self: the #PhoshSearchResultMeta + * + * Decrease the reference count of @self, potentially destroying it + */ +void +phosh_search_result_meta_unref (PhoshSearchResultMeta *self) +{ + g_return_if_fail (self != NULL); + + g_rc_box_release_full (self, result_free); +} + + +G_DEFINE_BOXED_TYPE (PhoshSearchResultMeta, + phosh_search_result_meta, + phosh_search_result_meta_ref, + phosh_search_result_meta_unref) + + +/** + * phosh_search_result_meta_get_id: + * @self: the #PhoshSearchResultMeta + * + * Get the result id, note this *should* be unique withing the source of @self + * but this is not guaranteed + * + * Returns: (transfer none): the result "id" of @self + */ +const char * +phosh_search_result_meta_get_id (PhoshSearchResultMeta *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->id; +} + + +/** + * phosh_search_result_meta_get_title: + * @self: the #PhoshSearchResultMeta + * + * Get the result title + * + * Returns: (transfer none): the title of @self + */ +const char * +phosh_search_result_meta_get_title (PhoshSearchResultMeta *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->title; +} + + +/** + * phosh_search_result_meta_get_description: + * @self: the #PhoshSearchResultMeta + * + * Get the result description, this optional text providing further + * information about @self + * + * Returns: (transfer none) (nullable): the description of @self + */ +const char * +phosh_search_result_meta_get_description (PhoshSearchResultMeta *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->desc; +} + + +/** + * phosh_search_result_meta_get_clipboard_text: + * @self: the #PhoshSearchResultMeta + * + * Get the clipboard text, this is a string that should be sent to the + * clipboard when the user activates @self + * + * NOTE: This is rarely provided + * + * Returns: (transfer none) (nullable): the clipboard text of @self + */ +const char * +phosh_search_result_meta_get_clipboard_text (PhoshSearchResultMeta *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->clipboard_text; +} + + +/** + * phosh_search_result_meta_get_icon: + * @self: the #PhoshSearchResultMeta + * + * Get the icon, this is a small image representing @self (such as an app icon + * or file thumbnail) + * + * Returns: (transfer none) (nullable): the icon of @self + */ +GIcon * +phosh_search_result_meta_get_icon (PhoshSearchResultMeta *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->icon; +} + + +/** + * phosh_search_result_meta_new: + * @id: the result id + * @title: the title + * @desc: (optional): the description + * @icon: (optional): the icon + * @clipboard: (optional): the clipboard text + * + * Construct a new #PhoshSearchResultMeta for the given data + * + * The @icon #GIcon implementation *must* support g_icon_serialize() + * + * Returns: (transfer full): the new #PhoshSearchResultMeta + */ +PhoshSearchResultMeta * +phosh_search_result_meta_new (const char *id, + const char *title, + const char *desc, + GIcon *icon, + const char *clipboard) +{ + PhoshSearchResultMeta *self; + + self = g_rc_box_new0 (PhoshSearchResultMeta); + self->id = g_strdup (id); + self->title = g_strdup (title); + self->desc = g_strdup (desc); + g_set_object (&self->icon, icon); + self->clipboard_text = g_strdup (clipboard); + + return self; +} + + +/** + * phosh_search_result_meta_serialise: + * @self: the #PhoshSearchResultMeta + * + * Generate a #GVariant representation of @self suitable for transpor over + * dbus, the exact format is not defined and may change - the variant should + * only be interpreted with phosh_search_result_meta_deserialise() + * + * Returns: (transfer full): a #GVariant representing @self + */ +GVariant * +phosh_search_result_meta_serialise (PhoshSearchResultMeta *self) +{ + GVariantDict dict; + g_autoptr (GVariant) icon = NULL; + + g_variant_dict_init (&dict, NULL); + + g_variant_dict_insert (&dict, "id", "s", self->id); + g_variant_dict_insert (&dict, "title", "s", self->title); + if (self->desc) { + g_variant_dict_insert (&dict, "desc", "s", self->desc); + } + if (self->clipboard_text) { + g_variant_dict_insert (&dict, "clipboard-text", "s", self->clipboard_text); + } + + if (self->icon) { + icon = g_icon_serialize (self->icon); + if (icon) { + g_variant_dict_insert_value (&dict, "icon", icon); + } else { + g_warning ("Can't serialise icon of type %s", G_OBJECT_TYPE_NAME (self->icon)); + } + } + + return g_variant_dict_end (&dict); +} + + +/** + * phosh_search_result_meta_deserialise: + * @variant: the #GVariant to deserialise + * + * Convert a #GVariant from phosh_search_result_meta_serialise() back to it's + * #PhoshSearchResultMeta, or %NULL for invalid input + * + * Returns: (transfer full) (nullable): the deserialised PhoshSearchResultMeta + * or %NULL on failure + */ +PhoshSearchResultMeta * +phosh_search_result_meta_deserialise (GVariant *variant) +{ + PhoshSearchResultMeta *self = NULL; + g_autofree char *id = NULL; + g_autofree char *title = NULL; + g_autofree char *desc = NULL; + g_autoptr (GVariant) icon_data = NULL; + g_autofree char *clipboard_text = NULL; + g_autoptr (GIcon) icon = NULL; + GVariantDict dict; + + g_variant_dict_init (&dict, variant); + + g_variant_dict_lookup (&dict, "id", "s", &id); + g_variant_dict_lookup (&dict, "title", "s", &title); + g_variant_dict_lookup (&dict, "desc", "s", &desc); + g_variant_dict_lookup (&dict, "clipboard-text", "s", &clipboard_text); + icon_data = g_variant_dict_lookup_value (&dict, "icon", G_VARIANT_TYPE_ANY); + + if (icon_data) { + icon = g_icon_deserialize (icon_data); + } + + self = phosh_search_result_meta_new (id, title, desc, icon, clipboard_text); + + return self; +} diff --git a/src/search/search-result-meta.h b/src/search/search-result-meta.h new file mode 100644 index 0000000000000000000000000000000000000000..b1e5ae0b71178bb0f7fbd985758e0139b56f1de1 --- /dev/null +++ b/src/search/search-result-meta.h @@ -0,0 +1,39 @@ +/* + * Copyright © 2019-2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#include +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SEARCH_RESULT_META (phosh_search_result_meta_get_type ()) + +typedef struct _PhoshSearchResultMeta PhoshSearchResultMeta; + +PhoshSearchResultMeta *phosh_search_result_meta_new (const char *id, + const char *title, + const char *description, + GIcon *icon, + const char *clipboard); +GType phosh_search_result_meta_get_type (void); +PhoshSearchResultMeta *phosh_search_result_meta_ref (PhoshSearchResultMeta *self); +void phosh_search_result_meta_unref (PhoshSearchResultMeta *self); +const char *phosh_search_result_meta_get_id (PhoshSearchResultMeta *self); +const char *phosh_search_result_meta_get_title (PhoshSearchResultMeta *self); +const char *phosh_search_result_meta_get_description (PhoshSearchResultMeta *self); +GIcon *phosh_search_result_meta_get_icon (PhoshSearchResultMeta *self); +const char *phosh_search_result_meta_get_clipboard_text (PhoshSearchResultMeta *self); +GVariant *phosh_search_result_meta_serialise (PhoshSearchResultMeta *self); +PhoshSearchResultMeta *phosh_search_result_meta_deserialise (GVariant *variant); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (PhoshSearchResultMeta, + phosh_search_result_meta_unref) + +G_END_DECLS diff --git a/src/search/search-result-source.c b/src/search/search-result-source.c new file mode 100644 index 0000000000000000000000000000000000000000..4ad3f149d402bfdff1a1ba13981957aa610b9f00 --- /dev/null +++ b/src/search/search-result-source.c @@ -0,0 +1,333 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-search-result-source" + +#include "config.h" +#include "search-result-source.h" +#include "search-result.h" +#include "search-source.h" + +#include + +/** + * SECTION:search-result-source + * @short_description: A #GListModel containing one or more #PhoshSearchResu;t + * @Title: PhoshSearchResultSource + * + * + */ + + +enum { + PROP_0, + PROP_SOURCE, + PROP_ID, + PROP_APP_INFO, + PROP_TOTAL_RESULTS, + LAST_PROP +}; +static GParamSpec *pspecs[LAST_PROP] = { NULL, }; + + +typedef struct _PhoshSearchResultSource { + GObject parent; + + PhoshSearchSource *source; + guint total; + GListStore *list; +} PhoshSearchResultSource; + + +static void list_iface_init (GListModelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshSearchResultSource, phosh_search_result_source, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_iface_init)) + + +static void +phosh_search_result_source_dispose (GObject *object) +{ + PhoshSearchResultSource *self = PHOSH_SEARCH_RESULT_SOURCE (object); + + g_clear_pointer (&self->source, phosh_search_source_unref); + g_clear_object (&self->list); + + G_OBJECT_CLASS (phosh_search_result_source_parent_class)->dispose (object); +} + + +static void +phosh_search_result_source_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshSearchResultSource *self = PHOSH_SEARCH_RESULT_SOURCE (object); + + switch (property_id) { + case PROP_SOURCE: + self->source = g_value_dup_boxed (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_search_result_source_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshSearchResultSource *self = PHOSH_SEARCH_RESULT_SOURCE (object); + + switch (property_id) { + case PROP_SOURCE: + g_value_set_boxed (value, phosh_search_result_source_get_source (self)); + break; + case PROP_ID: + g_value_set_string (value, + phosh_search_result_source_get_id (self)); + break; + case PROP_APP_INFO: + g_value_set_object (value, + phosh_search_result_source_get_app_info (self)); + break; + case PROP_TOTAL_RESULTS: + g_value_set_uint (value, + phosh_search_result_source_get_total_results (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_search_result_source_class_init (PhoshSearchResultSourceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = phosh_search_result_source_dispose; + object_class->set_property = phosh_search_result_source_set_property; + object_class->get_property = phosh_search_result_source_get_property; + + + /** + * PhoshSearchResultSource:source: + * + * The #PhoshSearchSource wrapped by #PhoshSearchResultSource:id and + * #PhoshSearchResultSource:app-info + */ + pspecs[PROP_SOURCE] = + g_param_spec_boxed ("source", "Result source", "Underlying source data", + PHOSH_TYPE_SEARCH_SOURCE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + /** + * PhoshSearchResultSource:id: + * + * See phosh_search_source_get_id() + */ + pspecs[PROP_ID] = + g_param_spec_string ("id", "ID", "Source id", + NULL, + G_PARAM_READABLE); + + /** + * PhoshSearchResultSource:app-info: + * + * See phosh_search_source_get_app_info() + */ + pspecs[PROP_APP_INFO] = + g_param_spec_object ("app-info", "App Info", "Source information", + G_TYPE_APP_INFO, + G_PARAM_READABLE); + + /** + * PhoshSearchResultSource:total-results: + * + * The total number of results from the source, this may be far greater + * than the value returned by g_list_model_get_n_items() and should be used + * to calculate "n more" results + */ + pspecs[PROP_TOTAL_RESULTS] = + g_param_spec_uint ("total-results", + "Total Results", + "Total number of results", + 0, G_MAXUINT, 0, + G_PARAM_READABLE); + + g_object_class_install_properties (object_class, LAST_PROP, pspecs); +} + + +static GType +list_get_item_type (GListModel *list) +{ + return PHOSH_TYPE_SEARCH_RESULT; +} + + +static gpointer +list_get_item (GListModel *list, guint position) +{ + PhoshSearchResultSource *self = PHOSH_SEARCH_RESULT_SOURCE (list); + + return g_list_model_get_item (G_LIST_MODEL (self->list), position); +} + + +static unsigned int +list_get_n_items (GListModel *list) +{ + PhoshSearchResultSource *self = PHOSH_SEARCH_RESULT_SOURCE (list); + + return g_list_model_get_n_items (G_LIST_MODEL (self->list)); +} + + +static void +list_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = list_get_item_type; + iface->get_item = list_get_item; + iface->get_n_items = list_get_n_items; +} + + +static void +items_changed (GListModel *list, + guint position, + guint removed, + guint added, + PhoshSearchResultSource *self) +{ + g_return_if_fail (PHOSH_IS_SEARCH_RESULT_SOURCE (self)); + + g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); +} + + +static void +phosh_search_result_source_init (PhoshSearchResultSource *self) +{ + self->list = g_list_store_new (PHOSH_TYPE_SEARCH_RESULT); + + g_signal_connect (self->list, + "items-changed", + G_CALLBACK (items_changed), + self); +} + + +/** + * phosh_search_result_source_new: + * @source: the underlying #PhoshSearchSource + * + * Creat a #PhoshSearchResultSource for results from a #PhoshSearchSource + * + * Returns: (transfer full): a new #PhoshSearchResultSource + */ +PhoshSearchResultSource * +phosh_search_result_source_new (PhoshSearchSource *source) +{ + return g_object_new (PHOSH_TYPE_SEARCH_RESULT_SOURCE, + "source", source, + NULL); +} + + +/** + * phosh_search_result_source_add: + * @self: the #PhoshSearchResultSource + * @result: (transfer none): the #PhoshSearchResult to add + * + * Add @result to the list of results in @self + */ +void +phosh_search_result_source_add (PhoshSearchResultSource *self, + PhoshSearchResult *result) +{ + g_return_if_fail (PHOSH_IS_SEARCH_RESULT_SOURCE (self)); + + g_list_store_append (self->list, result); +} + + +/** + * phosh_search_result_source_get_source: + * @self: the #PhoshSearchResultSource + * + * Get the #PhoshSearchSource providing data for @self + * + * Returns: (transfer none): the underlying #PhoshSearchSource + */ +PhoshSearchSource * +phosh_search_result_source_get_source (PhoshSearchResultSource *self) +{ + g_return_val_if_fail (PHOSH_IS_SEARCH_RESULT_SOURCE (self), NULL); + + return self->source; +} + + +/** + * phosh_search_result_source_get_id: + * @self: the #PhoshSearchResultSource + * + * See phosh_search_source_get_id() + * + * Returns: (transfer none): the source id + */ +const char * +phosh_search_result_source_get_id (PhoshSearchResultSource *self) +{ + g_return_val_if_fail (PHOSH_IS_SEARCH_RESULT_SOURCE (self), NULL); + + return phosh_search_source_get_id (self->source); +} + + +/** + * phosh_search_result_source_get_app_info: + * @self: the #PhoshSearchResultSource + * + * See phosh_search_source_get_app_info() + * + * Returns: (transfer none): the source #GAppInfo + */ +GAppInfo * +phosh_search_result_source_get_app_info (PhoshSearchResultSource *self) +{ + g_return_val_if_fail (PHOSH_IS_SEARCH_RESULT_SOURCE (self), NULL); + + return phosh_search_source_get_app_info (self->source); +} + + +/** + * phosh_search_result_source_get_total_results: + * @self: the #PhoshSearchResultSource + * + * Get the total number of results the source has vs the number actually + * stored in @self (as reported by g_list_model_get_n_items()) + * + * Returns: the total number of results + */ +guint +phosh_search_result_source_get_total_results (PhoshSearchResultSource *self) +{ + g_return_val_if_fail (PHOSH_IS_SEARCH_RESULT_SOURCE (self), 0); + + return self->total; +} diff --git a/src/search/search-result-source.h b/src/search/search-result-source.h new file mode 100644 index 0000000000000000000000000000000000000000..2791069625ef5f89d67941e00d9722c4a0ff467e --- /dev/null +++ b/src/search/search-result-source.h @@ -0,0 +1,33 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#pragma once + +#include + +#include "search-result.h" +#include "search-source.h" + +G_BEGIN_DECLS + + +#define PHOSH_TYPE_SEARCH_RESULT_SOURCE (phosh_search_result_source_get_type ()) + + +G_DECLARE_FINAL_TYPE (PhoshSearchResultSource, phosh_search_result_source, PHOSH, SEARCH_RESULT_SOURCE, GObject) + + +PhoshSearchResultSource *phosh_search_result_source_new (PhoshSearchSource *source); +void phosh_search_result_source_add (PhoshSearchResultSource *self, + PhoshSearchResult *result); +PhoshSearchSource *phosh_search_result_source_get_source (PhoshSearchResultSource *self); +const char *phosh_search_result_source_get_id (PhoshSearchResultSource *self); +GAppInfo *phosh_search_result_source_get_app_info (PhoshSearchResultSource *self); +guint phosh_search_result_source_get_total_results (PhoshSearchResultSource *self); + +G_END_DECLS diff --git a/src/search/search-result.c b/src/search/search-result.c new file mode 100644 index 0000000000000000000000000000000000000000..e442dce40b29809522213009994344e6b1c5d1e3 --- /dev/null +++ b/src/search/search-result.c @@ -0,0 +1,295 @@ +/* + * Copyright © 2019-2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-search-result" + +#include "search-result.h" +#include "search-result-meta.h" + + +/** + * SECTION:search-result + * @short_description: Information about a search result + * @Title: PhoshSearchResult + * + * A #GObject wrapper around #PhoshSearchResultMeta suitable for use in a + * #GListModel + */ + + +enum { + PROP_0, + PROP_DATA, + PROP_ID, + PROP_TITLE, + PROP_DESCRIPTION, + PROP_ICON, + PROP_CLIPBOARD_TEXT, + LAST_PROP +}; +static GParamSpec *pspecs[LAST_PROP] = { NULL, }; + + +struct _PhoshSearchResult { + GObject parent; + + PhoshSearchResultMeta *data; +}; + + +G_DEFINE_TYPE (PhoshSearchResult, phosh_search_result, G_TYPE_OBJECT) + + +static void +phosh_search_result_dispose (GObject *object) +{ + PhoshSearchResult *self = PHOSH_SEARCH_RESULT (object); + + g_clear_pointer (&self->data, phosh_search_result_meta_unref); + + G_OBJECT_CLASS (phosh_search_result_parent_class)->dispose (object); +} + + +static void +phosh_search_result_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshSearchResult *self = PHOSH_SEARCH_RESULT (object); + + switch (property_id) { + case PROP_DATA: + self->data = g_value_dup_boxed (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_search_result_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshSearchResult *self = PHOSH_SEARCH_RESULT (object); + + switch (property_id) { + case PROP_DATA: + g_value_set_boxed (value, self->data); + break; + case PROP_ID: + g_value_set_string (value, + phosh_search_result_meta_get_id (self->data)); + break; + case PROP_TITLE: + g_value_set_string (value, + phosh_search_result_meta_get_title (self->data)); + break; + case PROP_DESCRIPTION: + g_value_set_string (value, + phosh_search_result_meta_get_description (self->data)); + break; + case PROP_ICON: + g_value_set_object (value, + phosh_search_result_meta_get_icon (self->data)); + break; + case PROP_CLIPBOARD_TEXT: + g_value_set_string (value, + phosh_search_result_meta_get_clipboard_text (self->data)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_search_result_class_init (PhoshSearchResultClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = phosh_search_result_dispose; + object_class->set_property = phosh_search_result_set_property; + object_class->get_property = phosh_search_result_get_property; + + /** + * PhoshSearchResult::data: + * + * The underlying #PhoshSearchResultMeta + */ + pspecs[PROP_DATA] = + g_param_spec_boxed ("data", "Result data", "Underlying result metadata", + PHOSH_TYPE_SEARCH_RESULT_META, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + /** + * PhoshSearchResult::id: + * + * See phosh_search_result_meta_get_id() + */ + pspecs[PROP_ID] = + g_param_spec_string ("id", "ID", "Result id", + NULL, + G_PARAM_READABLE); + + /** + * PhoshSearchResult::title: + * + * See phosh_search_result_meta_get_title() + */ + pspecs[PROP_TITLE] = + g_param_spec_string ("title", "Title", "Result title", + NULL, + G_PARAM_READABLE); + + /** + * PhoshSearchResult::description: + * + * See phosh_search_result_meta_get_description() + */ + pspecs[PROP_DESCRIPTION] = + g_param_spec_string ("description", "Description", "Result description", + NULL, + G_PARAM_READABLE); + + /** + * PhoshSearchResult::icon: + * + * See phosh_search_result_meta_get_description() + */ + pspecs[PROP_ICON] = + g_param_spec_object ("icon", "Icon", "Result icon", + G_TYPE_ICON, + G_PARAM_READABLE); + + /** + * PhoshSearchResult::clipboard-text: + * + * See phosh_search_result_meta_get_clipboard_text() + */ + pspecs[PROP_CLIPBOARD_TEXT] = + g_param_spec_string ("clipboard-text", "Clipboard Text", "Text to copy to clipboard", + NULL, + G_PARAM_READABLE); + + g_object_class_install_properties (object_class, LAST_PROP, pspecs); +} + + +static void +phosh_search_result_init (PhoshSearchResult *self) +{ +} + + +/** + * phosh_search_result_new: + * @data: the #PhoshSearchResultMeta to wrap + * + * Create a new #PhoshSearchResult that wraps @data + * + * Returns: (transfer full): the new #PhoshSearchResult + */ +PhoshSearchResult * +phosh_search_result_new (PhoshSearchResultMeta *data) +{ + return g_object_new (PHOSH_TYPE_SEARCH_RESULT, + "data", data, + NULL); +} + + +/** + * phosh_search_result_get_id: + * @self: the #PhoshSearchResult + * + * See phosh_search_result_meta_get_id() + * + * Returns: (transfer none): the result "id" of @self + */ +const char * +phosh_search_result_get_id (PhoshSearchResult *self) +{ + g_return_val_if_fail (PHOSH_IS_SEARCH_RESULT (self), NULL); + + return phosh_search_result_meta_get_id (self->data); +} + + +/** + * phosh_search_result_get_title: + * @self: the #PhoshSearchResult + * + * See phosh_search_result_meta_get_title() + * + * Returns: (transfer none): the title of @self + */ +const char * +phosh_search_result_get_title (PhoshSearchResult *self) +{ + g_return_val_if_fail (PHOSH_IS_SEARCH_RESULT (self), NULL); + + return phosh_search_result_meta_get_title (self->data); +} + + +/** + * phosh_search_result_get_description: + * @self: the #PhoshSearchResult + * + * See phosh_search_result_get_description() + * + * Returns: (transfer none) (nullable): the description of @self + */ +const char * +phosh_search_result_get_description (PhoshSearchResult *self) +{ + g_return_val_if_fail (PHOSH_IS_SEARCH_RESULT (self), NULL); + + return phosh_search_result_meta_get_description (self->data); +} + + +/** + * phosh_search_result_get_clipboard_text: + * @self: the #PhoshSearchResult + * + * See phosh_search_result_meta_get_clipboard_text() + * + * Returns: (transfer none) (nullable): the clipboard text of @self + */ +const char * +phosh_search_result_get_clipboard_text (PhoshSearchResult *self) +{ + g_return_val_if_fail (PHOSH_IS_SEARCH_RESULT (self), NULL); + + return phosh_search_result_meta_get_clipboard_text (self->data); +} + + +/** + * phosh_search_result_get_icon: + * @self: the #PhoshSearchResult + * + * See phosh_search_result_get_icon() + * + * Returns: (transfer none) (nullable): the icon of @self + */ +GIcon * +phosh_search_result_get_icon (PhoshSearchResult *self) +{ + g_return_val_if_fail (PHOSH_IS_SEARCH_RESULT (self), NULL); + + return phosh_search_result_meta_get_icon (self->data); +} diff --git a/src/search/search-result.h b/src/search/search-result.h new file mode 100644 index 0000000000000000000000000000000000000000..9ed34aed4ed52d36acca30dadd6027650600f36e --- /dev/null +++ b/src/search/search-result.h @@ -0,0 +1,30 @@ +/* + * Copyright © 2019-2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#include + +#include "search-result-meta.h" + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SEARCH_RESULT (phosh_search_result_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshSearchResult, phosh_search_result, PHOSH, SEARCH_RESULT, GObject) + + +PhoshSearchResult *phosh_search_result_new (PhoshSearchResultMeta *data); +const char *phosh_search_result_get_id (PhoshSearchResult *self); +const char *phosh_search_result_get_title (PhoshSearchResult *self); +const char *phosh_search_result_get_description (PhoshSearchResult *self); +GIcon *phosh_search_result_get_icon (PhoshSearchResult *self); +const char *phosh_search_result_get_clipboard_text (PhoshSearchResult *self); + + +G_END_DECLS diff --git a/src/search/search-source.c b/src/search/search-source.c new file mode 100644 index 0000000000000000000000000000000000000000..82cb139d545ecaf3ade8182f12d72a1506c4a77d --- /dev/null +++ b/src/search/search-source.c @@ -0,0 +1,223 @@ +/* + * Copyright © 2019-2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#include + +#include "search-source.h" + + +/** + * SECTION:search-source + * @short_description: Information about a search source + * @Title: PhoshSearchSource + * + * A #PhoshSearchSource is a ref-counted ( g_rc_box_alloc0() ) #GBoxed type + * with information about a search source + */ + + +struct _PhoshSearchSource { + char *id; + GAppInfo *app_info; + guint position; +}; + + +G_DEFINE_BOXED_TYPE (PhoshSearchSource, + phosh_search_source, + phosh_search_source_ref, + phosh_search_source_unref) + + +/** + * phosh_search_source_ref: + * @self: the #PhoshSearchSource + * + * Increase the reference count of @self + * + * Returns: (transfer full): @self + */ +PhoshSearchSource * +phosh_search_source_ref (PhoshSearchSource *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return g_rc_box_acquire (self); +} + + +static void +search_source_free (gpointer source) +{ + PhoshSearchSource *self = source; + + g_clear_pointer (&self->id, g_free); + g_clear_object (&self->app_info); +} + + +/** + * phosh_search_source_unref: + * @self: the #PhoshSearchSource + * + * Decrease the reference count of @self, potentially destroying it + */ +void +phosh_search_source_unref (PhoshSearchSource *self) +{ + g_return_if_fail (self != NULL); + + g_rc_box_release_full (self, search_source_free); +} + + +/** + * phosh_search_source_new: + * @id: the source id (currently the bus path, DO NOT rely on this) + * @app_info: #GAppInfo with information about the source + * + * Creat a new #PhoshSearchSource with the given @app_info and @id + * + * Returns: (transfer full): the new #PhoshSearchSource + */ +PhoshSearchSource * +phosh_search_source_new (const char *id, + GAppInfo *app_info) +{ + PhoshSearchSource *self; + + self = g_rc_box_new0 (PhoshSearchSource); + self->id = g_strdup (id); + g_set_object (&self->app_info, app_info); + self->position = 0; + + return self; +} + + +/** + * phosh_search_source_get_id: + * @self: the #PhoshSearchSource + * + * Get the unique id of the source + * + * Returns: (transfer none): the source id of @self + */ +const char * +phosh_search_source_get_id (PhoshSearchSource *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->id; +} + + +/** + * phosh_search_source_get_app_info: + * @self: the #PhoshSearchSource + * + * Get the #GAppInfo associated with the source, this contains the name + * and #GIcon of the source + * + * Returns: (transfer none): the app info of @self + */ +GAppInfo * +phosh_search_source_get_app_info (PhoshSearchSource *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->app_info; +} + + +/** + * phosh_search_source_get_position: + * @self: the #PhoshSearchSource + * + * Get the sort position of the source + * + * Returns: the sort position of @self + */ +guint +phosh_search_source_get_position (PhoshSearchSource *self) +{ + g_return_val_if_fail (self != NULL, 0); + + return self->position; +} + + +/** + * phosh_search_source_set_position: + * @self: the #PhoshSearchSource + * @position: the sort position of the index + * + * The is only for use in the search daemon + */ +void +phosh_search_source_set_position (PhoshSearchSource *self, + guint position) +{ + g_return_if_fail (self != NULL); + + self->position = position; +} + + +/** + * phosh_search_source_serialise: + * @self: the #PhoshSearchSource + * + * Generate a #GVariant representation of @self suitable for transport over + * dbus, the exact format is not defined and may change - the variant should + * only be interpreted with phosh_search_source_deserialise() + * + * Returns: (transfer full): a #GVariant representing @self + */ +GVariant * +phosh_search_source_serialise (PhoshSearchSource *self) +{ + return g_variant_new ("(ssu)", + self->id, + g_app_info_get_id (self->app_info), + self->position); +} + + +/** + * phosh_search_source_deserialise: + * @variant: the #GVariant to deserialise + * + * Convert a #GVariant from phosh_search_source_serialise() back to it's + * #PhoshSearchSource, or %NULL for invalid input + * + * Returns: (transfer full) (nullable): the deserialised #PhoshSearchSource + * or %NULL on failure + */ +PhoshSearchSource * +phosh_search_source_deserialise (GVariant *variant) +{ + PhoshSearchSource *self = NULL; + g_autofree char *id = NULL; + g_autofree char *app_info_id = NULL; + g_autoptr (GAppInfo) app_info = NULL; + guint position; + + g_variant_get (variant, + "(ssu)", + &id, + &app_info_id, + &position); + + app_info = G_APP_INFO (g_desktop_app_info_new (app_info_id)); + + self = phosh_search_source_new (id, app_info); + phosh_search_source_set_position (self, position); + + return self; +} diff --git a/src/search/search-source.h b/src/search/search-source.h new file mode 100644 index 0000000000000000000000000000000000000000..70ec947ed8cf2b2549dd30616a5aff8e3cebf1ae --- /dev/null +++ b/src/search/search-source.h @@ -0,0 +1,35 @@ +/* + * Copyright © 2019-2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#include +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SEARCH_SOURCE (phosh_search_source_get_type ()) + +typedef struct _PhoshSearchSource PhoshSearchSource; + +PhoshSearchSource *phosh_search_source_new (const char *id, + GAppInfo *app_info); +GType phosh_search_source_get_type (void); +PhoshSearchSource *phosh_search_source_ref (PhoshSearchSource *self); +void phosh_search_source_unref (PhoshSearchSource *self); +const char *phosh_search_source_get_id (PhoshSearchSource *self); +GAppInfo *phosh_search_source_get_app_info (PhoshSearchSource *self); +guint phosh_search_source_get_position (PhoshSearchSource *self); +void phosh_search_source_set_position (PhoshSearchSource *self, + guint position); +GVariant *phosh_search_source_serialise (PhoshSearchSource *self); +PhoshSearchSource *phosh_search_source_deserialise (GVariant *variant); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (PhoshSearchSource, phosh_search_source_unref) + +G_END_DECLS diff --git a/src/stylesheet/common.css b/src/stylesheet/common.css index 442263345f0f0fd4abfc5fdf1ff9b338182fa467..703bd12e81b14460c4f377a95fb305a558850a45 100644 --- a/src/stylesheet/common.css +++ b/src/stylesheet/common.css @@ -1006,3 +1006,38 @@ scale trough slider:active { border-color: shade(@phosh_fg_color, 0.9); background: shade(@phosh_fg_color, 0.9); } + +/* Search */ +phosh-search-result-frame { + border: 1px solid transparent; + border-radius: 6px; + background-color: #282828; + margin: 12px; + box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.33), + 0px 5px 6px 0px rgba(0, 0, 0, 0.07), + 0px 0px 0px 1px rgba(0, 0, 0, 0.2); + -gtk-outline-radius: 6px; +} + +phosh-search-result-frame:focus { + border-color: @theme_selected_bg_color; +} + +phosh-search-result-frame list { + background-color: #282828; +} + +phosh-search-result-content { + border-radius: 6px; + border: 1px solid transparent; + -gtk-outline-radius: 6px; +} + +phosh-search-result-content:hover { + background: rgba(0, 0, 0, 0.2); +} + +phosh-search-result-content:focus { + border-color: @theme_selected_bg_color; +} + diff --git a/src/ui/search-result-app-frame.ui b/src/ui/search-result-app-frame.ui new file mode 100644 index 0000000000000000000000000000000000000000..29d6334055110268ab67cb121981b6d690e9b351 --- /dev/null +++ b/src/ui/search-result-app-frame.ui @@ -0,0 +1,20 @@ + + + + + diff --git a/src/ui/search-result-content.ui b/src/ui/search-result-content.ui new file mode 100644 index 0000000000000000000000000000000000000000..fe8523a04cbaf5febd47b622009f1bcc90fac0d6 --- /dev/null +++ b/src/ui/search-result-content.ui @@ -0,0 +1,63 @@ + + + + + diff --git a/src/ui/search-result-frame.ui b/src/ui/search-result-frame.ui new file mode 100644 index 0000000000000000000000000000000000000000..3f3eff4d377d6d24e628745c6e5f3acb61bdc678 --- /dev/null +++ b/src/ui/search-result-frame.ui @@ -0,0 +1,82 @@ + + + + + diff --git a/tests/meson.build b/tests/meson.build index 1ae27cc46a236df428c6fb0cb11acb64f1190cd0..f017135183205ff558155c43d611800dba056fc8 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -2,6 +2,7 @@ run_phoc_tests = false # Deps for all tests test_stub_deps = files( + 'stubs/evil-icon.c', 'stubs/phosh.c', 'stubs/app-tracker.c', 'stubs/lockscreen-manager.c', @@ -74,7 +75,7 @@ testlib_sources = [ ] testlib = static_library('phoshtest', testlib_sources, - dependencies: [phosh_deps, phosh_tool_dep], + dependencies: [phosh_deps, phosh_tool_dep, phoshsearch_dep], c_args: test_cflags) testlib_dep = declare_dependency( include_directories: include_directories('.'), @@ -85,6 +86,7 @@ tests = [ 'activity', 'app-grid-button', 'app-list-model', + 'app-search', 'connectivity-info', 'css', 'fading-label', @@ -102,6 +104,14 @@ tests = [ 'overview', 'plugin-loader', 'quick-setting', + 'search-result', + 'search-result-app-frame', + 'search-result-content', + 'search-result-frame', + 'search-result-list', + 'search-result-meta', + 'search-result-source', + 'search-source', 'status-icon', 'timestamp-label', 'util', diff --git a/tests/stubs/bad-instance.h b/tests/stubs/bad-instance.h new file mode 100644 index 0000000000000000000000000000000000000000..ca00eecbe36946afa26f84f1014fc37d3e2f77a7 --- /dev/null +++ b/tests/stubs/bad-instance.h @@ -0,0 +1,46 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#include + +#ifdef G_LOG_STRUCTURED +# error G_LOG_STRUCTURED is set, g_test_expect_message is noop +#endif + +/** + * NULL_INSTANCE_CALL: + * @func: the function to call + * @msg: the failed assertion + * @...: arguments to @func + * + * Attempt call @func on %NULL, expecting a log in @G_LOG_DOMAIN + * + * Used to check the call guards in methods + */ +#define NULL_INSTANCE_CALL(func, msg, ...) \ + g_test_expect_message (G_LOG_DOMAIN, \ + G_LOG_LEVEL_CRITICAL, \ + #func ": assertion '" msg "' failed"); \ + func (NULL, ##__VA_ARGS__); + +/** + * NULL_INSTANCE_CALL_RETURN: + * @func: the function to call + * @msg: the failed assertion + * @ret: the return value expected + * @...: arguments to @func + * + * Attempt call @func on %NULL, expecting a log in @G_LOG_DOMAIN + * + * Used to check the call guards in methods + */ +#define NULL_INSTANCE_CALL_RETURN(func, msg, ret, ...) \ + g_test_expect_message (G_LOG_DOMAIN, \ + G_LOG_LEVEL_CRITICAL, \ + #func ": assertion '" msg "' failed"); \ + g_assert_true (func (NULL, ##__VA_ARGS__) == ret); diff --git a/tests/stubs/bad-prop.h b/tests/stubs/bad-prop.h index bc8cdf6fce9d74e78f3f9e298e98e55a6209399b..42aad6fca85f202fb598008e07da4e2df3453691 100644 --- a/tests/stubs/bad-prop.h +++ b/tests/stubs/bad-prop.h @@ -8,6 +8,9 @@ #include +#ifdef G_LOG_STRUCTURED +# error G_LOG_STRUCTURED is set, g_test_expect_message is noop +#endif /** * BAD_PROP_GET: diff --git a/tests/stubs/evil-icon.c b/tests/stubs/evil-icon.c new file mode 100644 index 0000000000000000000000000000000000000000..71d11459df0d78e716611c9af06911657cbadf6e --- /dev/null +++ b/tests/stubs/evil-icon.c @@ -0,0 +1,73 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-evil-icon" + +#include "evil-icon.h" + + +typedef struct _PhoshEvilIcon { + GObject parent; +} PhoshEvilIcon; + + +static void icon_iface_init (GIconIface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshEvilIcon, phosh_evil_icon, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_ICON, icon_iface_init)) + + +static void +phosh_evil_icon_class_init (PhoshEvilIconClass *klass) +{ + +} + + +static guint +phosh_evil_icon_hash (GIcon *self) +{ + return 42; +} + + +static gboolean +phosh_evil_icon_equal (GIcon *self, GIcon *other) +{ + return FALSE; +} + + +static GVariant * +phosh_evil_icon_serialize (GIcon *self) +{ + return NULL; +} + + +static void +icon_iface_init (GIconIface *iface) +{ + iface->hash = phosh_evil_icon_hash; + iface->equal = phosh_evil_icon_equal; + iface->serialize = phosh_evil_icon_serialize; +} + + +static void +phosh_evil_icon_init (PhoshEvilIcon *self) +{ + +} + + +GIcon * +phosh_evil_icon_new (void) +{ + return g_object_new (PHOSH_TYPE_EVIL_ICON, NULL); +} diff --git a/tests/stubs/evil-icon.h b/tests/stubs/evil-icon.h new file mode 100644 index 0000000000000000000000000000000000000000..c47c5807012d1e03924b274c79fa7ac3c0062c46 --- /dev/null +++ b/tests/stubs/evil-icon.h @@ -0,0 +1,23 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_EVIL_ICON (phosh_evil_icon_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshEvilIcon, phosh_evil_icon, PHOSH, EVIL_ICON, GObject) + + +GIcon *phosh_evil_icon_new (void); + + +G_END_DECLS diff --git a/tests/system/share/icons/hicolor/scalable/apps/org.gnome.zbrown.KingsCross.Generic.svg b/tests/system/share/icons/hicolor/scalable/apps/org.gnome.zbrown.KingsCross.Generic.svg new file mode 100644 index 0000000000000000000000000000000000000000..1b6436ba375872fbc8a546b508dd4cb772ceb41e --- /dev/null +++ b/tests/system/share/icons/hicolor/scalable/apps/org.gnome.zbrown.KingsCross.Generic.svg @@ -0,0 +1,241 @@ + + + + + Adwaita Icon Template + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + GNOME Design Team + + + + + Adwaita Icon Template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/system/share/icons/hicolor/symbolic/apps/org.gnome.zbrown.KingsCross.Generic-symbolic.svg b/tests/system/share/icons/hicolor/symbolic/apps/org.gnome.zbrown.KingsCross.Generic-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..debac55601e3d380a8e384f4cbf5ffc49a7db61f --- /dev/null +++ b/tests/system/share/icons/hicolor/symbolic/apps/org.gnome.zbrown.KingsCross.Generic-symbolic.svg @@ -0,0 +1,48 @@ + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + + + + Gnome Symbolic Icon Theme + + + + + + + + + + + + + + + diff --git a/tests/test-app-search.c b/tests/test-app-search.c new file mode 100644 index 0000000000000000000000000000000000000000..065b845d15884bc8663fcda1aaf9a202868353e2 --- /dev/null +++ b/tests/test-app-search.c @@ -0,0 +1,31 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#define TESTING_APP_SEARCH +#include "app-search/app-search.c" + + +static void +test_phosh_app_search_main (void) +{ + int res = real_main (0, NULL); + + g_assert_cmpint (res, ==, 0); +} + + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/phosh/app-search/main", + test_phosh_app_search_main); + + return g_test_run (); +} diff --git a/tests/test-search-result-app-frame.c b/tests/test-search-result-app-frame.c new file mode 100644 index 0000000000000000000000000000000000000000..57226a84b7693bc5d4135f023775ae98604879dc --- /dev/null +++ b/tests/test-search-result-app-frame.c @@ -0,0 +1,189 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#include "search/search-result-app-frame.c" +#include "stubs/bad-instance.h" +#include "stubs/bad-prop.h" + + +static void +test_phosh_search_result_app_frame_new (void) +{ + g_autoptr (PhoshSearchResult) result = NULL; + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (PhoshSearchResultMeta) meta_dest = NULL; + g_autoptr (PhoshSearchResultSource) model = NULL; + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (GIcon) icon = NULL; + g_autoptr (GAppInfo) info = NULL; + GtkWidget *frame = NULL; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("demo.app.Second.desktop", + "Test", + "Result", + icon, + "copy-me"); + g_assert_nonnull (meta); + + result = phosh_search_result_new (meta); + g_assert_nonnull (result); + + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + g_assert_nonnull (info); + + source = phosh_search_source_new ("test", info); + g_assert_nonnull (source); + + model = phosh_search_result_source_new (source); + g_assert_nonnull (model); + + phosh_search_result_source_add (model, result); + + frame = phosh_search_result_app_frame_new (); + g_assert_nonnull (frame); + + gtk_widget_destroy (frame); +} + + +static void +test_phosh_search_result_app_frame_bind (void) +{ + g_autoptr (PhoshSearchResult) result = NULL; + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (PhoshSearchResultMeta) meta_dest = NULL; + g_autoptr (PhoshSearchResultSource) model = NULL; + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (GIcon) icon = NULL; + g_autoptr (GAppInfo) info = NULL; + GtkWidget *frame = NULL; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("demo.app.Second.desktop", + "Test", + "Result", + icon, + "copy-me"); + g_assert_nonnull (meta); + + result = phosh_search_result_new (meta); + g_assert_nonnull (result); + + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + g_assert_nonnull (info); + + source = phosh_search_source_new ("test", info); + g_assert_nonnull (source); + + model = phosh_search_result_source_new (source); + g_assert_nonnull (model); + + phosh_search_result_source_add (model, result); + + frame = phosh_search_result_app_frame_new (); + g_assert_nonnull (frame); + + phosh_search_result_app_frame_bind_source (PHOSH_SEARCH_RESULT_APP_FRAME (frame), + model); + + g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, 1); + + g_clear_object (&model); + + /* Rebind to a new model */ + model = phosh_search_result_source_new (source); + g_assert_nonnull (model); + + phosh_search_result_app_frame_bind_source (PHOSH_SEARCH_RESULT_APP_FRAME (frame), + model); + + g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, 0); + + /* Rebind to invalid model */ + NULL_INSTANCE_CALL (phosh_search_result_app_frame_bind_source, + "PHOSH_IS_SEARCH_RESULT_APP_FRAME (self)", + NULL); + g_test_expect_message (G_LOG_DOMAIN, + G_LOG_LEVEL_CRITICAL, + "phosh_search_result_app_frame_bind_source: assertion 'PHOSH_IS_SEARCH_RESULT_SOURCE (source)' failed"); + phosh_search_result_app_frame_bind_source (PHOSH_SEARCH_RESULT_APP_FRAME (frame), NULL); + + + gtk_widget_destroy (frame); +} + + +static void +test_phosh_search_result_app_frame_activate (void) +{ + g_autoptr (PhoshSearchResult) result = NULL; + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (PhoshSearchResultMeta) meta_dest = NULL; + g_autoptr (PhoshSearchResultSource) model = NULL; + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (GIcon) icon = NULL; + g_autoptr (GAppInfo) info = NULL; + GtkWidget *frame = NULL; + GtkFlowBoxChild *child = NULL; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("demo.app.Second.desktop", + "Test", + "Result", + icon, + "copy-me"); + g_assert_nonnull (meta); + + result = phosh_search_result_new (meta); + g_assert_nonnull (result); + + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + g_assert_nonnull (info); + + source = phosh_search_source_new ("test", info); + g_assert_nonnull (source); + + model = phosh_search_result_source_new (source); + g_assert_nonnull (model); + + phosh_search_result_source_add (model, result); + + frame = phosh_search_result_app_frame_new (); + g_assert_nonnull (frame); + + phosh_search_result_app_frame_bind_source (PHOSH_SEARCH_RESULT_APP_FRAME (frame), + model); + + child = gtk_flow_box_get_child_at_index (GTK_FLOW_BOX (PHOSH_SEARCH_RESULT_APP_FRAME (frame)->apps), 0); + + item_activated (GTK_FLOW_BOX (PHOSH_SEARCH_RESULT_APP_FRAME (frame)->apps), + child, + PHOSH_SEARCH_RESULT_APP_FRAME (frame)); + + gtk_widget_destroy (frame); +} + + +int +main (int argc, char **argv) +{ + gtk_test_init (&argc, &argv, NULL); + + g_test_add_func ("/phosh/search-result-app-frame/new", + test_phosh_search_result_app_frame_new); + g_test_add_func ("/phosh/search-result-app-frame/bind", + test_phosh_search_result_app_frame_bind); + g_test_add_func ("/phosh/search-result-appframe/activate", + test_phosh_search_result_app_frame_activate); + + return g_test_run (); +} diff --git a/tests/test-search-result-content.c b/tests/test-search-result-content.c new file mode 100644 index 0000000000000000000000000000000000000000..03853b8ce1f3a80467e5b6dcb226264098c4f628 --- /dev/null +++ b/tests/test-search-result-content.c @@ -0,0 +1,100 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#include "search/search-result-content.c" +#include "stubs/bad-instance.h" +#include "stubs/bad-prop.h" + + +static void +test_phosh_search_result_content_new (void) +{ + g_autoptr (PhoshSearchResult) result = NULL; + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (PhoshSearchResultMeta) meta_dest = NULL; + g_autoptr (GIcon) icon = NULL; + GtkWidget *content = NULL; + PhoshSearchResult *result_dest = NULL; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("test", + "Test", + "Result", + icon, + "copy-me"); + g_assert_nonnull (meta); + + result = phosh_search_result_new (meta); + g_assert_nonnull (result); + + content = phosh_search_result_content_new (result); + + result_dest = phosh_search_result_content_get_result (PHOSH_SEARCH_RESULT_CONTENT (content)); + g_assert_true (result == result_dest); + + NULL_INSTANCE_CALL_RETURN (phosh_search_result_content_get_result, + "PHOSH_IS_SEARCH_RESULT_CONTENT (self)", + NULL); + NULL_INSTANCE_CALL (phosh_search_result_content_set_result, + "PHOSH_IS_SEARCH_RESULT_CONTENT (self)", + NULL); + g_test_expect_message (G_LOG_DOMAIN, + G_LOG_LEVEL_CRITICAL, + "phosh_search_result_content_set_result: assertion 'PHOSH_IS_SEARCH_RESULT (result)' failed"); + phosh_search_result_content_set_result (PHOSH_SEARCH_RESULT_CONTENT (content), NULL); + + gtk_widget_destroy (content); +} + + +static void +test_phosh_search_result_content_get (void) +{ + g_autoptr (PhoshSearchResult) result = NULL; + g_autoptr (PhoshSearchResult) result_dest = NULL; + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (PhoshSearchResultMeta) meta_dest = NULL; + g_autoptr (GIcon) icon = NULL; + GtkWidget *content = NULL; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("test", + "Test", + "Result", + icon, + "copy-me"); + g_assert_nonnull (meta); + + result = phosh_search_result_new (meta); + g_assert_nonnull (result); + + content = phosh_search_result_content_new (result); + + g_object_get (content, "result", &result_dest, NULL); + g_assert_true (result == result_dest); + + BAD_PROP_SET (content, phosh_search_result_content, PhoshSearchResultContent); + + gtk_widget_destroy (content); +} + + +int +main (int argc, char **argv) +{ + gtk_test_init (&argc, &argv, NULL); + + g_test_add_func ("/phosh/search-result-content/new", + test_phosh_search_result_content_new); + g_test_add_func ("/phosh/search-result-content/get", + test_phosh_search_result_content_get); + + return g_test_run (); +} diff --git a/tests/test-search-result-frame.c b/tests/test-search-result-frame.c new file mode 100644 index 0000000000000000000000000000000000000000..262c8c6a3bde8349b3fd8d36f80a551462bf0559 --- /dev/null +++ b/tests/test-search-result-frame.c @@ -0,0 +1,192 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#include "search/search-result-frame.c" +#include "stubs/bad-instance.h" +#include "stubs/bad-prop.h" + + +static void +test_phosh_search_result_frame_new (void) +{ + g_autoptr (PhoshSearchResult) result = NULL; + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (PhoshSearchResultMeta) meta_dest = NULL; + g_autoptr (PhoshSearchResultSource) model = NULL; + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (GIcon) icon = NULL; + g_autoptr (GAppInfo) info = NULL; + GtkWidget *frame = NULL; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("test", + "Test", + "Result", + icon, + "copy-me"); + g_assert_nonnull (meta); + + result = phosh_search_result_new (meta); + g_assert_nonnull (result); + + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + g_assert_nonnull (info); + + source = phosh_search_source_new ("test", info); + g_assert_nonnull (source); + + model = phosh_search_result_source_new (source); + g_assert_nonnull (model); + + phosh_search_result_source_add (model, result); + + frame = phosh_search_result_frame_new (); + g_assert_nonnull (frame); + + gtk_widget_destroy (frame); +} + + +static void +test_phosh_search_result_frame_bind (void) +{ + g_autoptr (PhoshSearchResult) result = NULL; + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (PhoshSearchResultMeta) meta_dest = NULL; + g_autoptr (PhoshSearchResultSource) model = NULL; + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (GIcon) icon = NULL; + g_autoptr (GAppInfo) info = NULL; + GtkWidget *frame = NULL; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("test", + "Test", + "Result", + icon, + "copy-me"); + g_assert_nonnull (meta); + + result = phosh_search_result_new (meta); + g_assert_nonnull (result); + + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + g_assert_nonnull (info); + + source = phosh_search_source_new ("test", info); + g_assert_nonnull (source); + + model = phosh_search_result_source_new (source); + g_assert_nonnull (model); + + phosh_search_result_source_add (model, result); + + frame = phosh_search_result_frame_new (); + g_assert_nonnull (frame); + + phosh_search_result_frame_bind_source (PHOSH_SEARCH_RESULT_FRAME (frame), + model); + + g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, 1); + g_assert_cmpstr (gtk_label_get_label (GTK_LABEL (PHOSH_SEARCH_RESULT_FRAME (frame)->name_lbl)), + ==, + "Med"); + + g_clear_object (&model); + + /* Rebind to a new model */ + model = phosh_search_result_source_new (source); + g_assert_nonnull (model); + + phosh_search_result_frame_bind_source (PHOSH_SEARCH_RESULT_FRAME (frame), + model); + + g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, 0); + + /* Rebind to invalid model */ + NULL_INSTANCE_CALL (phosh_search_result_frame_bind_source, + "PHOSH_IS_SEARCH_RESULT_FRAME (self)", + NULL); + g_test_expect_message (G_LOG_DOMAIN, + G_LOG_LEVEL_CRITICAL, + "phosh_search_result_frame_bind_source: assertion 'PHOSH_IS_SEARCH_RESULT_SOURCE (source)' failed"); + phosh_search_result_frame_bind_source (PHOSH_SEARCH_RESULT_FRAME (frame), NULL); + + + gtk_widget_destroy (frame); +} + + +static void +test_phosh_search_result_frame_activate (void) +{ + g_autoptr (PhoshSearchResult) result = NULL; + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (PhoshSearchResultMeta) meta_dest = NULL; + g_autoptr (PhoshSearchResultSource) model = NULL; + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (GIcon) icon = NULL; + g_autoptr (GAppInfo) info = NULL; + GtkWidget *frame = NULL; + GtkListBoxRow *row = NULL; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("test", + "Test", + "Result", + icon, + "copy-me"); + g_assert_nonnull (meta); + + result = phosh_search_result_new (meta); + g_assert_nonnull (result); + + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + g_assert_nonnull (info); + + source = phosh_search_source_new ("test", info); + g_assert_nonnull (source); + + model = phosh_search_result_source_new (source); + g_assert_nonnull (model); + + phosh_search_result_source_add (model, result); + + frame = phosh_search_result_frame_new (); + g_assert_nonnull (frame); + + phosh_search_result_frame_bind_source (PHOSH_SEARCH_RESULT_FRAME (frame), + model); + + row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (PHOSH_SEARCH_RESULT_FRAME (frame)->items), 0); + + item_activated (GTK_LIST_BOX (PHOSH_SEARCH_RESULT_FRAME (frame)->items), + row, + PHOSH_SEARCH_RESULT_FRAME (frame)); + + gtk_widget_destroy (frame); +} + + +int +main (int argc, char **argv) +{ + gtk_test_init (&argc, &argv, NULL); + + g_test_add_func ("/phosh/search-result-frame/new", + test_phosh_search_result_frame_new); + g_test_add_func ("/phosh/search-result-frame/bind", + test_phosh_search_result_frame_bind); + g_test_add_func ("/phosh/search-result-frame/activate", + test_phosh_search_result_frame_activate); + + return g_test_run (); +} diff --git a/tests/test-search-result-list.c b/tests/test-search-result-list.c new file mode 100644 index 0000000000000000000000000000000000000000..4a3a04f90b6258897a991435f4e044de86c43b51 --- /dev/null +++ b/tests/test-search-result-list.c @@ -0,0 +1,120 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#include "search/search-result-list.c" +#include "stubs/bad-instance.h" +#include "stubs/bad-prop.h" + + +static void +test_phosh_search_result_list_new (void) +{ + g_autoptr (PhoshSearchResultList) list = NULL; + + list = phosh_search_result_list_new (); + + g_assert_nonnull (list); + g_assert_true (PHOSH_IS_SEARCH_RESULT_LIST (list)); +} + + +static void +test_phosh_search_result_list_null_instance (void) +{ + g_autoptr (PhoshSearchResultList) list = NULL; + + NULL_INSTANCE_CALL (phosh_search_result_list_add, + "PHOSH_IS_SEARCH_RESULT_LIST (self)", + NULL); +} + + +static void +test_phosh_search_result_list_g_list_iface (void) +{ + g_autoptr (PhoshSearchResultList) list = NULL; + + list = phosh_search_result_list_new (); + + g_assert_true (G_IS_LIST_MODEL (list)); + + g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (list)), ==, 0); + g_assert_true (g_list_model_get_item_type (G_LIST_MODEL (list)) == PHOSH_TYPE_SEARCH_RESULT_SOURCE); +} + + +static gboolean emitted = FALSE; + + +static void +changed (GListModel *list, + guint position, + guint removed, + guint added, + gpointer user_data) +{ + emitted = TRUE; + + g_assert_cmpuint (position, ==, 0); + g_assert_cmpuint (removed, ==, 0); + g_assert_cmpuint (added, ==, 1); +} + + +static void +test_phosh_search_result_list_add (void) +{ + g_autoptr (PhoshSearchResultList) list = NULL; + g_autoptr (PhoshSearchResultSource) model = NULL; + g_autoptr (PhoshSearchResultSource) model_dest = NULL; + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (PhoshSearchResult) result = NULL; + g_autoptr (GAppInfo) info = NULL; + + list = phosh_search_result_list_new (); + + g_assert_false (emitted); + g_signal_connect (list, "items-changed", G_CALLBACK (changed), NULL); + + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + g_assert_nonnull (info); + + source = phosh_search_source_new ("test", info); + g_assert_nonnull (source); + + model = phosh_search_result_source_new (source); + g_assert_nonnull (model); + + phosh_search_result_list_add (list, model); + + g_assert_true (emitted); + + g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (list)), ==, 1); + + model_dest = g_list_model_get_item (G_LIST_MODEL (list), 0); + + g_assert_true (model == model_dest); +} + + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/phosh/search-result-source/new", + test_phosh_search_result_list_new); + g_test_add_func ("/phosh/search-result-source/null-instance", + test_phosh_search_result_list_null_instance); + g_test_add_func ("/phosh/search-result-source/g_list_iface", + test_phosh_search_result_list_g_list_iface); + g_test_add_func ("/phosh/search-result-source/add", + test_phosh_search_result_list_add); + + return g_test_run (); +} diff --git a/tests/test-search-result-meta.c b/tests/test-search-result-meta.c new file mode 100644 index 0000000000000000000000000000000000000000..9523a69e56932287839b0385b25ebc2eb919265c --- /dev/null +++ b/tests/test-search-result-meta.c @@ -0,0 +1,371 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +/* We use g_test_expect_message */ +#undef G_LOG_USE_STRUCTURED + +#include "search/search-result-meta.c" +#include "stubs/bad-instance.h" +#include "stubs/evil-icon.h" + + +static void +test_phosh_search_result_meta_new (void) +{ + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (GIcon) icon = NULL; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("test", + "Test", + "Result", + icon, + "copy-me"); + + g_assert_nonnull (meta); + g_assert_cmpstr (phosh_search_result_meta_get_id (meta), ==, "test"); + g_assert_cmpstr (phosh_search_result_meta_get_title (meta), ==, "Test"); + g_assert_cmpstr (phosh_search_result_meta_get_description (meta), ==, "Result"); + g_assert_true (phosh_search_result_meta_get_icon (meta) == icon); + g_assert_cmpstr (phosh_search_result_meta_get_clipboard_text (meta), ==, "copy-me"); +} + + +static void +test_phosh_search_result_meta_refs (void) +{ + PhoshSearchResultMeta *meta; + PhoshSearchResultMeta *meta_ref; + + meta = phosh_search_result_meta_new (NULL, NULL, NULL, NULL, NULL); + + g_assert_nonnull (meta); + + meta_ref = phosh_search_result_meta_ref (meta); + + g_assert_true (meta == meta_ref); + + phosh_search_result_meta_unref (meta); + + g_clear_pointer (&meta, phosh_search_result_meta_unref); + + g_assert_null (meta); + + NULL_INSTANCE_CALL (phosh_search_result_meta_ref, "self != NULL"); + NULL_INSTANCE_CALL (phosh_search_result_meta_unref, "self != NULL"); +} + + +static void +test_phosh_search_result_meta_boxed (void) +{ + PhoshSearchResultMeta *meta; + PhoshSearchResultMeta *meta_ref; + + meta = phosh_search_result_meta_new (NULL, NULL, NULL, NULL, NULL); + + g_assert_nonnull (meta); + + meta_ref = g_boxed_copy (PHOSH_TYPE_SEARCH_RESULT_META, meta); + + g_assert_true (meta == meta_ref); + + g_boxed_free (PHOSH_TYPE_SEARCH_RESULT_META, meta_ref); + + g_clear_pointer (&meta, phosh_search_result_meta_unref); + + g_assert_null (meta); +} + + +static void +test_phosh_search_result_meta_null_instance (void) +{ + g_autoptr (PhoshSearchResultMeta) meta = NULL; + + NULL_INSTANCE_CALL_RETURN (phosh_search_result_meta_get_id, + "self != NULL", + NULL); + NULL_INSTANCE_CALL_RETURN (phosh_search_result_meta_get_title, + "self != NULL", + NULL); + NULL_INSTANCE_CALL_RETURN (phosh_search_result_meta_get_description, + "self != NULL", + NULL); + NULL_INSTANCE_CALL_RETURN (phosh_search_result_meta_get_icon, + "self != NULL", + NULL); + NULL_INSTANCE_CALL_RETURN (phosh_search_result_meta_get_clipboard_text, + "self != NULL", NULL); +} + + +static void +test_phosh_search_result_meta_serialise (void) +{ + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (GIcon) icon = NULL; + g_autoptr (GVariant) variant = NULL; + g_autoptr (GVariant) icon_src = NULL; + g_autoptr (GVariant) icon_res = NULL; + GVariantDict dict; + const char *val; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("test", + "Test", + "Result", + icon, + "copy-me"); + + variant = phosh_search_result_meta_serialise (meta); + + g_assert_nonnull (variant); + g_assert_true (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARDICT)); + + g_variant_dict_init (&dict, variant); + + g_assert_true (g_variant_dict_lookup (&dict, "id", "&s", &val, NULL)); + g_assert_cmpstr (val, ==, "test"); + + g_assert_true (g_variant_dict_lookup (&dict, "title", "&s", &val, NULL)); + g_assert_cmpstr (val, ==, "Test"); + + g_assert_true (g_variant_dict_lookup (&dict, "desc", "&s", &val, NULL)); + g_assert_cmpstr (val, ==, "Result"); + + icon_src = g_icon_serialize (icon); + g_assert_nonnull (icon_src); + + icon_res = g_variant_dict_lookup_value (&dict, "icon", G_VARIANT_TYPE_ANY); + g_assert_nonnull (icon_res); + + g_assert_true (g_variant_equal (icon_src, icon_res)); + + g_assert_true (g_variant_dict_lookup (&dict, "clipboard-text", "&s", &val, NULL)); + g_assert_cmpstr (val, ==, "copy-me"); +} + + +static void +test_phosh_search_result_meta_serialise_no_desc (void) +{ + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (GIcon) icon = NULL; + g_autoptr (GVariant) variant = NULL; + GVariantDict dict; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("test", + "Test", + NULL, + icon, + "copy-me"); + + variant = phosh_search_result_meta_serialise (meta); + + g_assert_nonnull (variant); + g_assert_true (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARDICT)); + + g_variant_dict_init (&dict, variant); + + g_assert_true (g_variant_dict_contains (&dict, "id")); + g_assert_true (g_variant_dict_contains (&dict, "title")); + g_assert_false (g_variant_dict_contains (&dict, "desc")); + g_assert_true (g_variant_dict_contains (&dict, "icon")); + g_assert_true (g_variant_dict_contains (&dict, "clipboard-text")); +} + + +static void +test_phosh_search_result_meta_serialise_no_icon (void) +{ + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (GVariant) variant = NULL; + GVariantDict dict; + + meta = phosh_search_result_meta_new ("test", + "Test", + "Result", + NULL, + "copy-me"); + + variant = phosh_search_result_meta_serialise (meta); + + g_assert_nonnull (variant); + g_assert_true (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARDICT)); + + g_variant_dict_init (&dict, variant); + + g_assert_true (g_variant_dict_contains (&dict, "id")); + g_assert_true (g_variant_dict_contains (&dict, "title")); + g_assert_true (g_variant_dict_contains (&dict, "desc")); + g_assert_false (g_variant_dict_contains (&dict, "icon")); + g_assert_true (g_variant_dict_contains (&dict, "clipboard-text")); +} + + +static void +test_phosh_search_result_meta_serialise_bad_icon (void) +{ + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (GVariant) variant = NULL; + g_autoptr (GIcon) icon = NULL; + GVariantDict dict; + + icon = phosh_evil_icon_new (); + + meta = phosh_search_result_meta_new ("test", + "Test", + "Result", + icon, + "copy-me"); + + g_test_expect_message (G_LOG_DOMAIN, + G_LOG_LEVEL_WARNING, + "Can't serialise icon of type PhoshEvilIcon"); + variant = phosh_search_result_meta_serialise (meta); + + g_assert_nonnull (variant); + g_assert_true (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARDICT)); + + g_variant_dict_init (&dict, variant); + + g_assert_true (g_variant_dict_contains (&dict, "id")); + g_assert_true (g_variant_dict_contains (&dict, "title")); + g_assert_true (g_variant_dict_contains (&dict, "desc")); + g_assert_false (g_variant_dict_contains (&dict, "icon")); + g_assert_true (g_variant_dict_contains (&dict, "clipboard-text")); +} + + +static void +test_phosh_search_result_meta_serialise_no_clipboard (void) +{ + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (GIcon) icon = NULL; + g_autoptr (GVariant) variant = NULL; + GVariantDict dict; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("test", + "Test", + "Result", + icon, + NULL); + + variant = phosh_search_result_meta_serialise (meta); + + g_assert_nonnull (variant); + g_assert_true (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARDICT)); + + g_variant_dict_init (&dict, variant); + + g_assert_true (g_variant_dict_contains (&dict, "id")); + g_assert_true (g_variant_dict_contains (&dict, "title")); + g_assert_true (g_variant_dict_contains (&dict, "desc")); + g_assert_true (g_variant_dict_contains (&dict, "icon")); + g_assert_false (g_variant_dict_contains (&dict, "clipboard-text")); +} + + +static void +test_phosh_search_result_meta_deserialise (void) +{ + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (GVariant) src = NULL; + g_autoptr (GIcon) icon = NULL; + g_autoptr (GVariant) icon_variant = NULL; + GVariantDict dict; + + icon = g_themed_icon_new ("start-here"); + icon_variant = g_icon_serialize (icon); + g_assert_nonnull (icon_variant); + + g_variant_dict_init (&dict, NULL); + g_variant_dict_insert (&dict, "id", "s", "test", NULL); + g_variant_dict_insert (&dict, "title", "s", "Test", NULL); + g_variant_dict_insert (&dict, "desc", "s", "Result", NULL); + g_variant_dict_insert_value (&dict, "icon", icon_variant); + g_variant_dict_insert (&dict, "clipboard-text", "s", "copy-me", NULL); + + src = g_variant_dict_end (&dict); + g_assert_nonnull (src); + + meta = phosh_search_result_meta_deserialise (src); + + g_assert_nonnull (meta); + g_assert_cmpstr (phosh_search_result_meta_get_id (meta), ==, "test"); + g_assert_cmpstr (phosh_search_result_meta_get_title (meta), ==, "Test"); + g_assert_cmpstr (phosh_search_result_meta_get_description (meta), ==, "Result"); + g_assert_true (g_icon_equal (phosh_search_result_meta_get_icon (meta), icon)); + g_assert_cmpstr (phosh_search_result_meta_get_clipboard_text (meta), ==, "copy-me"); +} + + +static void +test_phosh_search_result_meta_deserialise_no_icon (void) +{ + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (GVariant) src = NULL; + GVariantDict dict; + + g_variant_dict_init (&dict, NULL); + g_variant_dict_insert (&dict, "id", "s", "test", NULL); + g_variant_dict_insert (&dict, "title", "s", "Test", NULL); + g_variant_dict_insert (&dict, "desc", "s", "Result", NULL); + g_variant_dict_insert (&dict, "clipboard-text", "s", "copy-me", NULL); + + src = g_variant_dict_end (&dict); + g_assert_nonnull (src); + + meta = phosh_search_result_meta_deserialise (src); + + g_assert_nonnull (meta); + g_assert_nonnull (phosh_search_result_meta_get_id (meta)); + g_assert_nonnull (phosh_search_result_meta_get_title (meta)); + g_assert_nonnull (phosh_search_result_meta_get_description (meta)); + g_assert_null (phosh_search_result_meta_get_icon (meta)); + g_assert_nonnull (phosh_search_result_meta_get_clipboard_text (meta)); +} + + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/phosh/search-result-meta/new", + test_phosh_search_result_meta_new); + g_test_add_func ("/phosh/search-result-meta/refs", + test_phosh_search_result_meta_refs); + g_test_add_func ("/phosh/search-result-meta/boxed", + test_phosh_search_result_meta_boxed); + g_test_add_func ("/phosh/search-result-meta/null-instance", + test_phosh_search_result_meta_null_instance); + g_test_add_func ("/phosh/search-result-meta/serialise", + test_phosh_search_result_meta_serialise); + g_test_add_func ("/phosh/search-result-meta/serialise/no-description", + test_phosh_search_result_meta_serialise_no_desc); + g_test_add_func ("/phosh/search-result-meta/serialise/no-icon", + test_phosh_search_result_meta_serialise_no_icon); + g_test_add_func ("/phosh/search-result-meta/serialise/bad-icon", + test_phosh_search_result_meta_serialise_bad_icon); + g_test_add_func ("/phosh/search-result-meta/serialise/no-clipboard-text", + test_phosh_search_result_meta_serialise_no_clipboard); + g_test_add_func ("/phosh/search-result-meta/deserialise", + test_phosh_search_result_meta_deserialise); + g_test_add_func ("/phosh/search-result-meta/deserialise/no-icon", + test_phosh_search_result_meta_deserialise_no_icon); + + return g_test_run (); +} diff --git a/tests/test-search-result-source.c b/tests/test-search-result-source.c new file mode 100644 index 0000000000000000000000000000000000000000..1ed8f861e8ffe8bc003726b8720e6a1aae05d64e --- /dev/null +++ b/tests/test-search-result-source.c @@ -0,0 +1,216 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#include "search/search-result-source.c" +#include "stubs/bad-instance.h" +#include "stubs/bad-prop.h" + + +static void +test_phosh_search_result_source_new (void) +{ + g_autoptr (PhoshSearchResultSource) model = NULL; + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (PhoshSearchSource) source_dest = NULL; + g_autoptr (GAppInfo) info = NULL; + + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + g_assert_nonnull (info); + + source = phosh_search_source_new ("test", info); + g_assert_nonnull (source); + + model = phosh_search_result_source_new (source); + g_assert_nonnull (model); + + g_object_get (model, "source", &source_dest, NULL); + + g_assert_true (source == source_dest); +} + + +static void +test_phosh_search_result_source_get (void) +{ + g_autoptr (PhoshSearchResultSource) model = NULL; + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (PhoshSearchSource) source_dest = NULL; + g_autoptr (GAppInfo) info = NULL; + g_autoptr (GAppInfo) info_dest = NULL; + g_autofree char *id = NULL; + guint total = 12; + + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + g_assert_nonnull (info); + + source = phosh_search_source_new ("test", info); + g_assert_nonnull (source); + + model = phosh_search_result_source_new (source); + g_assert_nonnull (model); + + g_object_get (model, + "id", &id, + "app-info", &info_dest, + "total-results", &total, + NULL); + + g_assert_cmpstr (id, ==, phosh_search_source_get_id (source)); + g_assert_true (g_app_info_equal (info, info_dest)); + g_assert_cmpuint (total, ==, 0); + + BAD_PROP_SET (model, phosh_search_result_source, PhoshSearchResultSource); +} + + +static void +test_phosh_search_result_source_get_accessors (void) +{ + g_autoptr (PhoshSearchResultSource) model = NULL; + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (GAppInfo) info = NULL; + + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + g_assert_nonnull (info); + + source = phosh_search_source_new ("test", info); + g_assert_nonnull (source); + + model = phosh_search_result_source_new (source); + g_assert_nonnull (model); + + g_assert_true (phosh_search_result_source_get_source (model) == source); + g_assert_cmpstr (phosh_search_result_source_get_id (model), + ==, + phosh_search_source_get_id (source)); + g_assert_true (phosh_search_result_source_get_app_info (model) == info); + g_assert_cmpuint (phosh_search_result_source_get_total_results (model), + ==, + 0); +} + + +static void +test_phosh_search_result_source_null_instance (void) +{ + g_autoptr (PhoshSearchResultSource) source = NULL; + + NULL_INSTANCE_CALL (phosh_search_result_source_add, + "PHOSH_IS_SEARCH_RESULT_SOURCE (self)", + NULL); + NULL_INSTANCE_CALL_RETURN (phosh_search_result_source_get_source, + "PHOSH_IS_SEARCH_RESULT_SOURCE (self)", + NULL); + NULL_INSTANCE_CALL_RETURN (phosh_search_result_source_get_id, + "PHOSH_IS_SEARCH_RESULT_SOURCE (self)", + NULL); + NULL_INSTANCE_CALL_RETURN (phosh_search_result_source_get_app_info, + "PHOSH_IS_SEARCH_RESULT_SOURCE (self)", + NULL); + NULL_INSTANCE_CALL_RETURN (phosh_search_result_source_get_total_results, + "PHOSH_IS_SEARCH_RESULT_SOURCE (self)", + 0); +} + + +static void +test_phosh_search_result_source_g_list_iface (void) +{ + g_autoptr (PhoshSearchResultSource) model = NULL; + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (GAppInfo) info = NULL; + + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + g_assert_nonnull (info); + + source = phosh_search_source_new ("test", info); + g_assert_nonnull (source); + + model = phosh_search_result_source_new (source); + g_assert_nonnull (model); + + g_assert_true (G_IS_LIST_MODEL (model)); + + g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, 0); + g_assert_true (g_list_model_get_item_type (G_LIST_MODEL (model)) == PHOSH_TYPE_SEARCH_RESULT); +} + + +static gboolean emitted = FALSE; + + +static void +changed (GListModel *list, + guint position, + guint removed, + guint added, + gpointer user_data) +{ + emitted = TRUE; + + g_assert_cmpuint (position, ==, 0); + g_assert_cmpuint (removed, ==, 0); + g_assert_cmpuint (added, ==, 1); +} + + +static void +test_phosh_search_result_source_add (void) +{ + g_autoptr (PhoshSearchResultSource) model = NULL; + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (PhoshSearchResult) result = NULL; + g_autoptr (PhoshSearchResult) result_dest = NULL; + g_autoptr (GAppInfo) info = NULL; + + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + g_assert_nonnull (info); + + source = phosh_search_source_new ("test", info); + g_assert_nonnull (source); + + model = phosh_search_result_source_new (source); + g_assert_nonnull (model); + + g_assert_false (emitted); + g_signal_connect (model, "items-changed", G_CALLBACK (changed), NULL); + + result = phosh_search_result_new (NULL); + + phosh_search_result_source_add (model, result); + + g_assert_true (emitted); + + g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, 1); + + result_dest = g_list_model_get_item (G_LIST_MODEL (model), 0); + + g_assert_true (result == result_dest); +} + + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/phosh/search-result-source/new", + test_phosh_search_result_source_new); + g_test_add_func ("/phosh/search-result-source/get", + test_phosh_search_result_source_get); + g_test_add_func ("/phosh/search-result-source/get/accessors", + test_phosh_search_result_source_get_accessors); + g_test_add_func ("/phosh/search-result-source/null-instance", + test_phosh_search_result_source_null_instance); + g_test_add_func ("/phosh/search-result-source/g_list_iface", + test_phosh_search_result_source_g_list_iface); + g_test_add_func ("/phosh/search-result-source/add", + test_phosh_search_result_source_add); + + return g_test_run (); +} diff --git a/tests/test-search-result.c b/tests/test-search-result.c new file mode 100644 index 0000000000000000000000000000000000000000..bdb414582bdf0a1c558e61305ecc0d4aa7472648 --- /dev/null +++ b/tests/test-search-result.c @@ -0,0 +1,159 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#include "search/search-result.c" +#include "stubs/bad-instance.h" +#include "stubs/bad-prop.h" + + +static void +test_phosh_search_result_new (void) +{ + g_autoptr (PhoshSearchResult) result = NULL; + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (PhoshSearchResultMeta) meta_dest = NULL; + g_autoptr (GIcon) icon = NULL; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("test", + "Test", + "Result", + icon, + "copy-me"); + g_assert_nonnull (meta); + + result = phosh_search_result_new (meta); + g_assert_nonnull (result); + + g_object_get (result, "data", &meta_dest, NULL); + + g_assert_true (meta == meta_dest); +} + + +static void +test_phosh_search_result_get (void) +{ + g_autoptr (PhoshSearchResult) result = NULL; + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (GIcon) icon = NULL; + g_autofree char *id = NULL; + g_autofree char *title = NULL; + g_autofree char *desc = NULL; + g_autoptr (GIcon) icon_dest = NULL; + g_autofree char *clipboard_text = NULL; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("test", + "Test", + "Result", + icon, + "copy-me"); + g_assert_nonnull (meta); + + result = phosh_search_result_new (meta); + g_assert_nonnull (result); + + g_object_get (result, + "id", &id, + "title", &title, + "description", &desc, + "icon", &icon_dest, + "clipboard-text", &clipboard_text, + NULL); + + g_assert_cmpstr (id, ==, phosh_search_result_meta_get_id (meta)); + g_assert_cmpstr (title, ==, phosh_search_result_meta_get_title (meta)); + g_assert_cmpstr (desc, ==, phosh_search_result_meta_get_description (meta)); + g_assert_true (g_icon_equal (icon_dest, + phosh_search_result_meta_get_icon (meta))); + g_assert_cmpstr (clipboard_text, + ==, + phosh_search_result_meta_get_clipboard_text (meta)); + + BAD_PROP_SET (result, phosh_search_result, PhoshSearchResult); +} + + +static void +test_phosh_search_result_get_accessors (void) +{ + g_autoptr (PhoshSearchResult) result = NULL; + g_autoptr (PhoshSearchResultMeta) meta = NULL; + g_autoptr (GIcon) icon = NULL; + + icon = g_themed_icon_new ("start-here"); + + meta = phosh_search_result_meta_new ("test", + "Test", + "Result", + icon, + "copy-me"); + g_assert_nonnull (meta); + + result = phosh_search_result_new (meta); + g_assert_nonnull (result); + + g_assert_cmpstr (phosh_search_result_get_id (result), + ==, + phosh_search_result_meta_get_id (meta)); + g_assert_cmpstr (phosh_search_result_get_title (result), + ==, + phosh_search_result_meta_get_title (meta)); + g_assert_cmpstr (phosh_search_result_get_description (result), + ==, + phosh_search_result_meta_get_description (meta)); + g_assert_true (g_icon_equal (phosh_search_result_get_icon (result), + phosh_search_result_meta_get_icon (meta))); + g_assert_cmpstr (phosh_search_result_get_clipboard_text (result), + ==, + phosh_search_result_meta_get_clipboard_text (meta)); +} + + +static void +test_phosh_search_result_null_instance (void) +{ + g_autoptr (PhoshSearchResult) result = NULL; + + NULL_INSTANCE_CALL_RETURN (phosh_search_result_get_id, + "PHOSH_IS_SEARCH_RESULT (self)", + NULL); + NULL_INSTANCE_CALL_RETURN (phosh_search_result_get_title, + "PHOSH_IS_SEARCH_RESULT (self)", + NULL); + NULL_INSTANCE_CALL_RETURN (phosh_search_result_get_description, + "PHOSH_IS_SEARCH_RESULT (self)", + NULL); + NULL_INSTANCE_CALL_RETURN (phosh_search_result_get_icon, + "PHOSH_IS_SEARCH_RESULT (self)", + NULL); + NULL_INSTANCE_CALL_RETURN (phosh_search_result_get_clipboard_text, + "PHOSH_IS_SEARCH_RESULT (self)", + NULL); +} + + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/phosh/search-result/new", + test_phosh_search_result_new); + g_test_add_func ("/phosh/search-result/get", + test_phosh_search_result_get); + g_test_add_func ("/phosh/search-result/get/accessors", + test_phosh_search_result_get_accessors); + g_test_add_func ("/phosh/search-result/null-instance", + test_phosh_search_result_null_instance); + + return g_test_run (); +} diff --git a/tests/test-search-source.c b/tests/test-search-source.c new file mode 100644 index 0000000000000000000000000000000000000000..b30059a28c66f7fe73c6f40ec7333f54f5557cd5 --- /dev/null +++ b/tests/test-search-source.c @@ -0,0 +1,165 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#include "search/search-source.c" +#include "stubs/bad-instance.h" + + +static void +test_phosh_search_source_new (void) +{ + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (GAppInfo) info = NULL; + + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + g_assert_nonnull (info); + + source = phosh_search_source_new ("test", info); + g_assert_nonnull (source); + + g_assert_cmpstr (phosh_search_source_get_id (source), ==, "test"); + g_assert_true (phosh_search_source_get_app_info (source) == info); +} + + +static void +test_phosh_search_source_refs (void) +{ + PhoshSearchSource *source; + PhoshSearchSource *source_ref; + + source = phosh_search_source_new ("test", NULL); + g_assert_nonnull (source); + + source_ref = phosh_search_source_ref (source); + + g_assert_true (source == source_ref); + + phosh_search_source_unref (source); + + g_clear_pointer (&source, phosh_search_source_unref); + + g_assert_null (source); + + NULL_INSTANCE_CALL (phosh_search_source_ref, "self != NULL"); + NULL_INSTANCE_CALL (phosh_search_source_unref, "self != NULL"); +} + + +static void +test_phosh_search_source_boxed (void) +{ + PhoshSearchSource *source; + PhoshSearchSource *source_ref; + + source = phosh_search_source_new ("test", NULL); + g_assert_nonnull (source); + + source_ref = g_boxed_copy (PHOSH_TYPE_SEARCH_SOURCE, source); + + g_assert_true (source == source_ref); + + g_boxed_free (PHOSH_TYPE_SEARCH_SOURCE, source_ref); + + g_clear_pointer (&source, phosh_search_source_unref); + + g_assert_null (source); +} + + +static void +test_phosh_search_source_null_instance (void) +{ + g_autoptr (PhoshSearchSource) source = NULL; + + NULL_INSTANCE_CALL_RETURN (phosh_search_source_get_id, + "self != NULL", + NULL); + NULL_INSTANCE_CALL_RETURN (phosh_search_source_get_app_info, + "self != NULL", + NULL); +} + + +static void +test_phosh_search_source_serialise (void) +{ + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (GAppInfo) info = NULL; + g_autoptr (GVariant) variant = NULL; + g_autofree char *id = NULL; + g_autofree char *app_info_id = NULL; + g_autoptr (GAppInfo) app_info = NULL; + guint position; + + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + g_assert_nonnull (info); + + source = phosh_search_source_new ("test", info); + g_assert_nonnull (source); + + phosh_search_source_set_position (source, 42); + + variant = phosh_search_source_serialise (source); + + g_assert_nonnull (source); + g_assert_true (g_variant_is_of_type (variant, G_VARIANT_TYPE_TUPLE)); + g_assert_true (g_variant_is_of_type (variant, G_VARIANT_TYPE ("(ssu)"))); + + g_variant_get (variant, + "(ssu)", + &id, + &app_info_id, + &position); + + g_assert_cmpstr (id, ==, "test"); + g_assert_cmpstr (app_info_id, ==, "demo.app.Second.desktop"); + g_assert_cmpuint (position, ==, 42); +} + + +static void +test_phosh_search_source_deserialise (void) +{ + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (GVariant) src = NULL; + GAppInfo *info = NULL; + + src = g_variant_new ("(ssu)", "test", "demo.app.Second.desktop", 42); + g_assert_nonnull (src); + + source = phosh_search_source_deserialise (src); + + g_assert_nonnull (source); + g_assert_cmpstr (phosh_search_source_get_id (source), ==, "test"); + info = phosh_search_source_get_app_info (source); + g_assert_cmpstr (g_app_info_get_id (info), ==, "demo.app.Second.desktop"); + g_assert_cmpuint (phosh_search_source_get_position (source), ==, 42); +} + + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/phosh/search-source/new", + test_phosh_search_source_new); + g_test_add_func ("/phosh/search-source/refs", + test_phosh_search_source_refs); + g_test_add_func ("/phosh/search-source/boxed", + test_phosh_search_source_boxed); + g_test_add_func ("/phosh/search-source/null-instance", + test_phosh_search_source_null_instance); + g_test_add_func ("/phosh/search-source/serialise", + test_phosh_search_source_serialise); + g_test_add_func ("/phosh/search-source/deserialise", + test_phosh_search_source_deserialise); + + return g_test_run (); +} diff --git a/tests/user/share/icons/hicolor/scalable/apps/org.gnome.zbrown.Med.svg b/tests/user/share/icons/hicolor/scalable/apps/org.gnome.zbrown.Med.svg new file mode 100644 index 0000000000000000000000000000000000000000..39c8c752c59811bc25190f7aa3a6dabb5ed21b87 --- /dev/null +++ b/tests/user/share/icons/hicolor/scalable/apps/org.gnome.zbrown.Med.svg @@ -0,0 +1,413 @@ + + + + Adwaita Icon Template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + GNOME Design Team + + + + + Adwaita Icon Template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/user/share/icons/hicolor/symbolic/apps/org.gnome.zbrown.Med-symbolic.svg b/tests/user/share/icons/hicolor/symbolic/apps/org.gnome.zbrown.Med-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..a3c9337e16703f9fdb77eb7c429ffd42a1625db7 --- /dev/null +++ b/tests/user/share/icons/hicolor/symbolic/apps/org.gnome.zbrown.Med-symbolic.svg @@ -0,0 +1,162 @@ + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + + + + Gnome Symbolic Icon Theme + + + + + + + + + + + + + + + + + + diff --git a/tools/app-grid-standalone.c b/tools/app-grid-standalone.c index a59ab90dadae9eac6c7951c530a355142afe2bc9..b6b6e894ac7fde5b8b3bd3cecdd86d1488bfb638 100644 --- a/tools/app-grid-standalone.c +++ b/tools/app-grid-standalone.c @@ -10,6 +10,7 @@ #include + static void css_setup (void) { @@ -32,6 +33,7 @@ css_setup (void) g_object_unref (file); } + int main (int argc, char *argv[]) { @@ -47,7 +49,9 @@ main (int argc, char *argv[]) NULL); win = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_default_size (GTK_WINDOW (win), 360, 720); g_signal_connect (win, "delete_event", G_CALLBACK (gtk_main_quit), NULL); + gtk_window_set_default_size (GTK_WINDOW (win), 360, 720); gtk_widget_show (win); diff --git a/tools/meson.build b/tools/meson.build index fec4c27f6ab58b25fcd2a16d17deaf602c9161f3..7cbafd679707dadbedb87d8d694fda6fa05aec36 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -31,7 +31,7 @@ if get_option('tools') dependencies: phosh_tool_dep) executable('app-grid-standalone', ['app-grid-standalone.c'] + test_stub_deps, - dependencies: phosh_tool_dep) + dependencies: [phosh_tool_dep, phoshsearch_dep]) executable('image-notify', ['image-notify.c'] + test_stub_deps, dependencies: phosh_tool_dep) @@ -52,4 +52,11 @@ if get_option('tools') ], dependencies: phosh_tool_dep) endif + + executable('search', ['search.c'] + test_stub_deps, + dependencies: [phosh_tool_dep, phoshsearch_dep]) + + executable('search-view', ['search-view.c'] + test_stub_deps, + dependencies: phosh_tool_dep) + endif diff --git a/tools/run_tool.in b/tools/run_tool.in index 48af764faf55eaf3c8b9395947ff8686906254e7..dc144f4096ede1e2987039d87f8b1e50a3583c87 100755 --- a/tools/run_tool.in +++ b/tools/run_tool.in @@ -2,11 +2,17 @@ set -e ABS_BUILDDIR='@ABS_BUILDDIR@' +ABS_SRCDIR='@ABS_SRCDIR@' GSETTINGS_SCHEMA_DIR="${ABS_BUILDDIR}/data:${ABS_BUILDDIR}/plugins/ticket-box:${GSETTINGS_SCHEMA_DIR}" export GSETTINGS_SCHEMA_DIR export G_MESSAGES_DEBUG=all export GNOTIFICATION_BACKEND=freedesktop +export XDG_CONFIG_HOME="${ABS_SRCDIR}/tests/user/config/" +export XDG_CONFIG_DIRS="${ABS_SRCDIR}/tests/system/config/" +export XDG_DATA_HOME="${ABS_SRCDIR}/tests/user/share/" +export XDG_DATA_DIRS="${ABS_SRCDIR}/tests/system/share/":$XDG_DATA_DIRS + set -x exec $@ diff --git a/tools/search-view.c b/tools/search-view.c new file mode 100644 index 0000000000000000000000000000000000000000..65d1466d948b9b7f1e20e3f0115ab15ec173976a --- /dev/null +++ b/tools/search-view.c @@ -0,0 +1,144 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3-or-later + * + * Author: Zander Brown + */ + +#include +#include +#include + + +static void +css_setup (void) +{ + GtkCssProvider *provider; + g_autoptr (GFile) file = NULL; + g_autoptr (GError) error = NULL; + + provider = gtk_css_provider_new (); + file = g_file_new_for_uri ("resource:///sm/puri/phosh/stylesheet/adwaita-dark.css"); + + if (!gtk_css_provider_load_from_file (provider, file, &error)) { + g_warning ("Failed to load CSS file: %s", error->message); + return; + } + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), 600); +} + + +static GtkWidget * +create_frame (gpointer item, gpointer data) +{ + PhoshSearchResultSource *source = item; + const char *source_id = NULL; + GtkWidget *frame; + + source_id = phosh_search_result_source_get_id (source); + + if (G_UNLIKELY (g_strcmp0 (source_id, "/search/app") == 0)) { + frame = phosh_search_result_app_frame_new (); + phosh_search_result_app_frame_bind_source (PHOSH_SEARCH_RESULT_APP_FRAME (frame), + source); + + return frame; + } + + frame = phosh_search_result_frame_new (); + phosh_search_result_frame_bind_source (PHOSH_SEARCH_RESULT_FRAME (frame), + source); + + + return frame; +} + + +int +main (int argc, char **argv) +{ + g_autoptr (PhoshSearchResultList) list = NULL; + g_autoptr (PhoshSearchSource) app_source_meta = NULL; + g_autoptr (PhoshSearchResultSource) app_source = NULL; + g_autoptr (PhoshSearchSource) source_meta = NULL; + g_autoptr (PhoshSearchResultSource) source = NULL; + g_autoptr (PhoshSearchResultMeta) app_res_1_meta = NULL; + g_autoptr (PhoshSearchResult) app_res_1 = NULL; + g_autoptr (PhoshSearchResultMeta) app_res_2_meta = NULL; + g_autoptr (PhoshSearchResult) app_res_2 = NULL; + g_autoptr (PhoshSearchResultMeta) res_meta = NULL; + g_autoptr (PhoshSearchResult) res = NULL; + g_autoptr (GIcon) icon = NULL; + g_autoptr (GAppInfo) info = NULL; + GtkWidget *win; + GtkWidget *listbox; + + gtk_init (&argc, &argv); + + css_setup (); + + g_object_set (gtk_settings_get_default (), + "gtk-application-prefer-dark-theme", TRUE, + NULL); + + icon = g_themed_icon_new ("help-about"); + info = G_APP_INFO (g_desktop_app_info_new ("demo.app.Second.desktop")); + + list = phosh_search_result_list_new (); + + app_source_meta = phosh_search_source_new ("/search/app", info); + app_source = phosh_search_result_source_new (app_source_meta); + phosh_search_result_list_add (list, app_source); + + app_res_1_meta = phosh_search_result_meta_new ("demo.app.First.desktop", + "First", + NULL, + NULL, + NULL); + app_res_1 = phosh_search_result_new (app_res_1_meta); + phosh_search_result_source_add (app_source, app_res_1); + + app_res_2_meta = phosh_search_result_meta_new ("demo.app.Second.desktop", + "Second", + NULL, + NULL, + NULL); + app_res_2 = phosh_search_result_new (app_res_2_meta); + phosh_search_result_source_add (app_source, app_res_2); + + + source_meta = phosh_search_source_new ("/search/something", info); + source = phosh_search_result_source_new (source_meta); + phosh_search_result_list_add (list, source); + + res_meta = phosh_search_result_meta_new ("demo.app.First.desktop", + "Some result", + "for the current search", + icon, + NULL); + res = phosh_search_result_new (res_meta); + phosh_search_result_source_add (source, res); + + + win = gtk_window_new (GTK_WINDOW_TOPLEVEL); + g_signal_connect (win, "delete_event", G_CALLBACK (gtk_main_quit), NULL); + gtk_window_set_default_size (GTK_WINDOW (win), 360, 720); + + listbox = gtk_list_box_new (); + gtk_widget_show (listbox); + gtk_container_add (GTK_CONTAINER (win), listbox); + + gtk_list_box_bind_model (GTK_LIST_BOX (listbox), + G_LIST_MODEL (list), + create_frame, + NULL, + NULL); + + gtk_widget_show (win); + + gtk_main (); + + return 0; +} diff --git a/tools/search.c b/tools/search.c new file mode 100644 index 0000000000000000000000000000000000000000..5ec3c9962bd7fe57713c41ded72e9d0ce03a3cca --- /dev/null +++ b/tools/search.c @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0+ + * + * Author: Zander Brown + */ + +#include "search-client.h" +#include "search-result-meta.h" +#include "phosh-searchd.h" + + +static void +results (PhoshSearchClient *client, + const char *source_id, + GPtrArray *results) +{ + g_print ("%s\n", source_id); + + for (int i = 0; i < results->len; i++) { + PhoshSearchResultMeta *result = g_ptr_array_index (results, i); + + g_print (" - %s\n", phosh_search_result_meta_get_id (result)); + g_print (" name: %s\n", phosh_search_client_markup_string (client, phosh_search_result_meta_get_title (result))); + g_print (" desc: %s\n", phosh_search_client_markup_string (client, phosh_search_result_meta_get_description (result))); + if (phosh_search_result_meta_get_icon (result)) { + g_print (" gi-t: %s\n", G_OBJECT_TYPE_NAME (phosh_search_result_meta_get_icon (result))); + } + if (phosh_search_result_meta_get_clipboard_text (result)) { + g_print (" cp-t: %s\n", phosh_search_result_meta_get_clipboard_text (result)); + } + } + + g_print ("\n"); +} + + +static void +got_client (GObject *source, + GAsyncResult *result, + gpointer term) +{ + g_autoptr (GError) error = NULL; + g_autoptr (PhoshSearchClient) client = NULL; + + client = phosh_search_client_new_finish (source, result, &error); + + g_signal_connect (client, "source-results-changed", G_CALLBACK (results), NULL); + + phosh_search_client_query (client, (const char *)term); +} + + +int +main (int argc, char**argv) +{ + g_autoptr (GMainLoop) loop = NULL; + g_autoptr (PhoshSearchDBusSearch) search = NULL; + const char *term = argc > 1 ? argv[1] : "test"; + + g_print ("Searching for '%s'\n", term); + phosh_search_client_new (NULL, got_client, (gpointer)term); + + loop = g_main_loop_new (NULL, FALSE); + g_main_loop_run (loop); + + return 0; +}