Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
plugin_manager_panel.hpp
Go to the documentation of this file.
1#pragma once
2
14
17#include <pulp/view/view.hpp>
18
19// iOS skip: pulp::host is not linked or installed on iOS (App Store policy
20// disallows dlopen of third-party plugins), so the host scanner headers are
21// not on the include path there. `__has_include` lets downstream code
22// include this header unconditionally and key feature presence on the
23// `PULP_VIEW_HAS_PLUGIN_MANAGER_PANEL` macro defined below.
24// PluginManagerPanel is an opt-in host-backed widget, not part of the
25// view-only link surface. Desktop consumers that instantiate it must link
26// both `pulp::view` and `pulp::host`.
27#if defined(__has_include) && __has_include(<pulp/host/scanner.hpp>)
28#define PULP_VIEW_HAS_PLUGIN_MANAGER_PANEL 1
29
31#include <pulp/host/scanner.hpp>
32
33#include <algorithm>
34#include <atomic>
35#include <chrono>
36#include <functional>
37#include <memory>
38#include <optional>
39#include <string>
40#include <unordered_map>
41#include <vector>
42
43namespace pulp::view {
44
46struct PluginManagerRow {
48 std::string name;
49 std::string path;
50 std::string reason;
51 std::int64_t last_scan_unix = 0;
52
53 // ── Item 4.3 — drag-add identity ─────────────────────────────────────
54 // The fields below are populated by the model so a drag-add into a
55 // SignalGraph can synthesize the PluginInfo without having to re-scan
56 // or look the row up by path. They are intentionally optional — older
57 // models that pre-date item 4.3 leave them at defaults and the
58 // resulting drop produces an "unresolved" graph node (PluginSlot::load
59 // is still attempted via `to_plugin_info`).
60 std::string manufacturer;
61 std::string version;
62 std::string unique_id;
63 int num_inputs = 2;
64 int num_outputs = 2;
65 bool is_instrument = false;
66 bool is_effect = true;
67
71 pulp::host::PluginInfo to_plugin_info() const {
72 pulp::host::PluginInfo info;
73 info.name = name;
74 info.manufacturer = manufacturer;
75 info.version = version;
76 info.path = path;
77 info.unique_id = unique_id;
78 info.format = format;
79 info.is_instrument = is_instrument;
80 info.is_effect = is_effect;
81 info.num_inputs = num_inputs;
82 info.num_outputs = num_outputs;
83 return info;
84 }
85};
86
88enum class PluginManagerBucket : std::uint8_t {
89 scanned = 0,
90 failed = 1,
91 blacklisted = 2,
92};
93
101class PluginManagerModel {
102 public:
103 virtual ~PluginManagerModel() = default;
104
108 virtual std::vector<PluginManagerRow> rows(PluginManagerBucket bucket) const = 0;
109
112 virtual std::vector<std::string> search_paths(pulp::host::PluginFormat fmt) const = 0;
113 virtual void add_search_path(pulp::host::PluginFormat fmt, std::string path) = 0;
114 virtual void remove_search_path(pulp::host::PluginFormat fmt, const std::string& path) = 0;
115
118 virtual void start_rescan() = 0;
119
122 virtual void start_rescan(const std::string& path) = 0;
123
125 virtual float progress_fraction() const = 0;
126
128 virtual bool is_scanning() const = 0;
129
132 virtual bool is_blacklisted(const std::string& path) const = 0;
133 virtual void set_blacklisted(const std::string& path, bool blacklisted,
134 const std::string& reason = "user") = 0;
135
139 virtual void reveal_in_file_manager(const std::string& /*path*/) {}
140};
141
142// ── Mock / in-memory model for tests and examples ────────────────────────
143
147class InMemoryPluginManagerModel : public PluginManagerModel {
148 public:
149 std::vector<PluginManagerRow> scanned_rows;
150 std::vector<PluginManagerRow> failed_rows;
151 std::unordered_map<int, std::vector<std::string>> paths_by_format;
152 pulp::host::ScanBlacklist blacklist;
153
154 // Externally observable progress state — tests can tick this by hand.
155 std::atomic<float> progress{1.0f};
156 std::atomic<bool> scanning{false};
157
160 int rescan_count = 0;
161 int single_rescan_count = 0;
162 std::string last_single_rescan_path;
163 int reveal_count = 0;
164 std::string last_reveal_path;
165
166 std::vector<PluginManagerRow> rows(PluginManagerBucket bucket) const override {
167 switch (bucket) {
168 case PluginManagerBucket::scanned:
169 return filter_scanned();
170 case PluginManagerBucket::failed:
171 return failed_rows;
172 case PluginManagerBucket::blacklisted:
173 return blacklist_rows();
174 }
175 return {};
176 }
177
178 std::vector<std::string> search_paths(pulp::host::PluginFormat fmt) const override {
179 auto it = paths_by_format.find(static_cast<int>(fmt));
180 if (it == paths_by_format.end())
181 return {};
182 return it->second;
183 }
184 void add_search_path(pulp::host::PluginFormat fmt, std::string path) override {
185 auto& v = paths_by_format[static_cast<int>(fmt)];
186 if (std::find(v.begin(), v.end(), path) == v.end())
187 v.push_back(std::move(path));
188 }
189 void remove_search_path(pulp::host::PluginFormat fmt, const std::string& path) override {
190 auto& v = paths_by_format[static_cast<int>(fmt)];
191 v.erase(std::remove(v.begin(), v.end(), path), v.end());
192 }
193
194 void start_rescan() override {
195 ++rescan_count;
196 }
197 void start_rescan(const std::string& path) override {
198 ++single_rescan_count;
199 last_single_rescan_path = path;
200 }
201 float progress_fraction() const override {
202 return progress.load();
203 }
204 bool is_scanning() const override {
205 return scanning.load();
206 }
207
208 bool is_blacklisted(const std::string& path) const override {
209 return blacklist.is_blacklisted(path);
210 }
211 void set_blacklisted(const std::string& path, bool on, const std::string& reason) override {
212 if (on)
213 blacklist.blacklist(path, reason.empty() ? "user" : reason);
214 else
215 blacklist.clear(path);
216 }
217 void reveal_in_file_manager(const std::string& path) override {
218 ++reveal_count;
219 last_reveal_path = path;
220 }
221
222 private:
223 std::vector<PluginManagerRow> filter_scanned() const {
224 // Scanned bucket excludes anything the user has blacklisted.
225 std::vector<PluginManagerRow> out;
226 out.reserve(scanned_rows.size());
227 for (const auto& r : scanned_rows) {
228 if (!blacklist.is_blacklisted(r.path))
229 out.push_back(r);
230 }
231 return out;
232 }
233 std::vector<PluginManagerRow> blacklist_rows() const {
234 std::vector<PluginManagerRow> out;
235 for (const auto& [path, entry] : blacklist.entries()) {
236 PluginManagerRow row;
237 row.path = path;
238 row.reason = entry.reason;
239 row.last_scan_unix = entry.mtime;
240 // Pull format / name from the scanned list if we remember it.
241 for (const auto& s : scanned_rows) {
242 if (s.path == path) {
243 row.format = s.format;
244 row.name = s.name;
245 break;
246 }
247 }
248 out.push_back(std::move(row));
249 }
250 return out;
251 }
252};
253
254// ── The widget itself ────────────────────────────────────────────────────
255
271class PluginManagerPanel : public View {
272 public:
273 explicit PluginManagerPanel(PluginManagerModel& model) : model_(&model) {
274 set_focusable(true);
275 set_access_role(AccessRole::group);
276 set_access_label("Plugin manager");
277 refresh();
278 }
279
280 // ── Observable state (public for tests) ──────────────────────────────
281
284 void refresh() {
285 scanned_ = model_->rows(PluginManagerBucket::scanned);
286 failed_ = model_->rows(PluginManagerBucket::failed);
287 blacklisted_ = model_->rows(PluginManagerBucket::blacklisted);
288 apply_filter();
289 }
290
292 int visible_count(PluginManagerBucket b) const {
293 return static_cast<int>(visible(b).size());
294 }
295 const std::vector<PluginManagerRow>& rows(PluginManagerBucket b) const {
296 return visible(b);
297 }
298
300 void set_filter(std::string text) {
301 filter_ = std::move(text);
302 apply_filter();
303 }
304 const std::string& filter() const {
305 return filter_;
306 }
307
310 PluginManagerModel& model() {
311 return *model_;
312 }
313 const PluginManagerModel& model() const {
314 return *model_;
315 }
316
317 // ── Rescan button ────────────────────────────────────────────────────
318
322 void trigger_rescan() {
323 model_->start_rescan();
324 }
325
327 void trigger_rescan(const std::string& path) {
328 model_->start_rescan(path);
329 }
330
332 void toggle_blacklist(const std::string& path) {
333 const bool now = !model_->is_blacklisted(path);
334 model_->set_blacklisted(path, now, "user");
335 refresh();
336 }
337
339 void reveal_in_file_manager(const std::string& path) {
340 model_->reveal_in_file_manager(path);
341 }
342
343 // ── Per-format search paths ──────────────────────────────────────────
344
345 std::vector<std::string> search_paths(pulp::host::PluginFormat fmt) const {
346 return model_->search_paths(fmt);
347 }
348 void add_search_path(pulp::host::PluginFormat fmt, std::string path) {
349 model_->add_search_path(fmt, std::move(path));
350 }
351 void remove_search_path(pulp::host::PluginFormat fmt, const std::string& path) {
352 model_->remove_search_path(fmt, path);
353 }
354
355 // ── Context menu (right-click) ───────────────────────────────────────
356
358 const std::string& context_menu_path() const {
359 return context_menu_path_;
360 }
361 PluginManagerBucket context_menu_bucket() const {
362 return context_menu_bucket_;
363 }
364
367 enum class ContextItem : std::uint8_t {
368 rescan_this = 0,
369 toggle_blacklist = 1,
371 };
372 static std::vector<ContextItem> context_menu_items() {
373 return {ContextItem::rescan_this, ContextItem::toggle_blacklist,
374 ContextItem::reveal_in_file_manager};
375 }
376
379 std::string context_menu_label(ContextItem item) const {
380 switch (item) {
381 case ContextItem::rescan_this:
382 return "Rescan just this plugin";
383 case ContextItem::toggle_blacklist:
384 if (!context_menu_path_.empty() && model_->is_blacklisted(context_menu_path_))
385 return "Remove from blacklist";
386 return "Add to blacklist";
387 case ContextItem::reveal_in_file_manager:
388#if defined(_WIN32)
389 return "Show in Explorer";
390#elif defined(__APPLE__)
391 return "Show in Finder";
392#else
393 return "Show in file manager";
394#endif
395 }
396 return {};
397 }
398
402 void activate_context_item(ContextItem item) {
403 if (context_menu_path_.empty())
404 return;
405 switch (item) {
406 case ContextItem::rescan_this:
407 trigger_rescan(context_menu_path_);
408 break;
409 case ContextItem::toggle_blacklist:
410 toggle_blacklist(context_menu_path_);
411 break;
412 case ContextItem::reveal_in_file_manager:
413 reveal_in_file_manager(context_menu_path_);
414 break;
415 }
416 }
417
420 void open_context_menu(PluginManagerBucket b, const std::string& path) {
421 context_menu_bucket_ = b;
422 context_menu_path_ = path;
423 }
424 void close_context_menu() {
425 context_menu_path_.clear();
426 }
427
428 // ── Accessibility labels for the three columns ──────────────────────
429 //
430 // Exposed so external a11y checkers and tests can verify that each
431 // column carries a meaningful screen-reader label independent of the
432 // visual rendering. Values deliberately include the row counts.
433 std::string column_access_label(PluginManagerBucket b) const {
434 const auto& v = visible(b);
435 const char* name = b == PluginManagerBucket::scanned ? "Scanned plugins"
436 : b == PluginManagerBucket::failed ? "Failed plugins"
437 : "Blacklisted plugins";
438 return std::string(name) + " — " + std::to_string(v.size()) +
439 (v.size() == 1 ? " entry" : " entries");
440 }
441
442 // ── View overrides ──────────────────────────────────────────────────
443
444 void paint(canvas::Canvas& canvas) override {
445 const auto b = local_bounds();
446
447 canvas.set_fill_color(resolve_color("plugin_manager_bg", canvas::Color::hex(0x16213e)));
448 canvas.fill_rounded_rect(b.x, b.y, b.width, b.height, 6);
449
450 // ── Toolbar: search + rescan + progress bar ──────────────────────
451 const float toolbar_h = 28.0f;
452 canvas.set_fill_color(resolve_color("surface", canvas::Color::hex(0x1a1a2e)));
453 canvas.fill_rect(b.x, b.y, b.width, toolbar_h);
454
455 canvas.set_font("system", 12);
456 canvas.set_fill_color(resolve_color("text", canvas::Color::hex(0xe0e0e0)));
457 const std::string filter_label =
458 filter_.empty() ? "Search: (type to filter)" : "Search: " + filter_;
459 canvas.fill_text(filter_label, b.x + 8, b.y + 18);
460
461 const float button_w = 96.0f;
462 const float button_x = b.x + b.width - button_w - 8.0f;
463 canvas.set_fill_color(resolve_color("accent", canvas::Color::hex(0xe94560)));
464 canvas.fill_rounded_rect(button_x, b.y + 4, button_w, toolbar_h - 8, 4);
465 canvas.set_fill_color(canvas::Color::hex(0xffffff));
466 canvas.fill_text(model_->is_scanning() ? "Scanning…" : "Rescan", button_x + 12, b.y + 18);
467
468 if (model_->is_scanning()) {
469 const float pb_x = b.x + b.width - button_w - 8.0f - 120.0f;
470 const float pb_w = 110.0f;
471 canvas.set_fill_color(resolve_color("progress_track", canvas::Color::hex(0x0f3460)));
472 canvas.fill_rect(pb_x, b.y + 10, pb_w, 8);
473 canvas.set_fill_color(resolve_color("progress_fill", canvas::Color::hex(0x4ade80)));
474 const float f = std::clamp(model_->progress_fraction(), 0.0f, 1.0f);
475 canvas.fill_rect(pb_x, b.y + 10, pb_w * f, 8);
476 }
477
478 // ── Three columns ───────────────────────────────────────────────
479 const float col_w = (b.width - 16.0f) / 3.0f;
480 const float col_y = b.y + toolbar_h + 4.0f;
481 const float col_h = b.height - toolbar_h - 8.0f;
482
483 paint_column(canvas, b.x + 4.0f, col_y, col_w, col_h, PluginManagerBucket::scanned,
484 "Scanned");
485 paint_column(canvas, b.x + 4.0f + col_w, col_y, col_w, col_h, PluginManagerBucket::failed,
486 "Failed");
487 paint_column(canvas, b.x + 4.0f + col_w * 2.0f, col_y, col_w, col_h,
488 PluginManagerBucket::blacklisted, "Blacklisted");
489 }
490
491 void on_mouse_event(const MouseEvent& event) override {
492 if (!event.is_down)
493 return;
494
495 const auto b = local_bounds();
496 const float toolbar_h = 28.0f;
497
498 // Rescan button
499 const float button_w = 96.0f;
500 const float button_x = b.x + b.width - button_w - 8.0f;
501 if (event.position.y < toolbar_h && event.position.x >= button_x &&
502 event.position.x <= button_x + button_w) {
503 trigger_rescan();
504 return;
505 }
506
507 // Column hit test
508 const float col_w = (b.width - 16.0f) / 3.0f;
509 const int col_idx = static_cast<int>((event.position.x - 4.0f) / col_w);
510 const PluginManagerBucket bucket = col_idx <= 0 ? PluginManagerBucket::scanned
511 : col_idx == 1 ? PluginManagerBucket::failed
512 : PluginManagerBucket::blacklisted;
513
514 const float col_y = toolbar_h + 4.0f;
515 const float header_h = 20.0f;
516 const float row_y = event.position.y - col_y - header_h;
517 if (row_y < 0)
518 return;
519
520 const auto& rows_vec = visible(bucket);
521 const int row = static_cast<int>(row_y / row_height_);
522 if (row < 0 || row >= static_cast<int>(rows_vec.size())) {
523 close_context_menu();
524 return;
525 }
526
527 const auto& hit = rows_vec[static_cast<size_t>(row)];
528 if (event.button == MouseButton::right) {
529 open_context_menu(bucket, hit.path);
530 } else {
531 selected_path_ = hit.path;
532 selected_bucket_ = bucket;
533 if (on_row_selected)
534 on_row_selected(bucket, hit);
535 }
536 }
537
538 // ── Drag-add into a SignalGraph (item 4.3) ───────────────────────────
539 //
540 // The panel itself is rendering-only — it does not know what surface
541 // (graph editor, mixer column, etc.) the user is dropping onto. We
542 // emit a `on_row_drag_start` callback when the user presses on a
543 // scanned row and drags past `drag_threshold_px_`. The host is
544 // responsible for taking the row's `PluginInfo` and adding the node
545 // to whichever `SignalGraph` the drop landed on (see
546 // `pulp::host::add_plugin_node_from_drop`).
547 //
548 // The drag callback fires only for rows in the `scanned` bucket —
549 // dragging a failed or blacklisted entry into the graph would create
550 // a node that can never load, so the panel suppresses it.
551
552 void on_mouse_down(Point pos) override {
553 drag_origin_ = pos;
554 drag_origin_set_ = true;
555 drag_started_ = false;
556 drag_row_ = identify_row_at(pos);
557 }
558 void on_mouse_drag(Point pos) override {
559 if (drag_origin_set_ && !drag_started_ && drag_row_.has_value()) {
560 const float dx = pos.x - drag_origin_.x;
561 const float dy = pos.y - drag_origin_.y;
562 if (dx * dx + dy * dy >= drag_threshold_px_ * drag_threshold_px_) {
563 drag_started_ = true;
564 if (on_row_drag_start && drag_row_->first == PluginManagerBucket::scanned) {
565 on_row_drag_start(drag_row_->first, drag_row_->second, pos);
566 }
567 }
568 }
569 }
570 void on_mouse_up(Point pos) override {
571 if (drag_started_ && drag_row_.has_value() && on_row_drag_end) {
572 on_row_drag_end(drag_row_->first, drag_row_->second, pos);
573 }
574 drag_origin_set_ = false;
575 drag_started_ = false;
576 drag_row_.reset();
577 }
578 bool wants_mouse_input() const override {
579 return true;
580 }
581
587 bool simulate_row_drag(PluginManagerBucket bucket, const std::string& path,
588 Point drop_position = {0, 0}) {
589 const auto& rows_vec = visible(bucket);
590 for (const auto& r : rows_vec) {
591 if (r.path == path) {
592 if (bucket == PluginManagerBucket::scanned && on_row_drag_start) {
593 on_row_drag_start(bucket, r, drop_position);
594 }
595 if (on_row_drag_end) {
596 on_row_drag_end(bucket, r, drop_position);
597 }
598 return true;
599 }
600 }
601 return false;
602 }
603
606 float drag_threshold_px() const {
607 return drag_threshold_px_;
608 }
609 void set_drag_threshold_px(float px) {
610 drag_threshold_px_ = px;
611 }
612
615 std::function<void(PluginManagerBucket, const PluginManagerRow&, Point)> on_row_drag_start;
616
619 std::function<void(PluginManagerBucket, const PluginManagerRow&, Point)> on_row_drag_end;
620
621 bool on_key_event(const KeyEvent& event) override {
622 if (!event.is_down)
623 return false;
624 // Type-to-filter: printable keys land in `on_text_input` on most
625 // backends, but we also support backspace here so focus-only flows
626 // still work in tests.
627 if (event.key == KeyCode::backspace && !filter_.empty()) {
628 filter_.pop_back();
629 apply_filter();
630 return true;
631 }
632 return false;
633 }
634 void on_text_input(const TextInputEvent& event) override {
635 filter_ += event.text;
636 apply_filter();
637 }
638
639 // Callback fired when a row is left-clicked. Optional.
640 std::function<void(PluginManagerBucket, const PluginManagerRow&)> on_row_selected;
641
642 // Row height is exposed so tests can synthesize click coordinates.
643 float row_height() const {
644 return row_height_;
645 }
646 void set_row_height(float h) {
647 row_height_ = h;
648 }
649
650 private:
651 PluginManagerModel* model_;
652
653 std::vector<PluginManagerRow> scanned_, failed_, blacklisted_;
654 std::vector<PluginManagerRow> scanned_view_, failed_view_, blacklisted_view_;
655
656 std::string filter_;
657 std::string selected_path_;
658 PluginManagerBucket selected_bucket_ = PluginManagerBucket::scanned;
659
660 std::string context_menu_path_;
661 PluginManagerBucket context_menu_bucket_ = PluginManagerBucket::scanned;
662
663 float row_height_ = 22.0f;
664
665 // Drag-add state (item 4.3)
666 Point drag_origin_{0, 0};
667 bool drag_origin_set_ = false;
668 bool drag_started_ = false;
669 std::optional<std::pair<PluginManagerBucket, PluginManagerRow>> drag_row_;
670 float drag_threshold_px_ = 4.0f;
671
672 std::optional<std::pair<PluginManagerBucket, PluginManagerRow>>
673 identify_row_at(Point pos) const {
674 const auto b = local_bounds();
675 const float toolbar_h = 28.0f;
676 if (pos.y < toolbar_h)
677 return std::nullopt;
678
679 const float col_w = (b.width - 16.0f) / 3.0f;
680 const int col_idx = static_cast<int>((pos.x - 4.0f) / col_w);
681 const PluginManagerBucket bucket = col_idx <= 0 ? PluginManagerBucket::scanned
682 : col_idx == 1 ? PluginManagerBucket::failed
683 : PluginManagerBucket::blacklisted;
684
685 const float header_h = 20.0f;
686 const float row_y = pos.y - toolbar_h - 4.0f - header_h;
687 if (row_y < 0)
688 return std::nullopt;
689
690 const auto& rows_vec = visible(bucket);
691 const int row = static_cast<int>(row_y / row_height_);
692 if (row < 0 || row >= static_cast<int>(rows_vec.size()))
693 return std::nullopt;
694 return std::make_pair(bucket, rows_vec[static_cast<size_t>(row)]);
695 }
696
697 const std::vector<PluginManagerRow>& visible(PluginManagerBucket b) const {
698 switch (b) {
699 case PluginManagerBucket::scanned:
700 return scanned_view_;
701 case PluginManagerBucket::failed:
702 return failed_view_;
703 case PluginManagerBucket::blacklisted:
704 return blacklisted_view_;
705 }
706 return scanned_view_;
707 }
708
709 static std::string to_lower(std::string s) {
710 std::transform(s.begin(), s.end(), s.begin(),
711 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
712 return s;
713 }
714
715 void apply_filter() {
716 scanned_view_ = apply_filter_to(scanned_);
717 failed_view_ = apply_filter_to(failed_);
718 blacklisted_view_ = apply_filter_to(blacklisted_);
719 }
720 std::vector<PluginManagerRow> apply_filter_to(const std::vector<PluginManagerRow>& in) const {
721 if (filter_.empty())
722 return in;
723 const std::string f = to_lower(filter_);
724 std::vector<PluginManagerRow> out;
725 out.reserve(in.size());
726 for (const auto& r : in) {
727 if (to_lower(r.name).find(f) != std::string::npos ||
728 to_lower(r.path).find(f) != std::string::npos) {
729 out.push_back(r);
730 }
731 }
732 return out;
733 }
734
735 static const char* format_label(pulp::host::PluginFormat f) {
736 switch (f) {
738 return "VST3";
740 return "AU";
742 return "AUv3";
744 return "CLAP";
746 return "LV2";
747 }
748 return "?";
749 }
750
751 static std::string format_time(std::int64_t unix_seconds) {
752 if (unix_seconds <= 0)
753 return "never";
754 std::time_t t = static_cast<std::time_t>(unix_seconds);
755 std::tm tm{};
756#if defined(_WIN32)
757 localtime_s(&tm, &t);
758#else
759 localtime_r(&t, &tm);
760#endif
761 char buf[32];
762 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", &tm);
763 return buf;
764 }
765
766 void paint_column(canvas::Canvas& canvas, float x, float y, float w, float h,
767 PluginManagerBucket bucket, const char* title) {
768 canvas.set_fill_color(resolve_color("list_bg", canvas::Color::hex(0x0d1b33)));
769 canvas.fill_rect(x, y, w, h);
770
771 // Column header
772 const float header_h = 20.0f;
773 canvas.set_fill_color(resolve_color("surface", canvas::Color::hex(0x1a1a2e)));
774 canvas.fill_rect(x, y, w, header_h);
775
776 canvas.set_font("system", 12);
777 canvas.set_fill_color(resolve_color("text", canvas::Color::hex(0xe0e0e0)));
778 const std::string label =
779 std::string(title) + " (" + std::to_string(visible(bucket).size()) + ")";
780 canvas.fill_text(label, x + 6, y + 14);
781
782 // Rows
783 canvas.save();
784 canvas.clip_rect(x, y + header_h, w, h - header_h);
785 float ry = y + header_h + 2.0f;
786 const auto& rows_vec = visible(bucket);
787 for (const auto& r : rows_vec) {
788 canvas.set_font("system", 11);
789 const std::string fmt_name = format_label(r.format);
790 const std::string headline = fmt_name + " " + (r.name.empty() ? r.path : r.name);
791 canvas.set_fill_color(resolve_color("text", canvas::Color::hex(0xe0e0e0)));
792 canvas.fill_text(headline, x + 6, ry + 12);
793 canvas.set_fill_color(resolve_color("text_muted", canvas::Color::hex(0x808090)));
794 canvas.set_font("system", 10);
795 canvas.fill_text(format_time(r.last_scan_unix), x + 6, ry + 24);
796 ry += row_height_;
797 if (ry > y + h)
798 break;
799 }
800 canvas.restore();
801 }
802};
803
804} // namespace pulp::view
805
806#else // !__has_include(<pulp/host/scanner.hpp>)
807#define PULP_VIEW_HAS_PLUGIN_MANAGER_PANEL 0
808#endif
void clear(const std::string &path)
Unconditionally remove path from the blacklist.
Definition scan_blacklist.hpp:42
const std::unordered_map< std::string, BlacklistEntry > & entries() const
Definition scan_blacklist.hpp:60
void blacklist(const std::string &path, std::string reason)
Record that scanning path failed.
bool is_blacklisted(const std::string &path) const
Definition scan_blacklist.hpp:45
Definition view.hpp:73
Input event types with modifier keys and pointer IDs.
@ path
Definition scene_node.hpp:93
void apply_filter(HostQuirks &q, QuirkFilter filter)
In-place: zero out (reset to default) every field in q whose meta tier is not allowed by filter.
PluginFormat
Definition scanner.hpp:23
@ VST3
Definition scanner.hpp:24
@ LV2
Definition scanner.hpp:28
@ CLAP
Definition scanner.hpp:27
@ AudioUnit
Definition scanner.hpp:25
@ AudioUnitV3
Definition scanner.hpp:26
@ f
Definition pitch.hpp:25
@ b
Definition pitch.hpp:31
RevealOutcome reveal_in_file_manager(const std::filesystem::path &path)
Show path in the platform file manager: Finder (NSWorkspace activateFileViewerSelectingURLs),...
@ info
Definition gpu_diagnostics.hpp:56
@ size
Definition config.hpp:216
@ progress
Definition finite_stretch_builder.hpp:30
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
@ px
Absolute pixels.
Definition geometry.hpp:393
@ visible
Content renders beyond bounds (default for views).
Definition geometry.hpp:384
@ s
Definition input_events.hpp:41
@ h
Definition input_events.hpp:39
@ y
Definition input_events.hpp:42
@ r
Definition input_events.hpp:41
@ v
Definition input_events.hpp:41
@ t
Definition input_events.hpp:41
@ row
Definition design_ir.hpp:53
@ failed
Definition gesture.hpp:20