Skip to content

Commit c31e612

Browse files
committed
Add custom columns documentation
1 parent af07dbd commit c31e612

File tree

5 files changed

+484
-0
lines changed

5 files changed

+484
-0
lines changed

extending/custom_columns_api.rst

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
.. MusicBrainz Picard Documentation Project
2+
3+
Custom Columns API
4+
==================
5+
6+
Public API to define, register, sort, and persist custom columns for Picard's File and Album views.
7+
8+
.. note::
9+
This API was added in `PICARD-2103: Add API for custom columns (field ref, scripts, transforms, etc.) <https://github.com/metabrainz/picard/pull/2709>`_.
10+
11+
Overview and Flow
12+
-----------------
13+
14+
**Motivation**: Extend Picard's File/Album views with columns that compute values dynamically (from tags, scripts, or code) and integrate with sorting, sizing and visibility like built-ins.
15+
16+
**Lifecycle at a glance**:
17+
18+
- **Create**: Build a ``CustomColumn`` via factory helpers in ``picard.ui.itemviews.custom_columns.factory`` (e.g., ``make_field_column``, ``make_script_column``, ``make_callable_column``, ``make_transformed_column``) or construct ``CustomColumn`` with your own provider.
19+
- **Register**: Add the column to live views with ``registry.register(column, add_to_file_view=..., add_to_album_view=..., insert_after_key=...)``. This inserts into the mutable column collections used by the File/Album widgets.
20+
- **Persist** (optional): Store UI-defined columns as specs using ``picard.ui.itemviews.custom_columns.storage``. Use ``CustomColumnSpec`` + ``register_and_persist(spec)`` to save to config and auto-register; ``load_persisted_columns_once()`` restores saved columns on startup.
21+
- **Paint** (Qt): Once registered, the column participates like any other. Values come from the provider's ``evaluate(item)``. The header and sections are owned by the Qt views; width/resize hints are applied during registration (see ``registry._apply_column_width_to_headers``). Painting is handled by the standard header; only image columns overlay custom paint.
22+
23+
**Key modules**:
24+
25+
- ``custom_columns.__init__``: public API surface (factories, adapters, registry).
26+
- ``column.CustomColumn``: column type bridging providers to Picard's ``Column``.
27+
- ``factory``: helpers to create columns and infer sort behavior.
28+
- ``registry``: insertion/removal into File/Album views and live header updates.
29+
- ``providers``: reusable value providers and transforms.
30+
- ``sorting_adapters``: add ``.sort_key`` for ``ColumnSortType.SORTKEY`` sorting.
31+
- ``storage``: persist/load/register UI-defined column specs.
32+
33+
Imports
34+
-------
35+
36+
.. code-block:: python
37+
38+
from picard.ui.columns import ColumnAlign, ColumnSortType
39+
from picard.ui.itemviews.custom_columns import (
40+
CustomColumn,
41+
make_field_column,
42+
make_script_column,
43+
make_transformed_column,
44+
make_provider_column,
45+
make_callable_column,
46+
registry,
47+
# Sorting adapters
48+
CasefoldSortAdapter,
49+
DescendingCasefoldSortAdapter,
50+
NumericSortAdapter,
51+
DescendingNumericSortAdapter,
52+
LengthSortAdapter,
53+
RandomSortAdapter,
54+
ArticleInsensitiveAdapter,
55+
CompositeSortAdapter,
56+
NullsLastAdapter,
57+
NullsFirstAdapter,
58+
CachedSortAdapter,
59+
ReverseAdapter,
60+
)
61+
62+
Quick Start
63+
-----------
64+
65+
Field reference column:
66+
67+
.. code-block:: python
68+
69+
col = make_field_column(
70+
title="Bitrate",
71+
key="~bitrate", # same key you would pass to obj.column(key)
72+
width=80,
73+
align=ColumnAlign.RIGHT,
74+
)
75+
registry.register(col, add_to_file_view=True, add_to_album_view=False, insert_after_key="length")
76+
77+
Script column:
78+
79+
.. code-block:: python
80+
81+
script = "$if(%title%,$if2(%artist%,Unknown Artist) - $if2(%title%,Unknown Title),$if2(%albumartist%,Unknown Artist) - $if2(%album%,Unknown Album))"
82+
col = make_script_column(
83+
title="Artist – Title",
84+
key="artist_title_script",
85+
script=script,
86+
width=280,
87+
align=ColumnAlign.LEFT,
88+
)
89+
registry.register(col, add_to_album_view=True, insert_after_key="title")
90+
91+
Transformed base field:
92+
93+
.. code-block:: python
94+
95+
from picard.ui.itemviews.custom_columns.providers import FieldReferenceProvider
96+
97+
upper_title = make_transformed_column(
98+
title="TITLE (UPPER)",
99+
key="title_upper",
100+
base=FieldReferenceProvider("title"),
101+
transform=lambda s: s.upper(),
102+
)
103+
registry.register(upper_title)
104+
105+
Callable-backed column:
106+
107+
.. code-block:: python
108+
109+
from picard.item import Item
110+
111+
def file_ext(item: Item) -> str:
112+
return item.column("~extension")
113+
114+
col = make_callable_column("Ext", key="ext", func=file_ext, sort_type=ColumnSortType.TEXT)
115+
registry.register(col)
116+
117+
Registration
118+
------------
119+
120+
.. code-block:: python
121+
122+
registry.register(column,
123+
add_to_file_view=True,
124+
add_to_album_view=True,
125+
insert_after_key="title")
126+
127+
- Inserts into live UI collections (``FILEVIEW_COLUMNS``, ``ALBUMVIEW_COLUMNS``).
128+
- ``insert_after_key`` places the column after an existing key; falls back to append if not found.
129+
- Idempotent per ``key`` (re-registration replaces existing instances). Use ``registry.unregister(key)`` to remove.
130+
131+
Sorting
132+
-------
133+
134+
- Default sort type is text. To supply a computed sort key, wrap the provider with an adapter that implements ``sort_key`` and use ``ColumnSortType.SORTKEY``.
135+
136+
Case-insensitive sort for a script column:
137+
138+
.. code-block:: python
139+
140+
base = make_script_column("Artist – Title", key="artist_title_script", script=script)
141+
sorted_provider = CasefoldSortAdapter(base.provider) # provides .sort_key
142+
sorted_col = CustomColumn(
143+
title=base.title,
144+
key=base.key,
145+
provider=sorted_provider,
146+
width=base.width,
147+
align=base.align,
148+
sort_type=ColumnSortType.SORTKEY,
149+
)
150+
registry.register(sorted_col, insert_after_key="title")
151+
152+
Available adapters (imported from ``picard.ui.itemviews.custom_columns``):
153+
154+
- **CasefoldSortAdapter**: case-insensitive (str.casefold) text sort
155+
- **DescendingCasefoldSortAdapter**: descending case-insensitive text sort
156+
- **NumericSortAdapter**: numeric sort using parser (default float)
157+
- **DescendingNumericSortAdapter**: descending numeric (negated value)
158+
- **LengthSortAdapter**: sort by string length
159+
- **RandomSortAdapter**: deterministic pseudo-random by value and seed
160+
- **ArticleInsensitiveAdapter**: ignore leading articles (e.g. a, an, the)
161+
- **CompositeSortAdapter**: tuple sort from multiple key functions
162+
- **NullsFirstAdapter**: empty/whitespace values sort first
163+
- **NullsLastAdapter**: empty/whitespace values sort last
164+
- **CachedSortAdapter**: cache sort keys for performance
165+
- **ReverseAdapter**: invert existing sort key (numeric or string)
166+
167+
You can also create a custom provider that implements ``sort_key`` to participate in ``SORTKEY`` sorting.
168+
169+
Providers
170+
---------
171+
172+
Protocols (typing only):
173+
174+
.. code-block:: python
175+
176+
from picard.ui.itemviews.custom_columns import ColumnValueProvider, SortKeyProvider
177+
178+
Built-ins:
179+
180+
- **FieldReferenceProvider(key: str)**: returns ``obj.column(key)``; safe on missing keys.
181+
- **TransformProvider(base: ColumnValueProvider, transform: Callable[[str], str])**: applies a string transform.
182+
- **CallableProvider(func: Callable[[Item], str])**: wraps a Python callable.
183+
- Script provider is created via ``make_script_column(...)`` (do not instantiate directly).
184+
185+
Factory helpers return a ``CustomColumn`` and infer a sane ``sort_type`` when possible:
186+
187+
- ``make_field_column(...)``
188+
- ``make_script_column(...)`` (tunable: ``max_runtime_ms``, ``cache_size``, optional parser or factory)
189+
- ``make_transformed_column(...)``
190+
- ``make_callable_column(...)``
191+
- ``make_provider_column(...)``
192+
193+
``CustomColumn`` signature:
194+
195+
.. code-block:: python
196+
197+
CustomColumn(title, key, provider, width=None, align=ColumnAlign.LEFT,
198+
sort_type=ColumnSortType.TEXT, always_visible=False)
199+
200+
Persistence Utilities
201+
---------------------
202+
203+
Serialize specs to config and (optionally) auto-register columns.
204+
205+
.. code-block:: python
206+
207+
from picard.ui.itemviews.custom_columns.storage import (
208+
CustomColumnSpec, CustomColumnKind, TransformName,
209+
build_column_from_spec,
210+
load_specs_from_config, save_specs_to_config,
211+
add_or_update_spec, delete_spec_by_key, get_spec_by_key,
212+
register_and_persist, unregister_and_delete,
213+
load_persisted_columns_once,
214+
)
215+
216+
# Create and persist a script spec
217+
spec = CustomColumnSpec(
218+
title="Artist – Title",
219+
key="artist_title_script",
220+
kind=CustomColumnKind.SCRIPT,
221+
expression=script,
222+
width=280,
223+
align="LEFT",
224+
add_to_file_view=False,
225+
add_to_album_view=True,
226+
insert_after_key="title",
227+
)
228+
register_and_persist(spec) # saves to config and registers in views
229+
230+
# Load and register all saved specs once (idempotent)
231+
load_persisted_columns_once()
232+
233+
# Remove and delete
234+
unregister_and_delete("artist_title_script")
235+
236+
Notes:
237+
238+
- ``CustomColumnSpec.align`` accepts "LEFT" or "RIGHT" (mapped to ``ColumnAlign``).
239+
- ``CustomColumnSpec.kind``: ``FIELD``, ``SCRIPT``, or ``TRANSFORM``.
240+
- ``TRANSFORM`` specs use ``expression`` as the base field and optional ``transform: TransformName``.
241+
- Registry insertion uses the spec's ``add_to_file_view``, ``add_to_album_view``, and ``insert_after_key``.
242+
243+
Field Keys and Scripting
244+
------------------------
245+
246+
- Field keys are the same strings used with ``obj.column(key)`` and Picard variables without percent signs (e.g. ``title``, ``albumartist``, ``~bitrate``).
247+
- Script expressions use the standard Picard scripting language (e.g. ``$if()``, ``$if2()``, ``%artist%``).
248+
- See ``picard.const.tags.ALL_TAGS`` for the authoritative list of variables.
249+
250+
Runtime & Safety
251+
----------------
252+
253+
- Script provider has configurable ``max_runtime_ms`` and internal caching; errors return empty strings rather than raising.
254+
- ``registry.register`` is UI-safe after the main window has initialized; re-entrant calls replace existing keys.
255+
- ``registry.unregister(key)`` removes from both views (if present).

extending/extending.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ Scripts are stored within the user settings, and are managed from the :menuselec
2727
plugins
2828
scripts
2929
processing
30+
custom_columns_api

index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ plugins and tutorials are provided when available rather than trying to reproduc
5353
/usage/other
5454
/usage/option_profiles
5555
/usage/command_processing
56+
/usage/custom_columns
5657
/extending/extending
58+
/extending/custom_columns_api
5759
/faq/faq
5860

5961

0 commit comments

Comments
 (0)