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')
