The script filters the active playlist. Clicking on a first value sends all items in the source playlist tagged with the value to a new playlist named "Tag Results" and activates it, giving it focus. Subsequent selections will now filter this "Tag Results" playlist.
The panel automatically updates itself with all values found in the playlist with each selection. Deselecting all values in the panel reactivates the source playlist, returning focus to it.
I've not yet been able to locate the original post so as to give credit to the script's author. I think I might have made some cosmetic changes to the script but I'm not entirely certain.
function RGB(r,g,b){ return (0xff000000|(r<<16)|(g<<8)|(b)); }
IDC_ARROW = 32512;
IDC_HAND = 32649;
// window.SetCursor()
NOT_FOUND = 4294967295;
// MetaFind() and elsewhere for consistency
/**
Colors, coordinates, etc
**/
// General
meta_field = "genre";
tag_delimiter = ";";
working_playlist_name = "Tag Results";
playing_playlist_name = "Tag Results (Playback)";
font_std = gdi.Font("Segoe UI", 10, 0);
font_bold = gdi.Font("Segoe UI", 10, 1);
font_ital = gdi.Font("Segoe UI", 10, 2);
// Tags
t_x = 8;
t_y = 8;
t_spc = 8;
little_x = "ˣ";
label_color = RGB(151, 181, 192);
label_text_color = RGB(252, 252, 252);
fallback_text = "nothing here";
fallback_text_color = RGB(205, 205, 205);
// Status
s_x = 24;
s_y = 32;
status_line = "Found %t %btag%s in playlist:"
status_text_color = RGB(49, 49, 49);
// Columns
c_y = 55;
row_h = 24;
row_w = .8; // %
column_indent = 8;
no_of_columns = 2;
scroll_step = 1;
text_omitted = " ...";
tag_name_color = RGB(49, 49, 49);
alt_row_color = RGB(241, 246, 243);
/**
Playlist dict
**/
function build_dict() {
tag_dict = { };
for (n = 0; n < playlistdb.Count; n++) {
track = playlistdb.Item(n);
metadb = track.GetFileInfo();
tag_idx = metadb.MetaFind(meta_field);
if (tag_idx == NOT_FOUND)
continue;
tag_cnt = metadb.MetaValueCount(tag_idx);
for (t = 0; t < tag_cnt; t++) {
tag = metadb.MetaValue(tag_idx, t);
dict_push(tag, track, tag_dict);
}
}
}
function get_keys(dict) {
keys = [];
for (key in dict)
keys.push(key);
return keys;
}
function dict_push(key, v, dict) {
if (!(key in dict))
dict[key] = plman.GetPlaylistItems(-1);
dict[key].Add(v);
}
/**
Playlist utility
**/
function assess_playlist() {
playlistdb = plman.GetPlaylistItems(parent_playlist);
build_dict();
playlistdb.RemoveAll();
}
function playlist_add(tag) {
new_tag = tag_dict[tag];
switch (mode) {
case "&&":
playlistdb = new_tag.Clone();
build_dict();
break;
case "||":
playlistdb.AddRange(new_tag);
break;
}
}
function remake_playlist() {
assess_playlist();
for (tag in active_tags) {
playlist_add(tag);
}
}
function update_playlist() {
suppress_pl_events = true;
switch (working_playlist_index) {
case fb.PlayingPlaylist:
freeze_playlist();
case NOT_FOUND:
make_playlist();
}
focus_playlist();
clear_playlist();
plman.InsertPlaylistItems(working_playlist_index, 0, playlistdb);
}
function make_playlist() {
working_playlist_index = fb.PlaylistCount;
fb.CreatePlaylist(working_playlist_index, working_playlist_name);
}
function focus_playlist() {
fb.ActivePlaylist = working_playlist_index;
}
function clear_playlist() {
selection = [];
p_c = fb.PlaylistItemCount(fb.ActivePlaylist);
for (idx = 0; idx < p_c; idx++) {
selection.push(idx);
}
plman.SetPlaylistSelection(fb.ActivePlaylist, selection, true);
plman.RemovePlaylistSelection(fb.ActivePlaylist);
}
function freeze_playlist() {
fb.RenamePlaylist(working_playlist_index, playing_playlist_name);
playing_playlist_index = working_playlist_index;
}
// ~~~
function locate_playlists() {
// Called on init and on_playlists_changed()
working_playlist_index = NOT_FOUND,
playing_playlist_index = NOT_FOUND;
for (pl_idx = 0; pl_idx < fb.PlaylistCount; pl_idx++) {
pl_name = plman.GetPlaylistName(pl_idx);
if (pl_name == working_playlist_name) {
working_playlist_index = pl_idx;
}
else if (pl_name == playing_playlist_name) {
playing_playlist_index = pl_idx;
}
}
}
/**
Tags
**/
function draw_labels(gr) {
x = t_x;
y = t_y;
run_w = t_x;
if (len_active_tags() == 0) {
w = gr.CalcTextWidth(fallback_text, font_ital),
h = gr.CalcTextHeight(fallback_text, font_ital);
gr.GdiDrawText(fallback_text, font_ital, fallback_text_color, x, y, w, h);
}
else {
for (tag in active_tags) {
active_tags[tag].draw(gr);
}
}
}
label = function(tag) {
this.name = tag;
this.draw = function(gr) {
txt_w = gr.CalcTextWidth(this.name, font_std),
txt_h = gr.CalcTextHeight(this.name, font_std);
x = t_x + run_w;
y = t_y;
txt_pad_r = txt_h,
txt_pad_l = txt_h/2,
bg_w = txt_pad_l + txt_w + txt_pad_r,
bg_h = txt_h+3,
bg_arc = bg_h/2,
text_x = x+txt_pad_l,
text_y = y+1,
close_x = x+txt_w+txt_pad_r-3,
close_y = y+4;
// ~~~
gr.SetSmoothingMode(2);
gr.FillRoundRect(x, y, bg_w, bg_h, bg_arc, bg_arc, label_color);
gr.GdiDrawText(this.name, font_std, label_text_color, text_x, text_y, txt_w, txt_h);
gr.GdiDrawText(little_x, font_std, label_text_color, close_x, close_y, txt_h, txt_h, label_color);
// ~~~
run_w += bg_w + t_spc;
this.left = x,
this.right = x+bg_w,
this.top = y,
this.bottom = y+bg_h;
}
this.check_mouse = function(evt, x, y) {
mouse_is_over = (x >= this.left && x <= this.right &&
y >= this.top && y <= this.bottom);
if (mouse_is_over) {
switch (evt) {
case "up":
dismiss(this.name);
break;
case "move":
set_hand_cursor = true;
break;
}
}
}
}
function len_active_tags() {
n = 0;
for (tag in active_tags)
n++;
return n;
}
/**
Status
**/
function say_status(gr) {
text = status_line;
text = text.replace("%t", tags.tags.length)
.replace("%b", len_active_tags() > 0 && mode == "&&" ? "sub" : "")
.replace("%s", tags.tags.length == 1 ? "" : "s")
.replace("%a", len_active_tags())
.replace("%S", len_active_tags() == 1 ? "" : "s")
.replace("%p", fb.PlaylistItemCount(fb.ActivePlaylist));
text_w = gr.CalcTextWidth(text, font_ital);
text_h = gr.CalcTextHeight(text, font_ital);
gr.GdiDrawText(text, font_ital, status_text_color, s_x, s_y, text_w, text_h);
}
/**
Columns
**/
tag_list = function() {
this.update = function() {
this.offset = 0;
this.tags = get_keys(tag_dict);
this.tags.sort();
// ~~~
space_avail = (wh-c_y);
total_rows = Math.ceil(this.tags.length/no_of_columns);
rows_possible = Math.floor(space_avail/row_h)-1;
this.visible_rows = Math.min(rows_possible, total_rows);
this.out_of_bounds = total_rows - this.visible_rows;
}
this.draw_rows = function(gr) {
h = row_h;
w = row_w*ww;
x = (ww-w)/2;
y = c_y;
min = this.offset;
max = this.offset+this.visible_rows;
for (n = min; n < max; n++) {
if (n%2 == 0) {
gr.FillSolidRect(x, y, w, h, alt_row_color);
}
y += h;
}
this.left = x;
this.right = x+w;
this.top = c_y;
this.bottom = c_y + this.visible_rows*h;
this.column_w = w/no_of_columns;
}
this.populate = function(gr) {
th = gr.CalcTextHeight(".", font_std);
// Should be consistent
x = this.left + column_indent;
y = this.top + (row_h-th)/2-1;
min = no_of_columns*this.offset;
max = Math.min(no_of_columns*(this.offset+this.visible_rows), this.tags.length);
for (n = min; n < max; n++) {
tag = this.tags[n];
font = font_std;
if (tag in active_tags)
font = font_bold;
tw = gr.CalcTextWidth(tag, font);
while (column_indent + tw > this.column_w) {
if (tag.length == 0) break;
tag = tag.slice(0, -1);
tw = gr.CalcTextWidth(tag + text_omitted, font);
}
if (tag != this.tags[n]) tag += text_omitted;
gr.GdiDrawText(tag, font, tag_name_color, x, y, tw, th);
x += this.column_w;
if ((n+1)%no_of_columns == 0) {
x = this.left+column_indent;
y += row_h;
}
}
}
this.check_mouse = function(x, y) {
mouse_is_over = (x >= this.left && x <= this.right &&
y >= this.top && y <= this.bottom);
if (mouse_is_over) {
idx = this.check_index(x, y);
if (idx < this.tags.length) {
this.toggle(idx);
}
}
}
this.toggle = function(idx) {
tag = this.tags[idx];
if (tag in active_tags)
dismiss(tag);
else
activate(tag);
}
this.check_index = function(x, y) {
idx = no_of_columns*this.offset;
idx += Math.floor((y-this.top)/row_h)*no_of_columns;
idx += Math.floor((x-this.left)/this.column_w);
return idx;
}
this.jump_to_index = function(chr) {
for (t = 0; t < this.tags.length; t++) {
tag = this.tags[t];
first_chr = tag.charAt(0).toUpperCase();
if (chr == first_chr) {
new_offset = Math.floor(t/no_of_columns);
this.offset = Math.min(new_offset, this.out_of_bounds);
return;
}
}
}
}
function activate(tag) {
active_tags[tag] = new label(tag);
playlist_add(tag);
update_playlist();
if (mode == "&&") {
this.offset = 0;
tags.update();
}
}
function dismiss(tag) {
delete active_tags[tag];
if (len_active_tags() > 0) {
remake_playlist();
update_playlist();
}
else {
fb.ActivePlaylist = parent_playlist;
}
if (mode == "&&") {
this.offset = 0;
tags.update();
}
}
/**
Init, defaults, main events
**/
mode = "&&";
tags = new tag_list();
locate_playlists();
on_new_playlist();
function on_size() {
wh = window.Height,
ww = window.Width;
tags.update();
}
function on_paint(gr) {
draw_labels(gr);
tags.draw_rows(gr);
tags.populate(gr);
say_status(gr);
// ~~~
suppress_pl_events = false;
// Won't do at the end of update_playlist(). Not sure how else to execute this last.
}
/**
Mouse, key events
**/
function on_mouse_lbtn_up(x, y) {
tags.check_mouse(x, y);
for (tag in active_tags) {
active_tags[tag].check_mouse("up", x, y);
}
window.Repaint();
}
function on_mouse_move(x, y) {
set_hand_cursor = false;
for (tag in active_tags) {
active_tags[tag].check_mouse("move", x, y);
}
set_hand_cursor ? window.SetCursor(IDC_HAND) : window.SetCursor(IDC_ARROW);
}
function on_mouse_wheel(step) {
if (step > 0) {
tags.offset -= scroll_step;
tags.offset = Math.max(tags.offset, 0);
}
else {
tags.offset += scroll_step;
tags.offset = Math.min(tags.offset, tags.out_of_bounds);
}
window.Repaint();
}
function on_key_down(vkey) {
chr = String.fromCharCode(vkey);
tags.jump_to_index(chr);
// Gets awfully wonky with non-alpha characters, whatever "vkey" is doesn't
// mirror the common ASCII alphabet
window.Repaint();
}
/**
Playlist events
**/
function on_new_playlist() {
// Not a native event
active_tags = { };
parent_playlist = fb.ActivePlaylist;
assess_playlist();
}
function on_playlists_changed() {
if (suppress_pl_events) return;
// ~~~
locate_playlists();
}
function on_playback_new_track() {
if (playing_playlist_index != fb.PlayingPlaylist) {
fb.RemovePlaylist(playing_playlist_index);
}
}
function on_playlist_items_added() {
on_playlist_x();
}
function on_playlist_items_removed() {
on_playlist_x();
}
function on_playlist_switch() {
on_playlist_x();
}
var suppress_pl_events;
function on_playlist_x() {
if (suppress_pl_events) return;
// ~~~
on_new_playlist();
tags.update();
window.Repaint();
}