Hello community,

here is the log from the commit of package nwg-launchers for openSUSE:Factory 
checked in at 2020-09-23 18:45:07
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/nwg-launchers (Old)
 and      /work/SRC/openSUSE:Factory/.nwg-launchers.new.4249 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "nwg-launchers"

Wed Sep 23 18:45:07 2020 rev:4 rq:836228 version:0.4.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/nwg-launchers/nwg-launchers.changes      
2020-09-17 15:00:24.252457162 +0200
+++ /work/SRC/openSUSE:Factory/.nwg-launchers.new.4249/nwg-launchers.changes    
2020-09-23 18:46:26.997655234 +0200
@@ -1,0 +2,11 @@
+Wed Sep 23 07:31:57 UTC 2020 - Michael Vetter <[email protected]>
+
+- Update to 0.4.0:
+  * Respect NoDisplay=true setting in saner way
+  * Separate widgets from data
+  Breaking changes:
+  * Use desktop-id instead of exec to distinguish entries from each other.
+  This breaks existing pins/favs cache. Old caches will be overwritten after 
+  the first launch.
+
+-------------------------------------------------------------------

Old:
----
  v0.3.4.tar.gz

New:
----
  v0.4.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ nwg-launchers.spec ++++++
--- /var/tmp/diff_new_pack.hiaQju/_old  2020-09-23 18:46:29.945657946 +0200
+++ /var/tmp/diff_new_pack.hiaQju/_new  2020-09-23 18:46:29.953657954 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           nwg-launchers
-Version:        0.3.4
+Version:        0.4.0
 Release:        0
 Summary:        GTK launchers and menu for sway and i3
 License:        GPL-3.0-or-later

++++++ v0.3.4.tar.gz -> v0.4.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-launchers-0.3.4/bar/bar.cc 
new/nwg-launchers-0.4.0/bar/bar.cc
--- old/nwg-launchers-0.3.4/bar/bar.cc  2020-09-16 23:58:07.000000000 +0200
+++ new/nwg-launchers-0.4.0/bar/bar.cc  2020-09-22 02:41:14.000000000 +0200
@@ -200,6 +200,7 @@
         return EXIT_FAILURE;
     }
     auto& icon_theme_ref = *icon_theme.get();
+    auto icon_missing = Gdk::Pixbuf::create_from_file(DATA_DIR_STR 
"/nwgbar/icon-missing.svg");
 
     if (std::filesystem::is_regular_file(css_file)) {
         provider->load_from_path(css_file);
@@ -234,7 +235,7 @@
 
     /* Create buttons */
     for (auto& entry : bar_entries) {
-        Gtk::Image* image = app_image(icon_theme_ref, entry.icon);
+        Gtk::Image* image = app_image(icon_theme_ref, entry.icon, 
icon_missing);
         auto& ab = window.boxes.emplace_back(std::move(entry.name),
                                              std::move(entry.exec),
                                              std::move(entry.icon));
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-launchers-0.3.4/common/nwg_classes.h 
new/nwg-launchers-0.4.0/common/nwg_classes.h
--- old/nwg-launchers-0.3.4/common/nwg_classes.h        2020-09-16 
23:58:07.000000000 +0200
+++ new/nwg-launchers-0.4.0/common/nwg_classes.h        2020-09-22 
02:41:14.000000000 +0200
@@ -76,10 +76,6 @@
     std::string icon;
     std::string comment;
     std::string mime_type;
-    // We need this field to mask unwanted .desktop entries by their copies in 
./local/share/applications
-    // with the `NoDisplay=true` line added.
-    // See 
https://wiki.archlinux.org/index.php/desktop_entries#Hide_desktop_entries
-    bool no_display {false};
 };
 
 struct RGBA {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-launchers-0.3.4/common/nwg_tools.cc 
new/nwg-launchers-0.4.0/common/nwg_tools.cc
--- old/nwg-launchers-0.3.4/common/nwg_tools.cc 2020-09-16 23:58:07.000000000 
+0200
+++ new/nwg-launchers-0.4.0/common/nwg_tools.cc 2020-09-22 02:41:14.000000000 
+0200
@@ -151,7 +151,11 @@
 /*
  * Returns Gtk::Image out of the icon name of file path
  * */
-Gtk::Image* app_image(const Gtk::IconTheme& icon_theme, const std::string& 
icon) {
+Gtk::Image* app_image(
+    const Gtk::IconTheme& icon_theme,
+    const std::string& icon,
+    const Glib::RefPtr<Gdk::Pixbuf>& fallback
+) {
     Glib::RefPtr<Gdk::Pixbuf> pixbuf;
 
     try {
@@ -164,7 +168,7 @@
         try {
             pixbuf = Gdk::Pixbuf::create_from_file("/usr/share/pixmaps/" + 
icon, image_size, image_size, true);
         } catch (...) {
-            pixbuf = Gdk::Pixbuf::create_from_file(DATA_DIR_STR 
"/nwgbar/icon-missing.svg", image_size, image_size, true);
+            pixbuf = fallback;
         }
     }
     auto image = Gtk::manage(new Gtk::Image(pixbuf));
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-launchers-0.3.4/common/nwg_tools.h 
new/nwg-launchers-0.4.0/common/nwg_tools.h
--- old/nwg-launchers-0.3.4/common/nwg_tools.h  2020-09-16 23:58:07.000000000 
+0200
+++ new/nwg-launchers-0.4.0/common/nwg_tools.h  2020-09-22 02:41:14.000000000 
+0200
@@ -44,7 +44,7 @@
 
 std::string get_output(const std::string&);
 
-Gtk::Image* app_image(const Gtk::IconTheme&, const std::string&);
+Gtk::Image* app_image(const Gtk::IconTheme&, const std::string&, const 
Glib::RefPtr<Gdk::Pixbuf>&);
 Geometry display_geometry(const std::string&, Glib::RefPtr<Gdk::Display>, 
Glib::RefPtr<Gdk::Window>);
 
 void create_pid_file_or_kill_pid(std::string);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-launchers-0.3.4/grid/grid.cc 
new/nwg-launchers-0.4.0/grid/grid.cc
--- old/nwg-launchers-0.3.4/grid/grid.cc        2020-09-16 23:58:07.000000000 
+0200
+++ new/nwg-launchers-0.4.0/grid/grid.cc        2020-09-22 02:41:14.000000000 
+0200
@@ -18,10 +18,10 @@
 #include "grid.h"
 
 bool pins = false;              // whether to display pinned
-RGBA background = {0.0, 0.0, 0.0, 0.9};
+bool favs = false;              // whether to display favorites
 std::string wm {""};            // detected or forced window manager name
-
-int num_col = 6;                // number of grid columns
+std::size_t num_col = 6;        // number of grid columns
+RGBA background = {0.0, 0.0, 0.0, 0.9};
 
 std::string pinned_file {};
 std::vector<std::string> pinned;    // list of commands of pinned icons
@@ -29,7 +29,7 @@
 std::string cache_file {};
 
 const char* const HELP_MESSAGE =
-"GTK application grid: nwggrid " VERSION_STR " (c) Piotr Miller 2020 & 
Contributors \n\n\
+"GTK application grid: nwggrid " VERSION_STR " (c) 2020 Piotr Miller, Sergey 
Smirnykh & Contributors \n\n\
 \
 Options:\n\
 -h               show this help message and exit\n\
@@ -44,7 +44,6 @@
 -wm <wmname>     window manager name (if can not be detected)\n";
 
 int main(int argc, char *argv[]) {
-    bool favs (false);              // whether to display favourites
     std::string custom_css_file {"style.css"};
 
     struct timeval tp;
@@ -60,12 +59,8 @@
         std::cout << HELP_MESSAGE;
         std::exit(0);
     }
-    if (input.cmdOptionExists("-f")){
-        favs = true;
-    }
-    if (input.cmdOptionExists("-p")){
-        pins = true;
-    }
+    favs = input.cmdOptionExists("-f");
+    pins = input.cmdOptionExists("-p");
     auto forced_lang = input.getCmdOption("-l");
     if (!forced_lang.empty()){
         lang = forced_lang;
@@ -129,8 +124,21 @@
         }
     }
 
+    /* get current WM name if not forced */
+    if (wm.empty()) {
+        wm = detect_wm();
+    }
+    std::cout << "WM: " << wm << "\n";
+
+    /* get lang if not yet forced */
+    if (lang.empty()) {
+        lang = get_locale();
+    }
+    std::cout << "Locale: " << lang << "\n";
+
+    auto cache_home = get_cache_home();
     if (favs) {
-        cache_file = get_cache_path();
+        cache_file = cache_home / "nwg-fav-cache";
         try {
             cache = get_cache(cache_file);
         }  catch (...) {
@@ -141,12 +149,11 @@
             std::cout << cache.size() << " cache entries loaded\n";
         } else {
             std::cout << "No cached favourites found\n";
-            favs = false;   // ignore -f argument from now on
         }
     }
 
     if (pins) {
-        pinned_file = get_pinned_path();
+        pinned_file = cache_home / "nwg-pin-cache";
         pinned = get_pinned(pinned_file);
         if (pinned.size() > 0) {
           std::cout << pinned.size() << " pinned entries loaded\n";
@@ -175,66 +182,75 @@
     }
 
     // This will be read-only, to find n most clicked items (n = number of 
grid columns)
-    std::vector<CacheEntry> favourites {};
+    std::vector<CacheEntry> favourites;
     if (cache.size() > 0) {
-        auto n = cache.size() >= static_cast<std::size_t>(num_col) ? num_col : 
cache.size();
+        auto n = std::min(num_col, cache.size());
         favourites = get_favourites(std::move(cache), n);
     }
 
-    /* get current WM name if not forced */
-    if (wm.empty()) {
-        wm = detect_wm();
-    }
-    std::cout << "WM: " << wm << "\n";
+    /* get all applications dirs */
+    auto dirs = get_app_dirs();
 
-    /* get lang if not yet forced */
-    if (lang.empty()) {
-        lang = get_locale();
-    }
-    std::cout << "Locale: " << lang << "\n";
+    gettimeofday(&tp, NULL);
+    long int commons_ms  = tp.tv_sec * 1000 + tp.tv_usec / 1000;
 
-    /* get all applications dirs */
-    auto app_dirs = get_app_dirs();
+    // Maps desktop-ids to their table indices, nullopt stands for 'hidden'
+    std::unordered_map<std::string, std::optional<std::size_t>> desktop_ids;
 
-    /* get a list of paths to all *.desktop entries */
-    auto entries = list_entries(app_dirs);
-    std::cout << entries.size() << " .desktop entries found, ";
-
-    /* create the vector of DesktopEntry structs */
-    std::vector<DesktopEntry> desktop_entries {};
-    std::size_t hidden = 0;
-    for (auto& entry_ : entries) {
-        // string path -> DesktopEntry
-        auto maybe_entry = desktop_entry(std::move(entry_), lang);
-        // We need hidden desktop entries!
-        //if (!maybe_entry) {
-        //    hidden++;
-        //     continue; // the entry is NoDisplay, discard it and continue
-        //}
-        auto& entry = *maybe_entry;
-        if (entry.no_display) {
-            hidden++;
-        }
-
-        // only add if 'name' and 'exec' not empty
-        if (!entry.name.empty() && !entry.exec.empty()) {
-            // avoid adding duplicates
-            bool found = false;
-            for (auto& e: desktop_entries) {
-                // Checking the mime_type field should resolve #89
-                if (entry.name == e.name && entry.exec == e.exec && 
entry.mime_type == e.mime_type) {
-                    found = true;
-                }
+    // Table, only contains shown entries
+    std::vector<DesktopEntry> desktop_entries;
+    std::vector<std::string>  execs;
+    std::vector<Stats>        stats;
+    std::vector<Gtk::Image*>  images;
+
+    auto desktop_id = [](auto& path) {
+        return path.string(); // actual desktop_id requires '/' to be replaced 
with '-'
+    };
+
+    for (auto& dir : dirs) {
+        std::error_code ec;
+        auto dir_iter = fs::directory_iterator(dir, ec);
+        for (auto& entry : dir_iter) {
+            if (ec) {
+                std::cerr << ec.message() << '\n';
+                ec.clear();
+                continue;
+            }
+            if (!entry.is_regular_file()) {
+                continue;
             }
-            if (!found) {
-                desktop_entries.emplace_back(std::move(entry));
+            auto& path = entry.path();
+            auto&& rel_path = path.lexically_relative(dir);
+            auto&& id = desktop_id(rel_path);
+            if (auto [at, inserted] = desktop_ids.try_emplace(id, 
std::nullopt); inserted) {
+                if (auto entry = desktop_entry(path, lang)) {
+                    at->second = execs.size(); // set index
+                    execs.emplace_back(entry->exec);
+                    desktop_entries.emplace_back(std::move(*entry));
+                    stats.emplace_back(0, 0, Stats::Common, Stats::Unpinned);
+                    images.emplace_back(nullptr);
+                }
             }
         }
     }
-    std::cout << desktop_entries.size() << " unique, " << hidden << " hidden 
by NoDisplay=true\n";
 
-    /* sort above by the 'name' field */
-    std::sort(desktop_entries.begin(), desktop_entries.end(), [](auto& a, 
auto& b) { return a.name < b.name; });
+    int pin_index = 0; // preserve pins order
+    for (auto& pin : pinned) {
+        if (auto result = desktop_ids.find(pin); result != desktop_ids.end() 
&& result->second) {
+            stats[*result->second].pinned = Stats::Pinned;
+            stats[*result->second].position = pin_index;
+            pin_index++;
+        }
+    }
+    for (auto& [fav, clicks] : favourites) {
+        if (auto result = desktop_ids.find(fav); result != desktop_ids.end() 
&& result->second) {
+            stats[*result->second].clicks   = clicks;
+            stats[*result->second].favorite = Stats::Favorite;
+        }
+    }
+
+    gettimeofday(&tp, NULL);
+    long int bs_ms  = tp.tv_sec * 1000 + tp.tv_usec / 1000;
 
     auto app = Gtk::Application::create();
 
@@ -251,32 +267,24 @@
         std::cerr << "ERROR: Failed to load icon theme\n";
     }
     auto& icon_theme_ref = *icon_theme.get();
-    icon_theme_ref.add_resource_path(DATA_DIR_STR "/icon-missing.svg");
+    auto icon_missing = Gdk::Pixbuf::create_from_file(DATA_DIR_STR 
"/nwgbar/icon-missing.svg");
 
-    if (std::filesystem::is_regular_file(css_file)) {
-        provider->load_from_path(css_file);
-        std::cout << "Using " << css_file << '\n';
-    } else {
-        provider->load_from_path(default_css_file);
-        std::cout << "Using " << default_css_file << '\n';
+    if (!std::filesystem::is_regular_file(css_file)) {
+        css_file = default_css_file;
     }
+    provider->load_from_path(css_file);
+    std::cout << "Using " << css_file << '\n';
 
-    MainWindow window;
-
+    MainWindow window(execs, stats);
     window.show();
 
-    /* Detect focused display geometry: {x, y, width, height} */
-    auto geometry = display_geometry(wm, display, window.get_window());
-    std::cout << "Focused display: " << geometry.x << ", " << geometry.y << ", 
" << geometry.width << ", "
-    << geometry.height << '\n';
-
-    int x = geometry.x;
-    int y = geometry.y;
-    int w = geometry.width;
-    int h = geometry.height;
+    /* Detect focused display geometry: {x, y, w, h} */
+    auto [x, y, w, h] = display_geometry(wm, display, window.get_window());
+    std::cout << "Focused display: " << x << ", " << y << ", " << w << ", "
+    << h << '\n';
 
     /* turn off borders, enable floating on sway */
-    if (wm == "sway") {
+    if (wm == "sway") { // TODO: Use sway-ipc
         auto* cmd = "swaymsg for_window [title=\"~nwggrid*\"] floating enable";
         std::system(cmd);
         cmd = "swaymsg for_window [title=\"~nwggrid*\"] border none";
@@ -288,78 +296,46 @@
         window.move(x, y);
     }
 
-    /* Create buttons for pinned entries */
-    for (auto& pin : pinned) {
-        auto find = [&pin](auto& container) {
-            return std::find_if(container.begin(), container.end(), 
[&pin](auto& e) {
-                return pin == e.exec;
-            });
-        };
-        auto iter = find(desktop_entries);
-        if (iter != desktop_entries.end()) {
-            auto& entry = *iter;
-            auto found = find(favourites) != favourites.end();
-            // 0 -> Common
-            // 1 -> Favorite
-            auto fav_tag = GridBox::FavTag{ found };
-            auto& ab = window.emplace_box(std::move(entry.name),
-                                          std::move(entry.exec),
-                                          std::move(entry.comment),
-                                          fav_tag,
-                                          GridBox::Pinned);
-            Gtk::Image* image = app_image(icon_theme_ref, entry.icon);
-            ab.set_image_position(Gtk::POS_TOP);
-            ab.set_image(*image);
-        }
-    }
-
-    /* Create buttons for favourites */
-    for (auto& entry : favourites) {
-        for (auto& de : desktop_entries) {
-            if (entry.exec == de.exec) {
-                auto already_added = window.has_fav_with_exec(entry.exec);
-                if (already_added) {
-                    continue;
-                }
+    gettimeofday(&tp, NULL);
+    long int images_ms  = tp.tv_sec * 1000 + tp.tv_usec / 1000;
 
-                auto& ab = window.emplace_box(std::move(de.name),
-                                              std::move(de.exec),
-                                              std::move(de.comment),
-                                              GridBox::Favorite,
-                                              GridBox::Unpinned);
-
-                Gtk::Image* image = app_image(icon_theme_ref, de.icon);
-                ab.set_image_position(Gtk::POS_TOP);
-                ab.set_image(*image);
-            }
-        }
+    // The most expensive part
+    for (std::size_t i = 0; i < desktop_entries.size(); i++) {
+        images[i] = app_image(icon_theme_ref, desktop_entries[i].icon, 
icon_missing);
     }
 
-    /* Create buttons for the rest of entries */
-    for (auto& entry : desktop_entries) {
-        // if it's empty, it was probably moved from during the previous steps
-        // there should be some better way, but it works
-        if (!entry.exec.empty() && !entry.no_display) {
-             auto& ab = window.emplace_box(std::move(entry.name),
-                                           std::move(entry.exec),
-                                           std::move(entry.comment),
-                                           GridBox::Common,
-                                           GridBox::Unpinned);
-             Gtk::Image* image = app_image(icon_theme_ref, entry.icon);
-             ab.set_image_position(Gtk::POS_TOP);
-             ab.set_image(*image);
+    gettimeofday(&tp, NULL);
+    long int boxes_ms = tp.tv_sec * 1000 + tp.tv_usec / 1000;
+
+    for (auto& [desktop_id, pos_] : desktop_ids) {
+        if (auto pos = *pos_; pos_) {
+            auto& entry = desktop_entries[pos];
+            auto& ab = window.emplace_box(std::move(entry.name),
+                                          std::move(entry.comment),
+                                          desktop_id,
+                                          pos);
+            ab.set_image_position(Gtk::POS_TOP);
+            ab.set_image(*images[pos]);
         }
     }
 
+    gettimeofday(&tp, NULL);
+    long int grids_ms = tp.tv_sec * 1000 + tp.tv_usec / 1000;
 
     window.build_grids();
 
     gettimeofday(&tp, NULL);
     long int end_ms = tp.tv_sec * 1000 + tp.tv_usec / 1000;
 
-    std::cout << "Time: " << end_ms - start_ms << "ms\n";
-
-    app->run(window);
+    auto format = [&cout=std::cout](auto&& title, auto from, auto to) {
+        cout << title << to - from << "ms\n";
+    };
+    format("Total: ", start_ms, end_ms);
+    format("\tgrids:   ", grids_ms, end_ms);
+    format("\tboxes:   ", boxes_ms, grids_ms);
+    format("\timages:  ", images_ms, boxes_ms);
+    format("\tbs:      ", bs_ms, images_ms);
+    format("\tcommons: ", commons_ms, bs_ms);
 
-    return 0;
+    return app->run(window);
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-launchers-0.3.4/grid/grid.h 
new/nwg-launchers-0.4.0/grid/grid.h
--- old/nwg-launchers-0.3.4/grid/grid.h 2020-09-16 23:58:07.000000000 +0200
+++ new/nwg-launchers-0.4.0/grid/grid.h 2020-09-22 02:41:14.000000000 +0200
@@ -23,17 +23,26 @@
 namespace ns = nlohmann;
 
 extern bool pins;
+extern bool favs;
 extern std::string wm;
 
-extern int num_col;
+extern std::size_t num_col;
 
 extern std::string pinned_file;
 extern std::vector<std::string> pinned;
 extern ns::json cache;
 extern std::string cache_file;
 
-class GridBox : public AppBox {
-public:
+/* Primitive version of C++20's std::span */
+template <typename T>
+struct Span {
+    Span(const Span& s) = default;
+    Span(std::vector<T>& t): _ref(t.data()) { }
+    T& operator [](std::size_t n) { return _ref[n]; }
+    T* _ref;
+};
+
+struct Stats {
     enum FavTag: bool {
         Common = 0,
         Favorite = 1,
@@ -42,22 +51,34 @@
         Unpinned = 0,
         Pinned = 1,
     };
+    int    clicks;
+    int    position;
+    FavTag favorite;
+    PinTag pinned;
+    Stats(int c, int i, FavTag f, PinTag p)
+      : clicks(c), position(i), favorite(f), pinned(p) { }
+};
 
-    /* name, exec, comment, favorite, pinned */
-    GridBox(Glib::ustring, Glib::ustring, Glib::ustring, FavTag, PinTag);
+class GridBox : public Gtk::Button {
+public:
+    /* name, comment, desktop-id, index */
+    GridBox(Glib::ustring, Glib::ustring, const std::string& id, std::size_t);
+    ~GridBox() = default;
     bool on_button_press_event(GdkEventButton*) override;
     bool on_focus_in_event(GdkEventFocus*) override;
     void on_enter() override;
     void on_activate() override;
 
-
-    FavTag favorite;
-    PinTag pinned;
+    Glib::ustring      name;
+    Glib::ustring      comment;
+    const std::string* desktop_id; // ptr to desktop-id, never null
+    std::size_t        index;      // row index
 };
 
 class MainWindow : public CommonWindow {
     public:
-        MainWindow();
+        MainWindow(Span<std::string> entries, Span<Stats> stats);
+        MainWindow(const MainWindow&) = delete;
 
         Gtk::SearchEntry searchbox;              // Search apps
         Gtk::Label description;                  // To display .desktop entry 
Comment field at the bottom
@@ -77,21 +98,33 @@
         template <typename ... Args>
         GridBox& emplace_box(Args&& ... args);      // emplace box
 
-        bool has_fav_with_exec(const std::string&) const;
         void build_grids();
         void toggle_pinned(GridBox& box);
         void set_description(const Glib::ustring&);
+        void save_cache();
+
+        std::string& exec_of(const GridBox& box) {
+            return execs[box.index];
+        }
+        Stats& stats_of(const GridBox& box) {
+            return stats[box.index];
+        }
     protected:
         //Override default signal handler:
         bool on_key_press_event(GdkEventKey*) override;
         bool on_delete_event(GdkEventAny*) override;
         bool on_button_press_event(GdkEventButton*) override;
     private:
-        std::list<GridBox> all_boxes {};         // stores all applications 
buttons
-        std::vector<GridBox*> apps_boxes {};     // boxes attached to apps_grid
-        std::vector<GridBox*> filtered_boxes {}; // filtered boxes from
+        std::list<GridBox>    all_boxes {};      // stores all applications 
buttons
+        std::vector<GridBox*> apps_boxes {};     // all common boxes
+        std::vector<GridBox*> filtered_boxes {}; // common boxes meeting 
search criteria
         std::vector<GridBox*> fav_boxes {};      // attached to favs_grid
         std::vector<GridBox*> pinned_boxes {};   // attached to pinned_grid
+
+        Span<std::string> execs;
+        Span<Stats>       stats;
+
+        int  monotonic_index;       // to keep pins in order, see 
grid_classes.cc comment
         bool pins_changed = false;
         bool is_filtered = false;
 
@@ -103,18 +136,19 @@
 template <typename ... Args>
 GridBox& MainWindow::emplace_box(Args&& ... args) {
     auto& ab = this -> all_boxes.emplace_back(std::forward<Args>(args)...);
-    if (ab.pinned) {
-        pinned_boxes.push_back(&ab);
-    } else if (ab.favorite) {
-        fav_boxes.push_back(&ab);
-    } else {
-        apps_boxes.push_back(&ab);
+    auto* boxes = &apps_boxes;
+    auto& stats = this -> stats_of(ab);
+    if (stats.pinned) {
+        boxes = &pinned_boxes;
+    } else if (stats.favorite) {
+        boxes = &fav_boxes;
     }
+    boxes->push_back(&ab);
     return ab;
 }
 
 struct CacheEntry {
-    std::string exec;
+    std::string desktop_id;
     int clicks;
     CacheEntry(std::string, int);
 };
@@ -122,11 +156,9 @@
 /*
  * Function declarations
  * */
+fs::path                    get_cache_home();
 ns::json                    get_cache(const std::string&);
-std::string                 get_cache_path(void);
-std::string                 get_pinned_path(void);
 std::vector<std::string>    get_app_dirs(void);
-std::vector<std::string>    list_entries(const std::vector<std::string>&);
 std::vector<std::string>    get_pinned(const std::string&);
 std::vector<CacheEntry>     get_favourites(ns::json&&, int);
 std::optional<DesktopEntry> desktop_entry(std::string&&, const std::string&);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-launchers-0.3.4/grid/grid_classes.cc 
new/nwg-launchers-0.4.0/grid/grid_classes.cc
--- old/nwg-launchers-0.3.4/grid/grid_classes.cc        2020-09-16 
23:58:07.000000000 +0200
+++ new/nwg-launchers-0.4.0/grid/grid_classes.cc        2020-09-22 
02:41:14.000000000 +0200
@@ -17,14 +17,23 @@
 #include "nwg_tools.h"
 #include "grid.h"
 
-int by_name(Gtk::FlowBoxChild* a_, Gtk::FlowBoxChild* b_) {
-    auto a = dynamic_cast<GridBox*>(a_->get_child());
-    auto b = dynamic_cast<GridBox*>(b_->get_child());
-    return a->name.compare(b->name);
+// we only store GridBoxes inside of our FlowBoxes, so dynamic_cast won't fail
+inline auto child_ = [](auto c) -> auto& { return 
*dynamic_cast<GridBox*>(c->get_child()); };
+int by_name(Gtk::FlowBoxChild* a, Gtk::FlowBoxChild* b) {
+    return child_(a).name.compare(child_(b).name);
+}
+// return -1 if a < b, 0 if a == b, 1 if a > b
+inline auto cmp_ = [](auto a, auto b) { return int(a > b) - int(a < b); };
+int by_position(Gtk::FlowBoxChild* a, Gtk::FlowBoxChild* b) {
+    auto& toplevel = *dynamic_cast<MainWindow*>(a->get_toplevel());
+    return cmp_(toplevel.stats_of(child_(a)).position, 
toplevel.stats_of(child_(b)).position);
+}
+int by_clicks(Gtk::FlowBoxChild* a, Gtk::FlowBoxChild* b) {
+    auto& toplevel = *dynamic_cast<MainWindow*>(a->get_toplevel());
+    return -cmp_(toplevel.stats_of(child_(a)).clicks, 
toplevel.stats_of(child_(b)).clicks);
 }
-
-MainWindow::MainWindow()
- : CommonWindow("~nwggrid", "~nwggrid")
+MainWindow::MainWindow(Span<std::string> es, Span<Stats> ss)
+ : CommonWindow("~nwggrid", "~nwggrid"), execs(es), stats(ss)
 {
     searchbox
         .signal_search_changed()
@@ -43,7 +52,10 @@
     setup_grid(apps_grid);
     setup_grid(favs_grid);
     setup_grid(pinned_grid);
+
+    pinned_grid.set_sort_func(&by_position);
     apps_grid.set_sort_func(&by_name);
+    favs_grid.set_sort_func(&by_clicks);
 
     description.set_text("");
     description.set_name("description");
@@ -99,8 +111,7 @@
 }
 
 bool MainWindow::on_key_press_event(GdkEventKey* key_event) {
-    auto key_val = key_event -> keyval;
-    switch (key_val) {
+    switch (key_event->keyval) {
         case GDK_KEY_Escape:
             this->close();
             break;
@@ -139,14 +150,13 @@
 inline auto refresh_max_children_per_line = [](auto& flowbox, auto& container) 
{
     auto size = container.size();
     decltype(size) cols = num_col;
-    if (auto min = std::min(cols, size)) {
+    if (auto min = std::min(cols, size)) { // suppress gtk warnings about 
size=0
         flowbox.set_min_children_per_line(std::min(size, cols));
         flowbox.set_max_children_per_line(std::min(size, cols));
     }
 };
-/*
- * Populate grid with widgets from container
- * */
+
+/* Populate grid with widgets from container */
 inline auto build_grid = [](auto& grid, auto& container) {
     for (auto child : container) {
         grid.add(*child);
@@ -155,6 +165,7 @@
     refresh_max_children_per_line(grid, container);
 };
 
+/* Called each time `search_entry` changes, rebuilds `apps_grid` according to 
search criteria */
 void MainWindow::filter_view() {
     auto clean_grid = [](auto& grid) {
         grid.foreach([&grid](auto& child) {
@@ -172,7 +183,9 @@
             return str.casefold().find(phrase) != Glib::ustring::npos;
         };
         for (auto* box : apps_boxes) {
-            if (matches(box->name) || matches(box->exec) || 
matches(box->comment)) {
+            auto& exec = this->exec_of(*box);
+            auto matches_exec = exec.find(phrase) != std::string::npos;
+            if (matches(box->name) || matches_exec || matches(box->comment)) {
                 filtered_boxes.push_back(box);
             }
         }
@@ -187,6 +200,7 @@
     apps_grid.thaw_child_notify();
 }
 
+/* Sets separators' visibility according to grid status */
 void MainWindow::refresh_separators() {
     auto set_shown = [](auto c, auto& s) { if (c) s.show(); else s.hide(); };
     auto p = !pinned_boxes.empty();
@@ -210,6 +224,8 @@
     build_grid(this->favs_grid, this->fav_boxes);
     build_grid(this->apps_grid, this->apps_boxes);
 
+    this -> monotonic_index = this->pinned_boxes.size();
+
     this -> pinned_grid.show_all_children();
     this -> favs_grid.show_all_children();
     this -> apps_grid.show_all_children();
@@ -223,10 +239,17 @@
 }
 
 void MainWindow::focus_first_box() {
-    std::array containers{ &filtered_boxes, &pinned_boxes, &fav_boxes, 
&apps_boxes };
-    for (auto container : containers) {
-        if (container->size() > 0) {
-            container->front()->grab_focus();
+    // flowbox -> flowboxchild -> gridbox
+    if (is_filtered) {
+        if (auto child = apps_grid.get_child_at_index(0)) {
+            child->get_child()->grab_focus();
+            return;
+        }
+    }
+    std::array grids { &pinned_grid,  &favs_grid, &apps_grid };
+    for (auto grid : grids) {
+        if (auto child = grid->get_child_at_index(0)) {
+            child->get_child()->grab_focus();
             return;
         }
     }
@@ -243,13 +266,19 @@
     // disable prelight
     box.unset_state_flags(Gtk::STATE_FLAG_PRELIGHT);
 
-    auto is_pinned = box.pinned == GridBox::Pinned;
-    box.pinned = GridBox::PinTag{ !is_pinned };
+    auto& stats = this->stats_of(box);
+    auto is_pinned = stats.pinned == Stats::Pinned;
+    stats.pinned = Stats::PinTag{ !is_pinned };
+    
+    // monotonic index increases each time an entry is pinned
+    // ensuring it will appear last
+    stats.position = this->monotonic_index * !is_pinned;
+    this->monotonic_index += !is_pinned;
 
     auto* from_grid = &this->apps_grid;
     auto* from = &this->apps_boxes;
 
-    if (box.favorite) {
+    if (stats.favorite) {
         from_grid = &this->favs_grid;
         from = &this->fav_boxes;
     }
@@ -280,65 +309,84 @@
     }
 }
 
+
 /*
  * Saves pinned cache file
  * */
-bool MainWindow::on_delete_event(GdkEventAny* event) {
-    if (this->pins_changed) {
-        std::ofstream out_file(pinned_file);
-        for (auto pin : this->pinned_boxes) {
-            out_file << pin->exec << '\n';
+void MainWindow::save_cache() {
+    if (pins_changed) {
+        std::sort(pinned_boxes.begin(), pinned_boxes.end(), [this](auto* a, 
auto* b) {
+            return this->stats_of(*a).position < this->stats_of(*b).position;
+        });
+        std::ofstream out(pinned_file, std::ios::trunc);
+        for (auto* pin : this->pinned_boxes) {
+            out << *pin->desktop_id << '\n';
         }
     }
-    return CommonWindow::on_delete_event(event);
+    if (favs) {
+        ns::json favs_cache;
+        // find min positive clicks count
+        decltype(Stats::clicks) min = 1000000; // avoid including <limits>
+        for (auto& box : this->all_boxes) {
+            if (auto clicks = stats_of(box).clicks; clicks > 0) {
+                min = std::min(min, clicks);
+            }
+        }
+        // only save positives, substract min to keep clicks low, but preserve 
order
+        for (auto& box : this->all_boxes) {
+            if (auto clicks = stats_of(box).clicks - min + 1; clicks > 0) {
+                favs_cache[*box.desktop_id] = clicks;
+            }
+        }
+        save_json(favs_cache, cache_file);
+    }
 }
 
-bool MainWindow::has_fav_with_exec(const std::string& exec) const {
-    auto pred = [&exec](auto* b) { return exec == b->exec; };
-    return std::find_if(fav_boxes.begin(), fav_boxes.end(), pred) != 
fav_boxes.end();
+bool MainWindow::on_delete_event(GdkEventAny* event) {
+    this -> save_cache();
+    return CommonWindow::on_delete_event(event);
 }
 
-// This constructor is not needed since C++20
-GridBox::GridBox(Glib::ustring name, Glib::ustring exec, Glib::ustring 
comment, GridBox::FavTag fav, GridBox::PinTag pinned)
- : AppBox(std::move(name), std::move(exec), std::move(comment)), 
favorite(fav), pinned(pinned) {}
+GridBox::GridBox(Glib::ustring name, Glib::ustring comment, const std::string& 
id, std::size_t index)
+: name(std::move(name)), comment(std::move(comment)), desktop_id(&id), 
index(index)
+{
+   if (this->name.length() > 25) {
+       this->name.resize(22);
+       this->name += "...";
+   }
+   this->set_always_show_image(true);
+   this->set_label(this->name);
+}
 
 bool GridBox::on_button_press_event(GdkEventButton* event) {
-    if (pins && event->button == 3) {
-        auto toplevel = dynamic_cast<MainWindow*>(this -> get_toplevel());
-        toplevel->toggle_pinned(*this);
+    auto& toplevel = *dynamic_cast<MainWindow*>(this -> get_toplevel());
+    if (pins && event->button == 3) { // right-clicked
+        toplevel.toggle_pinned(*this);
     } else {
-        int clicks = 0;
-        try {
-            clicks = cache[exec];
-            clicks++;
-        } catch(...) {
-            clicks = 1;
-        }
-        cache[exec] = clicks;
-        save_json(cache, cache_file);    
-
         this -> activate();
     }
-    return AppBox::on_button_press_event(event);
+    return Gtk::Button::on_button_press_event(event);
 }
 
 bool GridBox::on_focus_in_event(GdkEventFocus* event) {
     (void) event; // suppress warning
 
-    auto toplevel = dynamic_cast<MainWindow*>(this->get_toplevel());
-    toplevel->set_description(comment);
+    auto& toplevel = *dynamic_cast<MainWindow*>(this->get_toplevel());
+    toplevel.set_description(comment);
     return true;
 }
 
 void GridBox::on_enter() {
-    auto toplevel = dynamic_cast<MainWindow*>(this->get_toplevel());
-    toplevel->set_description(comment);
-    return AppBox::on_enter();
+    auto& toplevel = *dynamic_cast<MainWindow*>(this->get_toplevel());
+    toplevel.set_description(comment);
+    return Gtk::Button::on_enter();
 }
 
 void GridBox::on_activate() {
-    auto cmd = exec + " &";
+    auto& toplevel = *dynamic_cast<MainWindow*>(this->get_toplevel());
+    toplevel.stats_of(*this).clicks++;
+    std::string cmd = toplevel.exec_of(*this);
+    cmd += " &";
     std::system(cmd.data());
-    auto toplevel = dynamic_cast<MainWindow*>(this->get_toplevel());
-    toplevel->close();
+    toplevel.close();
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-launchers-0.3.4/grid/grid_tools.cc 
new/nwg-launchers-0.4.0/grid/grid_tools.cc
--- old/nwg-launchers-0.3.4/grid/grid_tools.cc  2020-09-16 23:58:07.000000000 
+0200
+++ new/nwg-launchers-0.4.0/grid/grid_tools.cc  2020-09-22 02:41:14.000000000 
+0200
@@ -13,46 +13,26 @@
 #include "nwg_tools.h"
 #include "grid.h"
 
-CacheEntry::CacheEntry(std::string exec, int clicks): exec(std::move(exec)), 
clicks(clicks) { }
+CacheEntry::CacheEntry(std::string desktop_id, int clicks): 
desktop_id(std::move(desktop_id)), clicks(clicks) { }
 
 /*
- * Returns cache file path
+ * Returns path to cache directory
  * */
-std::string get_cache_path() {
-    std::string s = "";
-    char* val = getenv("XDG_CACHE_HOME");
-    if (val) {
-        s = val;
+fs::path get_cache_home() {
+    char* home_ = getenv("XDG_CACHE_HOME");
+    fs::path home;
+    if (home_) {
+        home = home_;
     } else {
-        char* val = getenv("HOME");
-        s = val;
-        s += "/.cache";
-    }
-    fs::path dir (s);
-    fs::path file ("nwg-fav-cache");
-    fs::path full_path = dir / file;
-
-    return full_path;
-}
-
-/*
- * Returns pinned cache file path
- * */
-std::string get_pinned_path() {
-    std::string s = "";
-    char* val = getenv("XDG_CACHE_HOME");
-    if (val) {
-        s = val;
-    } else {
-        val = getenv("HOME");
-        s = val;
-        s += "/.cache";
-    }
-    fs::path dir (s);
-    fs::path file ("nwg-pin-cache");
-    fs::path full_path = dir / file;
-
-    return full_path;
+        home_ = getenv("HOME");
+        if (!home_) {
+            std::cerr << "ERROR: Neither XDG_CACHE_HOME nor HOME are not 
defined\n";
+            std::exit(EXIT_FAILURE);
+        }
+        home = home_;
+        home /= ".cache";
+    }
+    return home;
 }
 
 /*
@@ -109,23 +89,6 @@
     return result;
 }
 
-/*
- * Returns all .desktop files paths
- * */
-std::vector<std::string> list_entries(const std::vector<std::string>& paths) {
-    std::vector<std::string> desktop_paths;
-    std::error_code ec;
-    for (auto& dir : paths) {
-        // if directory exists
-        if (std::filesystem::is_directory(dir, ec) && !ec) {
-            for (const auto & entry : fs::directory_iterator(dir)) {
-                desktop_paths.emplace_back(entry.path());
-            }
-        }
-    }
-    return desktop_paths;
-}
-
 // desktop_entry helpers
 template<typename> inline constexpr bool lazy_false_v = false;
 template<typename ... Ts> struct visitor : Ts... { using Ts::operator()...; };
@@ -185,6 +148,9 @@
         }
         auto view = std::string_view{str};
         auto view_len = std::size(view);
+        if (view == nodisplay) {
+            return std::nullopt;
+        }
         auto try_strip_prefix = [&view, view_len](auto& prefix) {
             auto len = std::min(view_len, std::size(prefix));
             return Result {
@@ -192,19 +158,11 @@
                 len
             };
         };
-        if (view == nodisplay) {
-            // @Siborgium: return std::nullopt won't do the job, as we DO NEED 
this object.
-            // See 
https://wiki.archlinux.org/index.php/desktop_entries#Hide_desktop_entries
-            entry.no_display = true;
-        }
-        for (auto& match : matches) {
-            auto result = try_strip_prefix(match.prefix);
-            auto& dest = match.dest; // it was 2020, clang failed to capture
-            auto  pos  = result.pos; // var from structured binding, so we had 
to write it by hand
-            if (result.ok) {
+        for (auto& [prefix, dest, tag] : matches) {
+            if (auto [ok, pos] = try_strip_prefix(prefix); ok) {
                 std::visit(visitor {
-                    [dest, pos, &view](nop_t) { *dest = view.substr(pos); },
-                    [dest, pos, &view](cut_t) {
+                    [dest=dest, pos=pos, &view](nop_t) { *dest = 
view.substr(pos); },
+                    [dest=dest, pos=pos, &view](cut_t) {
                         auto idx = view.find(" %", pos);
                         if (idx == std::string_view::npos) {
                             idx = std::size(view);
@@ -212,7 +170,7 @@
                         *dest = view.substr(pos, idx - pos);
                     }
                 },
-                match.tag);
+                tag);
                 break;
             }
         }
@@ -224,6 +182,9 @@
     if (!comment_ln.empty()) {
         entry.comment = std::move(comment_ln);
     }
+    if (entry.name.empty() || entry.exec.empty()) {
+        return std::nullopt;
+    }
     return entry;
 }
 
@@ -247,9 +208,7 @@
         save_string_to_file("", pinned_file);
         return lines;
     }
-    std::string str;
-
-    while (std::getline(in, str)) {
+    for (std::string str; std::getline(in, str);) {
         // add non-empty lines to the vector
         if (!str.empty()) {
             lines.emplace_back(std::move(str));
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nwg-launchers-0.3.4/meson.build 
new/nwg-launchers-0.4.0/meson.build
--- old/nwg-launchers-0.3.4/meson.build 2020-09-16 23:58:07.000000000 +0200
+++ new/nwg-launchers-0.4.0/meson.build 2020-09-22 02:41:14.000000000 +0200
@@ -4,7 +4,7 @@
                        'warning_level=3'
                        ],
        license: 'GPL-3.0-or-later',
-       version: '0.3.4'
+       version: '0.4.0'
 )
 
 compiler = meson.get_compiler('cpp')


Reply via email to