diff --git a/src/Backend/App.vala b/src/Backend/App.vala index e312d4afd..7b10ab8b3 100644 --- a/src/Backend/App.vala +++ b/src/Backend/App.vala @@ -34,7 +34,7 @@ public class Slingshot.Backend.App : Object { public string exec { get; private set; } public string[] keywords { get; private set;} public Icon icon { get; private set; default = new ThemedIcon ("application-default-icon"); } - public double popularity { get; set; } + public int popularity { get; set; } public string desktop_path { get; private set; } public string categories { get; private set; } public string generic_name { get; private set; default = ""; } diff --git a/src/Backend/AppSystem.vala b/src/Backend/AppSystem.vala index 49950c1bc..de9dc79e8 100644 --- a/src/Backend/AppSystem.vala +++ b/src/Backend/AppSystem.vala @@ -63,13 +63,19 @@ public class Slingshot.Backend.AppSystem : Object { private void update_app_system () { debug ("Updating Applications menu tree…"); #if HAVE_ZEITGEIST - rl_service.refresh_popularity (); + rl_service.refresh_popularity.begin (); #endif update_categories_index (); changed (); } + public async void update_popularities () { +#if HAVE_ZEITGEIST + yield rl_service.refresh_popularity (); +#endif + } + private void update_categories_index () { categories_cache.clear (); @@ -220,4 +226,30 @@ public class Slingshot.Backend.AppSystem : Object { private static int sort_apps_by_name (Backend.App a, Backend.App b) { return a.name.collate (b.name); } + +#if HAVE_ZEITGEIST + public SList get_apps_by_popularity () { + var sorted_apps = new SList (); + string[] sorted_apps_execs = {}; + + foreach (Gee.ArrayList category in apps.values) { + foreach (App app in category) { + if (!(app.exec in sorted_apps_execs)) { + sorted_apps.insert_sorted_with_data (app, sort_apps_by_popularity); + sorted_apps_execs += app.exec; + } + } + } + + return sorted_apps; + } + + private static int sort_apps_by_popularity (Backend.App a, Backend.App b) { + if (a.popularity == b.popularity) { + return a.name.collate (b.name); + } + + return a.popularity > b.popularity ? 1 : -1; + } +#endif } diff --git a/src/Backend/RelevancyService.vala b/src/Backend/RelevancyService.vala index 364962f63..36882d0fc 100644 --- a/src/Backend/RelevancyService.vala +++ b/src/Backend/RelevancyService.vala @@ -26,8 +26,6 @@ public class Slingshot.Backend.RelevancyService : Object { private bool has_datahub_gio_module = false; private bool refreshing = false; - private const float MULTIPLIER = 65535.0f; - public signal void update_complete (); public RelevancyService () { @@ -35,10 +33,13 @@ public class Slingshot.Backend.RelevancyService : Object { zg_log = new Zeitgeist.Log (); app_popularity = new Gee.HashMap (); - refresh_popularity (); + refresh_popularity.begin (); check_data_sources.begin (); - Timeout.add_seconds (60*30, refresh_popularity); + Timeout.add_seconds (60*30, () => { + refresh_popularity.begin (); + return Source.CONTINUE; + }); } @@ -64,12 +65,12 @@ public class Slingshot.Backend.RelevancyService : Object { } } - public bool refresh_popularity () { - - load_application_relevancies.begin (); + public async bool refresh_popularity () { + yield load_application_relevancies (); return true; } + private void reload_relevancies () { Idle.add_full (Priority.LOW, () => { @@ -104,7 +105,6 @@ public class Slingshot.Backend.RelevancyService : Object { var ptr_arr = new GLib.GenericArray (); ptr_arr.add (event); - try { Zeitgeist.ResultSet rs = yield zg_log.find_events (tr, ptr_arr, Zeitgeist.StorageState.ANY, @@ -119,13 +119,15 @@ public class Slingshot.Backend.RelevancyService : Object { // Zeitgeist (0.6) doesn't have any stats API, so let's approximate foreach (Zeitgeist.Event e in rs) { + if (e.num_subjects () <= 0) { + continue; + } - if (e.num_subjects () <= 0) continue; Zeitgeist.Subject s = e.get_subject (0); - float power = index / (size * 2) + 0.5f; // linearly <0.5, 1.0> - float relevancy = 1.0f / Math.powf (index + 1, power); - app_popularity[s.uri] = (int)(relevancy * MULTIPLIER); + // Simply use the (inverted) order of the app in the result set as + // measure of popularity for now. Suffices for the applications menu. + app_popularity[s.uri] = (int) (size - index); index++; } update_complete (); @@ -137,15 +139,15 @@ public class Slingshot.Backend.RelevancyService : Object { } } - public float get_app_popularity (string desktop_id) { - + public int get_app_popularity (string desktop_id) { var id = "application://" + desktop_id; if (app_popularity.has_key (id)) { - return app_popularity[id] / MULTIPLIER; + // return app_popularity[id] / MULTIPLIER; + return app_popularity[id]; } - return 0.0f; + return 0; } public void app_launched (App app) { diff --git a/src/Indicator.vala b/src/Indicator.vala index 4aa2843f3..218c1a06c 100644 --- a/src/Indicator.vala +++ b/src/Indicator.vala @@ -19,6 +19,7 @@ public class Slingshot.Indicator : Wingpanel.Indicator { private const string KEYBINDING_SCHEMA = "io.elementary.desktop.wm.keybindings"; private const string GALA_BEHAVIOR_SCHEMA = "io.elementary.desktop.wm.behavior"; + private const string DOCK_SCHEMA = "io.elementary.dock"; private DBusService? dbus_service = null; private Gtk.Grid? indicator_grid = null; @@ -26,6 +27,7 @@ public class Slingshot.Indicator : Wingpanel.Indicator { private static GLib.Settings? keybinding_settings; private static GLib.Settings? gala_behavior_settings; + private static GLib.Settings? dock_settings; public Indicator () { Object (code_name: Wingpanel.Indicator.APP_LAUNCHER); @@ -39,6 +41,10 @@ public class Slingshot.Indicator : Wingpanel.Indicator { if (SettingsSchemaSource.get_default ().lookup (GALA_BEHAVIOR_SCHEMA, true) != null) { gala_behavior_settings = new GLib.Settings (GALA_BEHAVIOR_SCHEMA); } + + if (SettingsSchemaSource.get_default ().lookup (DOCK_SCHEMA, true) != null) { + dock_settings = new GLib.Settings (DOCK_SCHEMA); + } } construct { @@ -54,19 +60,6 @@ public class Slingshot.Indicator : Wingpanel.Indicator { } public override Gtk.Widget? get_widget () { - if (view == null) { - view = new SlingshotView (); - - unowned var unity_client = Unity.get_default (); - unity_client.add_client (view); - - view.close_indicator.connect (on_close_indicator); - - if (dbus_service == null) { - dbus_service = new DBusService (view); - } - } - return view; } @@ -101,12 +94,42 @@ public class Slingshot.Indicator : Wingpanel.Indicator { visible = true; + // Create the view now so that there is time for app popularities to initialise + // before the view is shown for the first time. + if (view == null) { + view = new SlingshotView (); + + unowned var unity_client = Unity.get_default (); + unity_client.add_client (view); + + view.close_indicator.connect (on_close_indicator); + + if (dbus_service == null) { + dbus_service = new DBusService (view); + } + + if (dock_settings != null) { + dock_settings.changed.connect ((key) => { + if (key == "launchers") { + view.update (dock_settings.get_strv ("launchers")); + } + }); + } + } + + // Wait for AppSystem to initialize + Idle.add (() => { + view.update (dock_settings != null ? dock_settings.get_strv ("launchers") : null); + return Source.REMOVE; + }); + return indicator_grid; } public override void opened () { - if (view != null) + if (view != null) { view.show_slingshot (); + } } public override void closed () { diff --git a/src/SlingshotView.vala b/src/SlingshotView.vala index d490acc93..436c00423 100644 --- a/src/SlingshotView.vala +++ b/src/SlingshotView.vala @@ -159,11 +159,14 @@ public class Slingshot.SlingshotView : Gtk.Grid, UnityClient { // Auto-update applications grid app_system.changed.connect (() => { grid_view.populate (app_system); - - category_view.setup_sidebar (); + category_view.update (null); }); } + public void update (string[] pinned) { + category_view.update (pinned); + } + public void update_launcher_entry (string sender_name, GLib.Variant parameters, bool is_retry = false) { if (!is_retry) { // Wait to let further update requests come in to catch the case where one application @@ -323,6 +326,7 @@ public class Slingshot.SlingshotView : Gtk.Grid, UnityClient { public void show_slingshot () { search_entry.text = ""; + category_view.update (null); /* TODO set_focus (null); diff --git a/src/Views/CategoryView.vala b/src/Views/CategoryView.vala index 04c97828c..bd93ab477 100644 --- a/src/Views/CategoryView.vala +++ b/src/Views/CategoryView.vala @@ -19,13 +19,19 @@ public class Slingshot.Widgets.CategoryView : Gtk.EventBox { public signal void search_focus_request (); + public const string PINNED_CATEGORY = N_("Pinned"); + public const string RECENT_CATEGORY = N_("Recent"); + public const double MIN_POPULARITY = 1; + public SlingshotView view { get; construct; } private bool dragging = false; private string? drag_uri = null; private NavListBox category_switcher; private NavListBox listbox; - + private Gee.ArrayList pinned; + private bool show_pinned = true; + private Gee.ArrayList popular_apps; private const Gtk.TargetEntry DND = { "text/uri-list", 0, 0 }; public CategoryView (SlingshotView view) { @@ -36,6 +42,11 @@ public class Slingshot.Widgets.CategoryView : Gtk.EventBox { set_visible_window (false); hexpand = true; + popular_apps = new Gee.ArrayList (); + pinned = new Gee.ArrayList (); +#if HAVE_ZEITGEIST + show_pinned = false; +#endif category_switcher = new NavListBox (); category_switcher.selection_mode = Gtk.SelectionMode.BROWSE; category_switcher.set_sort_func ((Gtk.ListBoxSortFunc) category_sort_func); @@ -65,7 +76,12 @@ public class Slingshot.Widgets.CategoryView : Gtk.EventBox { add (container); - category_switcher.row_selected.connect (() => { + category_switcher.row_selected.connect ((row) => { + if (row != null && ((CategoryRow) row).cat_name == _(RECENT_CATEGORY)) { + listbox.set_sort_func ((Gtk.ListBoxSortFunc) recent_sort_func); + } else { + listbox.set_sort_func (null); + } listbox.invalidate_filter (); }); @@ -121,7 +137,7 @@ public class Slingshot.Widgets.CategoryView : Gtk.EventBox { var drag_item = (AppListRow) selected_row; drag_uri = "file://" + drag_item.desktop_path; if (drag_uri != null) { - Gtk.drag_set_icon_gicon (ctx, drag_item.app_info.get_icon (), 32, 32); + Gtk.drag_set_icon_gicon (ctx, drag_item.icon, 32, 32); } view.close_indicator (); @@ -151,9 +167,25 @@ public class Slingshot.Widgets.CategoryView : Gtk.EventBox { } private static int category_sort_func (CategoryRow row1, CategoryRow row2) { + if (row1.cat_name == _(RECENT_CATEGORY)) { + return -1; + } + + if (row1.cat_name == _(PINNED_CATEGORY)) { + return row2.cat_name != _(RECENT_CATEGORY) ? -1 : 1; + } + return row1.cat_name.collate (row2.cat_name); } + private static int recent_sort_func (AppListRow row1, AppListRow row2) { + if (row1.popularity == row2.popularity) { + return row1.display_name.collate (row2.display_name); + } + + return row1.popularity > row2.popularity ? -1 : 1; + } + private bool create_context_menu (Gdk.Event event) { var selected_row = (AppListRow) listbox.get_selected_row (); @@ -193,7 +225,7 @@ public class Slingshot.Widgets.CategoryView : Gtk.EventBox { } } - public void setup_sidebar () { + private CategoryRow? setup_sidebar () { CategoryRow? old_selected = (CategoryRow) category_switcher.get_selected_row (); foreach (unowned Gtk.Widget child in category_switcher.get_children ()) { child.destroy (); @@ -202,12 +234,29 @@ public class Slingshot.Widgets.CategoryView : Gtk.EventBox { listbox.foreach ((app_list_row) => listbox.remove (app_list_row)); foreach (unowned Backend.App app in view.app_system.get_apps_by_name ()) { - listbox.add (new AppListRow (app.desktop_id, app.desktop_path)); + listbox.add (new AppListRow (app)); } + listbox.show_all (); // Fill the sidebar - unowned Gtk.ListBoxRow? new_selected = null; + CategoryRow? new_selected = null; + CategoryRow? recent_row = null; + CategoryRow? pinned_row = null; + int n_rows = 0; + // Add pinned/recent category if there are any pinned/recent apps + if (popular_apps.size > 0) { + recent_row = new CategoryRow (_(RECENT_CATEGORY)); + category_switcher.add (recent_row); + n_rows++; + } + + if (pinned.size > 0) { + pinned_row = new CategoryRow (_(PINNED_CATEGORY)); + category_switcher.add (pinned_row); + n_rows++; + } + foreach (string cat_name in view.app_system.apps.keys) { if (cat_name == "switchboard") { continue; @@ -215,19 +264,62 @@ public class Slingshot.Widgets.CategoryView : Gtk.EventBox { var row = new CategoryRow (cat_name); category_switcher.add (row); - if (old_selected != null && old_selected.cat_name == cat_name) { - new_selected = row; + n_rows++; + } + + if (old_selected != null) { + for (int i = 0; i < n_rows; i++) { + var row = (CategoryRow) category_switcher.get_row_at_index (i); + if (old_selected.cat_name == row.cat_name) { + new_selected = row; + break; + } } } category_switcher.show_all (); - category_switcher.select_row (new_selected ?? category_switcher.get_row_at_index (0)); + return new_selected ?? (recent_row ?? pinned_row); + } + + public void update (string[]? pinned_apps) { + if (pinned_apps != null && show_pinned) { + pinned.clear (); + foreach (string app_id in pinned_apps) { + pinned.add (app_id); + } + } + +#if HAVE_ZEITGEIST + view.app_system.update_popularities.begin ((obj, res) => { + view.app_system.update_popularities.end (res); + popular_apps.clear (); + var popularity = view.app_system.get_apps_by_popularity (); + popularity.@foreach ((app) => { + popular_apps.add (app.desktop_id); + }); + // Any new entries will not appear until next showing, but + // at lease resort the existing entries. + listbox.invalidate_filter (); + listbox.invalidate_sort (); + }); +#endif + var selected = setup_sidebar (); + category_switcher.select_row (selected); + } [CCode (instance_pos = -1)] private bool filter_function (AppListRow row) { unowned CategoryRow category_row = (CategoryRow) category_switcher.get_selected_row (); if (category_row != null) { + if (category_row.cat_name == _(PINNED_CATEGORY)) { + return pinned.contains (row.app_id); + } + + if (category_row.cat_name == _(RECENT_CATEGORY)) { + return popular_apps.contains (row.app_id); + } + foreach (Backend.App app in view.app_system.apps[category_row.cat_name]) { if (row.app_id == app.desktop_id) { return true; diff --git a/src/Widgets/AppListRow.vala b/src/Widgets/AppListRow.vala index 9cf303586..94a373e19 100644 --- a/src/Widgets/AppListRow.vala +++ b/src/Widgets/AppListRow.vala @@ -17,21 +17,23 @@ * Boston, MA 02110-1301 USA. */ -public class AppListRow : Gtk.ListBoxRow { - public string app_id { get; construct; } - public string desktop_path { get; construct; } - public GLib.DesktopAppInfo app_info { get; private set; } +public class Slingshot.AppListRow : Gtk.ListBoxRow { + public Backend.App app {get; construct; } + public string app_id { get { return app.desktop_id; }} + public int popularity { get { return app.popularity; }} + public string desktop_path { get { return app.desktop_path; }} + public string display_name { get; private set; } + public Icon icon { get { return app_info.get_icon (); }} + private GLib.DesktopAppInfo app_info; - public AppListRow (string app_id, string desktop_path) { + public AppListRow (Backend.App app) { Object ( - app_id: app_id, - desktop_path: desktop_path + app: app ); } construct { app_info = new GLib.DesktopAppInfo (app_id); - var icon = app_info.get_icon (); weak Gtk.IconTheme theme = Gtk.IconTheme.get_default (); if (icon == null || theme.lookup_by_gicon (icon, 32, Gtk.IconLookupFlags.USE_BUILTIN) == null) { @@ -42,7 +44,8 @@ public class AppListRow : Gtk.ListBoxRow { image.gicon = icon; image.pixel_size = 32; - var name_label = new Gtk.Label (app_info.get_display_name ()); + display_name = app_info.get_display_name (); + var name_label = new Gtk.Label (display_name); name_label.set_ellipsize (Pango.EllipsizeMode.END); name_label.xalign = 0;