27#if defined(__has_include) && __has_include(<pulp/host/scanner.hpp>)
28#define PULP_VIEW_HAS_PLUGIN_MANAGER_PANEL 1
40#include <unordered_map>
46struct PluginManagerRow {
51 std::int64_t last_scan_unix = 0;
60 std::string manufacturer;
62 std::string unique_id;
65 bool is_instrument =
false;
66 bool is_effect =
true;
71 pulp::host::PluginInfo to_plugin_info()
const {
72 pulp::host::PluginInfo
info;
74 info.manufacturer = manufacturer;
75 info.version = version;
77 info.unique_id = unique_id;
79 info.is_instrument = is_instrument;
80 info.is_effect = is_effect;
81 info.num_inputs = num_inputs;
82 info.num_outputs = num_outputs;
88enum class PluginManagerBucket : std::uint8_t {
101class PluginManagerModel {
103 virtual ~PluginManagerModel() =
default;
108 virtual std::vector<PluginManagerRow> rows(PluginManagerBucket bucket)
const = 0;
118 virtual void start_rescan() = 0;
122 virtual void start_rescan(
const std::string& path) = 0;
125 virtual float progress_fraction()
const = 0;
128 virtual bool is_scanning()
const = 0;
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;
147class InMemoryPluginManagerModel :
public PluginManagerModel {
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;
156 std::atomic<bool> scanning{
false};
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;
166 std::vector<PluginManagerRow> rows(PluginManagerBucket bucket)
const override {
168 case PluginManagerBucket::scanned:
169 return filter_scanned();
170 case PluginManagerBucket::failed:
172 case PluginManagerBucket::blacklisted:
173 return blacklist_rows();
179 auto it = paths_by_format.find(
static_cast<int>(fmt));
180 if (it == paths_by_format.end())
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));
190 auto&
v = paths_by_format[
static_cast<int>(fmt)];
191 v.erase(std::remove(
v.begin(),
v.end(), path),
v.end());
194 void start_rescan()
override {
197 void start_rescan(
const std::string& path)
override {
198 ++single_rescan_count;
199 last_single_rescan_path =
path;
201 float progress_fraction()
const override {
204 bool is_scanning()
const override {
205 return scanning.load();
208 bool is_blacklisted(
const std::string& path)
const override {
211 void set_blacklisted(
const std::string& path,
bool on,
const std::string& reason)
override {
213 blacklist.
blacklist(path, reason.empty() ?
"user" : reason);
215 blacklist.
clear(path);
219 last_reveal_path =
path;
223 std::vector<PluginManagerRow> filter_scanned()
const {
225 std::vector<PluginManagerRow> out;
226 out.reserve(scanned_rows.size());
227 for (
const auto& r : scanned_rows) {
233 std::vector<PluginManagerRow> blacklist_rows()
const {
234 std::vector<PluginManagerRow> out;
235 for (
const auto& [path, entry] : blacklist.
entries()) {
236 PluginManagerRow
row;
238 row.reason = entry.reason;
239 row.last_scan_unix = entry.mtime;
241 for (
const auto& s : scanned_rows) {
242 if (
s.path == path) {
243 row.format =
s.format;
248 out.push_back(std::move(row));
271class PluginManagerPanel :
public View {
273 explicit PluginManagerPanel(PluginManagerModel& model) : model_(&model) {
275 set_access_role(AccessRole::group);
276 set_access_label(
"Plugin manager");
285 scanned_ = model_->rows(PluginManagerBucket::scanned);
286 failed_ = model_->rows(PluginManagerBucket::failed);
287 blacklisted_ = model_->rows(PluginManagerBucket::blacklisted);
292 int visible_count(PluginManagerBucket b)
const {
293 return static_cast<int>(
visible(b).size());
295 const std::vector<PluginManagerRow>& rows(PluginManagerBucket b)
const {
300 void set_filter(std::string text) {
301 filter_ = std::move(text);
304 const std::string& filter()
const {
310 PluginManagerModel& model() {
313 const PluginManagerModel& model()
const {
322 void trigger_rescan() {
323 model_->start_rescan();
327 void trigger_rescan(
const std::string& path) {
328 model_->start_rescan(path);
332 void toggle_blacklist(
const std::string& path) {
333 const bool now = !model_->is_blacklisted(path);
334 model_->set_blacklisted(path, now,
"user");
340 model_->reveal_in_file_manager(path);
346 return model_->search_paths(fmt);
349 model_->add_search_path(fmt, std::move(path));
352 model_->remove_search_path(fmt, path);
358 const std::string& context_menu_path()
const {
359 return context_menu_path_;
361 PluginManagerBucket context_menu_bucket()
const {
362 return context_menu_bucket_;
367 enum class ContextItem : std::uint8_t {
369 toggle_blacklist = 1,
372 static std::vector<ContextItem> context_menu_items() {
373 return {ContextItem::rescan_this, ContextItem::toggle_blacklist,
374 ContextItem::reveal_in_file_manager};
379 std::string context_menu_label(ContextItem item)
const {
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:
389 return "Show in Explorer";
390#elif defined(__APPLE__)
391 return "Show in Finder";
393 return "Show in file manager";
402 void activate_context_item(ContextItem item) {
403 if (context_menu_path_.empty())
406 case ContextItem::rescan_this:
407 trigger_rescan(context_menu_path_);
409 case ContextItem::toggle_blacklist:
410 toggle_blacklist(context_menu_path_);
412 case ContextItem::reveal_in_file_manager:
420 void open_context_menu(PluginManagerBucket b,
const std::string& path) {
421 context_menu_bucket_ =
b;
422 context_menu_path_ =
path;
424 void close_context_menu() {
425 context_menu_path_.clear();
433 std::string column_access_label(PluginManagerBucket b)
const {
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");
444 void paint(canvas::Canvas& canvas)
override {
445 const auto b = local_bounds();
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);
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);
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);
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);
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);
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;
483 paint_column(canvas,
b.x + 4.0f, col_y, col_w, col_h, PluginManagerBucket::scanned,
485 paint_column(canvas,
b.x + 4.0f + col_w, col_y, col_w, col_h, PluginManagerBucket::failed,
487 paint_column(canvas,
b.x + 4.0f + col_w * 2.0f, col_y, col_w, col_h,
488 PluginManagerBucket::blacklisted,
"Blacklisted");
491 void on_mouse_event(
const MouseEvent& event)
override {
495 const auto b = local_bounds();
496 const float toolbar_h = 28.0f;
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) {
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;
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;
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();
527 const auto& hit = rows_vec[
static_cast<size_t>(
row)];
528 if (event.button == MouseButton::right) {
529 open_context_menu(bucket, hit.path);
531 selected_path_ = hit.path;
532 selected_bucket_ = bucket;
534 on_row_selected(bucket, hit);
552 void on_mouse_down(Point pos)
override {
554 drag_origin_set_ =
true;
555 drag_started_ =
false;
556 drag_row_ = identify_row_at(pos);
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);
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);
574 drag_origin_set_ =
false;
575 drag_started_ =
false;
578 bool wants_mouse_input()
const override {
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);
595 if (on_row_drag_end) {
596 on_row_drag_end(bucket, r, drop_position);
606 float drag_threshold_px()
const {
607 return drag_threshold_px_;
609 void set_drag_threshold_px(
float px) {
610 drag_threshold_px_ =
px;
615 std::function<void(PluginManagerBucket,
const PluginManagerRow&, Point)> on_row_drag_start;
619 std::function<void(PluginManagerBucket,
const PluginManagerRow&, Point)> on_row_drag_end;
621 bool on_key_event(
const KeyEvent& event)
override {
627 if (event.key == KeyCode::backspace && !filter_.empty()) {
634 void on_text_input(
const TextInputEvent& event)
override {
635 filter_ +=
event.text;
640 std::function<void(PluginManagerBucket,
const PluginManagerRow&)> on_row_selected;
643 float row_height()
const {
646 void set_row_height(
float h) {
651 PluginManagerModel* model_;
653 std::vector<PluginManagerRow> scanned_, failed_, blacklisted_;
654 std::vector<PluginManagerRow> scanned_view_, failed_view_, blacklisted_view_;
657 std::string selected_path_;
658 PluginManagerBucket selected_bucket_ = PluginManagerBucket::scanned;
660 std::string context_menu_path_;
661 PluginManagerBucket context_menu_bucket_ = PluginManagerBucket::scanned;
663 float row_height_ = 22.0f;
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;
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)
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;
685 const float header_h = 20.0f;
686 const float row_y = pos.y - toolbar_h - 4.0f - header_h;
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()))
694 return std::make_pair(bucket, rows_vec[
static_cast<size_t>(row)]);
697 const std::vector<PluginManagerRow>&
visible(PluginManagerBucket b)
const {
699 case PluginManagerBucket::scanned:
700 return scanned_view_;
701 case PluginManagerBucket::failed:
703 case PluginManagerBucket::blacklisted:
704 return blacklisted_view_;
706 return scanned_view_;
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)); });
716 scanned_view_ = apply_filter_to(scanned_);
717 failed_view_ = apply_filter_to(failed_);
718 blacklisted_view_ = apply_filter_to(blacklisted_);
720 std::vector<PluginManagerRow> apply_filter_to(
const std::vector<PluginManagerRow>& in)
const {
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) {
751 static std::string format_time(std::int64_t unix_seconds) {
752 if (unix_seconds <= 0)
754 std::time_t
t =
static_cast<std::time_t
>(unix_seconds);
757 localtime_s(&tm, &t);
759 localtime_r(&t, &tm);
762 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M", &tm);
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);
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);
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);
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);
807#define PULP_VIEW_HAS_PLUGIN_MANAGER_PANEL 0
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
@ path
Definition scene_node.hpp:93
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
@ 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