From 2da3df6e4d2271ddaef3fa71474ebdbe18168904 Mon Sep 17 00:00:00 2001
From: David Sehnal <david.sehnal@gmail.com>
Date: Thu, 27 Feb 2020 16:17:23 +0100
Subject: [PATCH] ParamDefinition.Select grouping & used in theme definitions

---
 src/apps/state-docs/pd-to-md.ts               |  2 +-
 src/examples/proteopedia-wrapper/coloring.ts  |  1 +
 .../common/custom-element-property.ts         |  1 +
 .../themes/accessible-surface-area.ts         |  1 +
 .../computed/themes/interaction-type.ts       |  1 +
 .../integrative/cross-link-restraint/color.ts |  1 +
 .../pdbe/themes/structure-quality-report.ts   |  1 +
 .../rcsb/themes/assembly-symmetry-cluster.ts  |  1 +
 .../rcsb/themes/density-fit.ts                |  1 +
 .../rcsb/themes/geometry-quality.ts           |  1 +
 .../rcsb/themes/random-coil-index.ts          |  1 +
 src/mol-plugin-ui/controls/action-menu.tsx    | 61 ++++++++++++++++++-
 src/mol-plugin-ui/controls/parameters.tsx     | 20 ++----
 src/mol-theme/color.ts                        |  9 ++-
 src/mol-theme/color/carbohydrate-symbol.ts    |  3 +-
 src/mol-theme/color/chain-id.ts               |  1 +
 src/mol-theme/color/element-index.ts          |  1 +
 src/mol-theme/color/element-symbol.ts         |  1 +
 src/mol-theme/color/entity-source.ts          |  1 +
 src/mol-theme/color/hydrophobicity.ts         |  1 +
 src/mol-theme/color/illustrative.ts           |  1 +
 src/mol-theme/color/model-index.ts            |  1 +
 src/mol-theme/color/molecule-type.ts          |  1 +
 src/mol-theme/color/occupancy.ts              |  1 +
 src/mol-theme/color/operator-hkl.ts           |  1 +
 src/mol-theme/color/operator-name.ts          |  1 +
 src/mol-theme/color/polymer-id.ts             |  1 +
 src/mol-theme/color/polymer-index.ts          |  1 +
 src/mol-theme/color/residue-name.ts           |  1 +
 src/mol-theme/color/secondary-structure.ts    |  1 +
 src/mol-theme/color/sequence-id.ts            |  1 +
 src/mol-theme/color/shape-group.ts            |  1 +
 src/mol-theme/color/uncertainty.ts            |  1 +
 src/mol-theme/color/uniform.ts                |  1 +
 src/mol-theme/color/unit-index.ts             |  1 +
 src/mol-theme/size.ts                         |  2 +-
 src/mol-theme/size/physical.ts                |  1 +
 src/mol-theme/size/shape-group.ts             |  1 +
 src/mol-theme/size/uncertainty.ts             |  1 +
 src/mol-theme/size/uniform.ts                 |  1 +
 src/mol-theme/theme.ts                        | 17 +++++-
 src/mol-util/param-definition.ts              |  8 +--
 42 files changed, 129 insertions(+), 27 deletions(-)

diff --git a/src/apps/state-docs/pd-to-md.ts b/src/apps/state-docs/pd-to-md.ts
index f0df30519..a55d372a0 100644
--- a/src/apps/state-docs/pd-to-md.ts
+++ b/src/apps/state-docs/pd-to-md.ts
@@ -39,7 +39,7 @@ function paramInfo(param: PD.Any, offset: number): string {
     }
 }
 
-function oToS(options: readonly (readonly [string, string])[]) {
+function oToS(options: readonly (readonly [string, string] | [string, string, string])[]) {
     return options.map(o => `'${o[0]}'`).join(', ');
 }
 
diff --git a/src/examples/proteopedia-wrapper/coloring.ts b/src/examples/proteopedia-wrapper/coloring.ts
index 0f7bc4b3a..3b2549636 100644
--- a/src/examples/proteopedia-wrapper/coloring.ts
+++ b/src/examples/proteopedia-wrapper/coloring.ts
@@ -96,6 +96,7 @@ export function createProteopediaCustomTheme(colors: number[]) {
 
     const ProteopediaCustomColorThemeProvider: ColorTheme.Provider<ProteopediaCustomColorThemeParams> = {
         label: 'Proteopedia Custom',
+        category: 'Custom',
         factory: ProteopediaCustomColorTheme,
         getParams: getChainIdColorThemeParams,
         defaultValues: PD.getDefaultValues(ProteopediaCustomColorThemeParams),
diff --git a/src/mol-model-props/common/custom-element-property.ts b/src/mol-model-props/common/custom-element-property.ts
index 3dea414a9..4684a20e4 100644
--- a/src/mol-model-props/common/custom-element-property.ts
+++ b/src/mol-model-props/common/custom-element-property.ts
@@ -99,6 +99,7 @@ namespace CustomElementProperty {
 
         return {
             label: modelProperty.label,
+            category: 'Custom',
             factory: Coloring,
             getParams: () => ({}),
             defaultValues: {},
diff --git a/src/mol-model-props/computed/themes/accessible-surface-area.ts b/src/mol-model-props/computed/themes/accessible-surface-area.ts
index fe2a60134..3218bc0b5 100644
--- a/src/mol-model-props/computed/themes/accessible-surface-area.ts
+++ b/src/mol-model-props/computed/themes/accessible-surface-area.ts
@@ -68,6 +68,7 @@ export function AccessibleSurfaceAreaColorTheme(ctx: ThemeDataContext, props: PD
 
 export const AccessibleSurfaceAreaColorThemeProvider: ColorTheme.Provider<AccessibleSurfaceAreaColorThemeParams> = {
     label: 'Accessible Surface Area',
+    category: ColorTheme.Category.Computed,
     factory: AccessibleSurfaceAreaColorTheme,
     getParams: getAccessibleSurfaceAreaColorThemeParams,
     defaultValues: PD.getDefaultValues(AccessibleSurfaceAreaColorThemeParams),
diff --git a/src/mol-model-props/computed/themes/interaction-type.ts b/src/mol-model-props/computed/themes/interaction-type.ts
index d6ede61af..f816d2b41 100644
--- a/src/mol-model-props/computed/themes/interaction-type.ts
+++ b/src/mol-model-props/computed/themes/interaction-type.ts
@@ -108,6 +108,7 @@ export function InteractionTypeColorTheme(ctx: ThemeDataContext, props: PD.Value
 
 export const InteractionTypeColorThemeProvider: ColorTheme.Provider<InteractionTypeColorThemeParams> = {
     label: 'Interaction Type',
+    category: ColorTheme.Category.Computed,
     factory: InteractionTypeColorTheme,
     getParams: getInteractionTypeColorThemeParams,
     defaultValues: PD.getDefaultValues(InteractionTypeColorThemeParams),
diff --git a/src/mol-model-props/integrative/cross-link-restraint/color.ts b/src/mol-model-props/integrative/cross-link-restraint/color.ts
index 4000f4371..3ba49d73d 100644
--- a/src/mol-model-props/integrative/cross-link-restraint/color.ts
+++ b/src/mol-model-props/integrative/cross-link-restraint/color.ts
@@ -63,6 +63,7 @@ export function CrossLinkColorTheme(ctx: ThemeDataContext, props: PD.Values<Cros
 
 export const CrossLinkColorThemeProvider: ColorTheme.Provider<CrossLinkColorThemeParams> = {
     label: 'Cross Link',
+    category: ColorTheme.Category.Advanced,
     factory: CrossLinkColorTheme,
     getParams: getCrossLinkColorThemeParams,
     defaultValues: PD.getDefaultValues(CrossLinkColorThemeParams),
diff --git a/src/mol-model-props/pdbe/themes/structure-quality-report.ts b/src/mol-model-props/pdbe/themes/structure-quality-report.ts
index e3296bab9..a4b23e9eb 100644
--- a/src/mol-model-props/pdbe/themes/structure-quality-report.ts
+++ b/src/mol-model-props/pdbe/themes/structure-quality-report.ts
@@ -79,6 +79,7 @@ export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: P
 
 export const StructureQualityReportColorThemeProvider: ColorTheme.Provider<Params> =  {
     label: 'PDBe Structure Quality Report',
+    category: 'PDBe',
     factory: StructureQualityReportColorTheme,
     getParams: ctx => {
         const issueTypes = StructureQualityReport.getIssueTypes(ctx.structure);
diff --git a/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts b/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts
index b8e1d2f7c..cbab8e1e4 100644
--- a/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts
+++ b/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts
@@ -93,6 +93,7 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
 
 export const AssemblySymmetryClusterColorThemeProvider: ColorTheme.Provider<AssemblySymmetryClusterColorThemeParams> = {
     label: 'RCSB Assembly Symmetry Cluster',
+    category: 'RCSB',
     factory: AssemblySymmetryClusterColorTheme,
     getParams: getAssemblySymmetryClusterColorThemeParams,
     defaultValues: PD.getDefaultValues(AssemblySymmetryClusterColorThemeParams),
diff --git a/src/mol-model-props/rcsb/themes/density-fit.ts b/src/mol-model-props/rcsb/themes/density-fit.ts
index 9bc3d3d0f..319c3c602 100644
--- a/src/mol-model-props/rcsb/themes/density-fit.ts
+++ b/src/mol-model-props/rcsb/themes/density-fit.ts
@@ -62,6 +62,7 @@ export function DensityFitColorTheme(ctx: ThemeDataContext, props: {}): ColorThe
 
 export const DensityFitColorThemeProvider: ColorTheme.Provider<{}> = {
     label: 'RCSB Density Fit',
+    category: 'RCSB',
     factory: DensityFitColorTheme,
     getParams: () => ({}),
     defaultValues: PD.getDefaultValues({}),
diff --git a/src/mol-model-props/rcsb/themes/geometry-quality.ts b/src/mol-model-props/rcsb/themes/geometry-quality.ts
index 1f39b8de4..ca6d54e35 100644
--- a/src/mol-model-props/rcsb/themes/geometry-quality.ts
+++ b/src/mol-model-props/rcsb/themes/geometry-quality.ts
@@ -102,6 +102,7 @@ export function GeometryQualityColorTheme(ctx: ThemeDataContext, props: PD.Value
 
 export const GeometryQualityColorThemeProvider: ColorTheme.Provider<GeometricQualityColorThemeParams> = {
     label: 'RCSB Geometry Quality',
+    category: 'RCSB',
     factory: GeometryQualityColorTheme,
     getParams: getGeometricQualityColorThemeParams,
     defaultValues: PD.getDefaultValues(getGeometricQualityColorThemeParams({})),
diff --git a/src/mol-model-props/rcsb/themes/random-coil-index.ts b/src/mol-model-props/rcsb/themes/random-coil-index.ts
index 596aa2055..c8a7e90f2 100644
--- a/src/mol-model-props/rcsb/themes/random-coil-index.ts
+++ b/src/mol-model-props/rcsb/themes/random-coil-index.ts
@@ -53,6 +53,7 @@ export function RandomCoilIndexColorTheme(ctx: ThemeDataContext, props: {}): Col
 
 export const RandomCoilIndexColorThemeProvider: ColorTheme.Provider<{}> = {
     label: 'RCSB Random Coil Index',
+    category: 'RCSB',
     factory: RandomCoilIndexColorTheme,
     getParams: () => ({}),
     defaultValues: PD.getDefaultValues({}),
diff --git a/src/mol-plugin-ui/controls/action-menu.tsx b/src/mol-plugin-ui/controls/action-menu.tsx
index 0d67b8c80..bc223a0de 100644
--- a/src/mol-plugin-ui/controls/action-menu.tsx
+++ b/src/mol-plugin-ui/controls/action-menu.tsx
@@ -7,6 +7,7 @@
 import * as React from 'react'
 import { Icon } from './common';
 import { Subscription, BehaviorSubject, Observable } from 'rxjs';
+import { ParamDefinition } from '../../mol-util/param-definition';
 
 export class ActionMenu {
     private _command: BehaviorSubject<ActionMenu.Command>;
@@ -139,16 +140,29 @@ export namespace ActionMenu {
         }
     }
 
-    class Section extends React.PureComponent<{ menu: ActionMenu, header?: string, items: Spec, onSelect: OnSelect, current: Item | undefined  }, { isExpanded: boolean }> {
-        state = { isExpanded: false }
+    type SectionProps = { menu: ActionMenu, header?: string, items: Spec, onSelect: OnSelect, current: Item | undefined }
+    type SectionState = { items: Spec, current: Item | undefined, isExpanded: boolean }
+
+    class Section extends React.PureComponent<SectionProps, SectionState> {
+        state = {
+            items: this.props.items,
+            current: this.props.current,
+            isExpanded: !!this.props.current && !!findCurrent(this.props.items, this.props.current.value)
+        }
 
         toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
             this.setState({ isExpanded: !this.state.isExpanded });
             e.currentTarget.blur();
         }
 
+        static getDerivedStateFromProps(props: SectionProps, state: SectionState) {
+            if (props.items === state.items && props.current === state.current) return null;
+            return { items: props.items, current: props.current, isExpanded: props.current && !!findCurrent(props.items, props.current.value) }
+        }
+
         render() {
             const { header, items, onSelect, current, menu } = this.props;
+
             if (typeof items === 'string') return null;
             if (isItem(items)) return <Action menu={menu} item={items} onSelect={onSelect} current={current} />
             return <div>
@@ -196,4 +210,47 @@ export namespace ActionMenu {
         if (value) return { name, icon: iconOrValue, value };
         return { name, value: iconOrValue };
     }
+
+    function createSpecFromSelectParamSimple(param: ParamDefinition.Select<any>) {
+        const spec: Item[] = [];
+        for (const [v, l] of param.options) {
+            spec.push(ActionMenu.Item(l, v));
+        }
+        return spec as Spec;
+    }
+
+    function createSpecFromSelectParamCategories(param: ParamDefinition.Select<any>) {
+        const cats = new Map<string, (Item | string)[]>();
+        const spec: (Item | (Item | string)[] | string)[] = [];
+        for (const [v, l, c] of param.options) {
+            if (!!c) {
+                let cat = cats.get(c);
+                if (!cat) {
+                    cat = [c];
+                    cats.set(c, cat);
+                    spec.push(cat);
+                }
+                cat.push(ActionMenu.Item(l, v));
+            } else {
+                spec.push(ActionMenu.Item(l, v));
+            }
+        }
+        return spec as Spec;
+    }
+
+    export function createSpecFromSelectParam(param: ParamDefinition.Select<any>) {
+        for (const o of param.options) {
+            if (!!o[2]) return createSpecFromSelectParamCategories(param);
+        }
+        return createSpecFromSelectParamSimple(param);
+    }
+
+    export function findCurrent(spec: Spec, value: any): Item | undefined {
+        if (typeof spec === 'string') return;
+        if (isItem(spec)) return spec.value === value ? spec : void 0;
+        for (const s of spec) {
+            const found = findCurrent(s, value);
+            if (found) return found;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/mol-plugin-ui/controls/parameters.tsx b/src/mol-plugin-ui/controls/parameters.tsx
index f52c546cc..2576f59e7 100644
--- a/src/mol-plugin-ui/controls/parameters.tsx
+++ b/src/mol-plugin-ui/controls/parameters.tsx
@@ -8,7 +8,7 @@
 import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
 import { Color } from '../../mol-util/color';
 import { ColorListName, getColorListFromName } from '../../mol-util/color/lists';
-import { memoize1 } from '../../mol-util/memoize';
+import { memoize1, memoizeLatest } from '../../mol-util/memoize';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { camelCaseToWords } from '../../mol-util/string';
 import * as React from 'react';
@@ -328,22 +328,14 @@ export class SelectControl extends SimpleParam<PD.Select<string | number>> {
         this.update(value);
     }
 
+    items = memoizeLatest((param: PD.Select<any>) => ActionMenu.createSpecFromSelectParam(param));
+
     renderControl() {
-        const isInvalid = this.props.value !== void 0 && !this.props.param.options.some(e => e[0] === this.props.value);
-        const items: ActionMenu.Item[] = [];
-        let current: ActionMenu.Item | undefined = void 0;
-        if (isInvalid) {
-            current = ActionMenu.Item(`[Invalid] ${this.props.value}`, this.props.value);
-            items.push(current);
-        }
-        for (const [value, label] of this.props.param.options) {
-            const item = ActionMenu.Item(label, value);
-            items.push(item);
-            if (value === this.props.value) current = item;
-        }
+        const items = this.items(this.props.param);
+        const current = ActionMenu.findCurrent(items, this.props.value);
 
         return <ActionMenu.Toggle menu={this.menu} disabled={this.props.isDisabled} 
-            onSelect={this.onSelect} items={items as ActionMenu.Spec} label={current?.name}
+            onSelect={this.onSelect} items={items as ActionMenu.Spec} label={current?.name || `[Invalid] ${this.props.value}`}
             current={current} />;
     }
 
diff --git a/src/mol-theme/color.ts b/src/mol-theme/color.ts
index 67537bb8f..6efcc7691 100644
--- a/src/mol-theme/color.ts
+++ b/src/mol-theme/color.ts
@@ -46,6 +46,13 @@ interface ColorTheme<P extends PD.Params> {
     readonly legend?: Readonly<ScaleLegend | TableLegend>
 }
 namespace ColorTheme {
+    export const enum Category {
+        Basic = 'Basic',
+        Advanced = 'Advanced',
+        Computed = 'Computed',
+        Misc = 'Miscellaneous'
+    }
+
     export type Props = { [k: string]: any }
     export type Factory<P extends PD.Params> = (ctx: ThemeDataContext, props: PD.Values<P>) => ColorTheme<P>
     export const EmptyFactory = () => Empty
@@ -62,7 +69,7 @@ namespace ColorTheme {
     }
 
     export interface Provider<P extends PD.Params> extends ThemeProvider<ColorTheme<P>, P> { }
-    export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {}, isApplicable: () => true }
+    export const EmptyProvider: Provider<{}> = { label: '', category: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {}, isApplicable: () => true }
 
     export type Registry = ThemeRegistry<ColorTheme<any>>
     export function createRegistry() {
diff --git a/src/mol-theme/color/carbohydrate-symbol.ts b/src/mol-theme/color/carbohydrate-symbol.ts
index f29d10170..853e7cf9c 100644
--- a/src/mol-theme/color/carbohydrate-symbol.ts
+++ b/src/mol-theme/color/carbohydrate-symbol.ts
@@ -61,7 +61,8 @@ export function CarbohydrateSymbolColorTheme(ctx: ThemeDataContext, props: PD.Va
 }
 
 export const CarbohydrateSymbolColorThemeProvider: ColorTheme.Provider<CarbohydrateSymbolColorThemeParams> = {
-    label: 'Carbohydrate Symbol',
+    label: 'Carbohydrate Symbol',    
+    category: ColorTheme.Category.Advanced,
     factory: CarbohydrateSymbolColorTheme,
     getParams: getCarbohydrateSymbolColorThemeParams,
     defaultValues: PD.getDefaultValues(CarbohydrateSymbolColorThemeParams),
diff --git a/src/mol-theme/color/chain-id.ts b/src/mol-theme/color/chain-id.ts
index 16c072ff4..d9ae75cb9 100644
--- a/src/mol-theme/color/chain-id.ts
+++ b/src/mol-theme/color/chain-id.ts
@@ -119,6 +119,7 @@ export function ChainIdColorTheme(ctx: ThemeDataContext, props: PD.Values<ChainI
 
 export const ChainIdColorThemeProvider: ColorTheme.Provider<ChainIdColorThemeParams> = {
     label: 'Chain Id',
+    category: ColorTheme.Category.Basic,
     factory: ChainIdColorTheme,
     getParams: getChainIdColorThemeParams,
     defaultValues: PD.getDefaultValues(ChainIdColorThemeParams),
diff --git a/src/mol-theme/color/element-index.ts b/src/mol-theme/color/element-index.ts
index be94f3376..d2e13588b 100644
--- a/src/mol-theme/color/element-index.ts
+++ b/src/mol-theme/color/element-index.ts
@@ -73,6 +73,7 @@ export function ElementIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<E
 
 export const ElementIndexColorThemeProvider: ColorTheme.Provider<ElementIndexColorThemeParams> = {
     label: 'Element Index',
+    category: ColorTheme.Category.Basic,
     factory: ElementIndexColorTheme,
     getParams: getElementIndexColorThemeParams,
     defaultValues: PD.getDefaultValues(ElementIndexColorThemeParams),
diff --git a/src/mol-theme/color/element-symbol.ts b/src/mol-theme/color/element-symbol.ts
index 6f2f8881f..8c559ff64 100644
--- a/src/mol-theme/color/element-symbol.ts
+++ b/src/mol-theme/color/element-symbol.ts
@@ -69,6 +69,7 @@ export function ElementSymbolColorTheme(ctx: ThemeDataContext, props: PD.Values<
 
 export const ElementSymbolColorThemeProvider: ColorTheme.Provider<ElementSymbolColorThemeParams> = {
     label: 'Element Symbol',
+    category: ColorTheme.Category.Basic,
     factory: ElementSymbolColorTheme,
     getParams: getElementSymbolColorThemeParams,
     defaultValues: PD.getDefaultValues(ElementSymbolColorThemeParams),
diff --git a/src/mol-theme/color/entity-source.ts b/src/mol-theme/color/entity-source.ts
index a8ac312bf..e8b4e71bc 100644
--- a/src/mol-theme/color/entity-source.ts
+++ b/src/mol-theme/color/entity-source.ts
@@ -175,6 +175,7 @@ export function EntitySourceColorTheme(ctx: ThemeDataContext, props: PD.Values<E
 
 export const EntitySourceColorThemeProvider: ColorTheme.Provider<EntitySourceColorThemeParams> = {
     label: 'Entity Source',
+    category: ColorTheme.Category.Advanced,
     factory: EntitySourceColorTheme,
     getParams: getEntitySourceColorThemeParams,
     defaultValues: PD.getDefaultValues(EntitySourceColorThemeParams),
diff --git a/src/mol-theme/color/hydrophobicity.ts b/src/mol-theme/color/hydrophobicity.ts
index 47f3b94e4..e49e3f25c 100644
--- a/src/mol-theme/color/hydrophobicity.ts
+++ b/src/mol-theme/color/hydrophobicity.ts
@@ -94,6 +94,7 @@ export function HydrophobicityColorTheme(ctx: ThemeDataContext, props: PD.Values
 
 export const HydrophobicityColorThemeProvider: ColorTheme.Provider<HydrophobicityColorThemeParams> = {
     label: 'Hydrophobicity',
+    category: ColorTheme.Category.Advanced,
     factory: HydrophobicityColorTheme,
     getParams: getHydrophobicityColorThemeParams,
     defaultValues: PD.getDefaultValues(HydrophobicityColorThemeParams),
diff --git a/src/mol-theme/color/illustrative.ts b/src/mol-theme/color/illustrative.ts
index 6025f1eed..9b86d8b97 100644
--- a/src/mol-theme/color/illustrative.ts
+++ b/src/mol-theme/color/illustrative.ts
@@ -76,6 +76,7 @@ export function IllustrativeColorTheme(ctx: ThemeDataContext, props: PD.Values<I
 
 export const IllustrativeColorThemeProvider: ColorTheme.Provider<IllustrativeColorThemeParams> = {
     label: 'Illustrative',
+    category: ColorTheme.Category.Misc,
     factory: IllustrativeColorTheme,
     getParams: getIllustrativeColorThemeParams,
     defaultValues: PD.getDefaultValues(IllustrativeColorThemeParams),
diff --git a/src/mol-theme/color/model-index.ts b/src/mol-theme/color/model-index.ts
index fd98c44f7..f7cc54bf7 100644
--- a/src/mol-theme/color/model-index.ts
+++ b/src/mol-theme/color/model-index.ts
@@ -61,6 +61,7 @@ export function ModelIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<Mod
 
 export const ModelIndexColorThemeProvider: ColorTheme.Provider<ModelIndexColorThemeParams> = {
     label: 'Model Index',
+    category: ColorTheme.Category.Basic,
     factory: ModelIndexColorTheme,
     getParams: getModelIndexColorThemeParams,
     defaultValues: PD.getDefaultValues(ModelIndexColorThemeParams),
diff --git a/src/mol-theme/color/molecule-type.ts b/src/mol-theme/color/molecule-type.ts
index 49f891381..668f8ccfe 100644
--- a/src/mol-theme/color/molecule-type.ts
+++ b/src/mol-theme/color/molecule-type.ts
@@ -78,6 +78,7 @@ export function MoleculeTypeColorTheme(ctx: ThemeDataContext, props: PD.Values<M
 
 export const MoleculeTypeColorThemeProvider: ColorTheme.Provider<MoleculeTypeColorThemeParams> = {
     label: 'Molecule Type',
+    category: ColorTheme.Category.Advanced,
     factory: MoleculeTypeColorTheme,
     getParams: getMoleculeTypeColorThemeParams,
     defaultValues: PD.getDefaultValues(MoleculeTypeColorThemeParams),
diff --git a/src/mol-theme/color/occupancy.ts b/src/mol-theme/color/occupancy.ts
index cf609f010..7cfa1c458 100644
--- a/src/mol-theme/color/occupancy.ts
+++ b/src/mol-theme/color/occupancy.ts
@@ -60,6 +60,7 @@ export function OccupancyColorTheme(ctx: ThemeDataContext, props: PD.Values<Occu
 
 export const OccupancyColorThemeProvider: ColorTheme.Provider<OccupancyColorThemeParams> = {
     label: 'Occupancy',
+    category: ColorTheme.Category.Advanced,
     factory: OccupancyColorTheme,
     getParams: getOccupancyColorThemeParams,
     defaultValues: PD.getDefaultValues(OccupancyColorThemeParams),
diff --git a/src/mol-theme/color/operator-hkl.ts b/src/mol-theme/color/operator-hkl.ts
index 2469356ea..d08c29033 100644
--- a/src/mol-theme/color/operator-hkl.ts
+++ b/src/mol-theme/color/operator-hkl.ts
@@ -119,6 +119,7 @@ export function OperatorHklColorTheme(ctx: ThemeDataContext, props: PD.Values<Op
 
 export const OperatorHklColorThemeProvider: ColorTheme.Provider<OperatorHklColorThemeParams> = {
     label: 'Operator HKL',
+    category: ColorTheme.Category.Advanced,
     factory: OperatorHklColorTheme,
     getParams: getOperatorHklColorThemeParams,
     defaultValues: PD.getDefaultValues(OperatorHklColorThemeParams),
diff --git a/src/mol-theme/color/operator-name.ts b/src/mol-theme/color/operator-name.ts
index 16d48b147..61b4e95c4 100644
--- a/src/mol-theme/color/operator-name.ts
+++ b/src/mol-theme/color/operator-name.ts
@@ -85,6 +85,7 @@ export function OperatorNameColorTheme(ctx: ThemeDataContext, props: PD.Values<O
 
 export const OperatorNameColorThemeProvider: ColorTheme.Provider<OperatorNameColorThemeParams> = {
     label: 'Operator Name',
+    category: ColorTheme.Category.Advanced,
     factory: OperatorNameColorTheme,
     getParams: getOperatorNameColorThemeParams,
     defaultValues: PD.getDefaultValues(OperatorNameColorThemeParams),
diff --git a/src/mol-theme/color/polymer-id.ts b/src/mol-theme/color/polymer-id.ts
index 46955e905..3e796d3d9 100644
--- a/src/mol-theme/color/polymer-id.ts
+++ b/src/mol-theme/color/polymer-id.ts
@@ -128,6 +128,7 @@ export function PolymerIdColorTheme(ctx: ThemeDataContext, props: PD.Values<Poly
 
 export const PolymerIdColorThemeProvider: ColorTheme.Provider<PolymerIdColorThemeParams> = {
     label: 'Polymer Id',
+    category: ColorTheme.Category.Basic,
     factory: PolymerIdColorTheme,
     getParams: getPolymerIdColorThemeParams,
     defaultValues: PD.getDefaultValues(PolymerIdColorThemeParams),
diff --git a/src/mol-theme/color/polymer-index.ts b/src/mol-theme/color/polymer-index.ts
index 02bfa5b44..ef37ce818 100644
--- a/src/mol-theme/color/polymer-index.ts
+++ b/src/mol-theme/color/polymer-index.ts
@@ -88,6 +88,7 @@ export function PolymerIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<P
 
 export const PolymerIndexColorThemeProvider: ColorTheme.Provider<PolymerIndexColorThemeParams> = {
     label: 'Polymer Index',
+    category: ColorTheme.Category.Advanced,
     factory: PolymerIndexColorTheme,
     getParams: getPolymerIndexColorThemeParams,
     defaultValues: PD.getDefaultValues(PolymerIndexColorThemeParams),
diff --git a/src/mol-theme/color/residue-name.ts b/src/mol-theme/color/residue-name.ts
index 175518740..72259d10f 100644
--- a/src/mol-theme/color/residue-name.ts
+++ b/src/mol-theme/color/residue-name.ts
@@ -130,6 +130,7 @@ export function ResidueNameColorTheme(ctx: ThemeDataContext, props: PD.Values<Re
 
 export const ResidueNameColorThemeProvider: ColorTheme.Provider<ResidueNameColorThemeParams> = {
     label: 'Residue Name',
+    category: ColorTheme.Category.Basic,
     factory: ResidueNameColorTheme,
     getParams: getResidueNameColorThemeParams,
     defaultValues: PD.getDefaultValues(ResidueNameColorThemeParams),
diff --git a/src/mol-theme/color/secondary-structure.ts b/src/mol-theme/color/secondary-structure.ts
index 9eef3bb1c..6afa2dc1b 100644
--- a/src/mol-theme/color/secondary-structure.ts
+++ b/src/mol-theme/color/secondary-structure.ts
@@ -113,6 +113,7 @@ export function SecondaryStructureColorTheme(ctx: ThemeDataContext, props: PD.Va
 
 export const SecondaryStructureColorThemeProvider: ColorTheme.Provider<SecondaryStructureColorThemeParams> = {
     label: 'Secondary Structure',
+    category: ColorTheme.Category.Basic,
     factory: SecondaryStructureColorTheme,
     getParams: getSecondaryStructureColorThemeParams,
     defaultValues: PD.getDefaultValues(SecondaryStructureColorThemeParams),
diff --git a/src/mol-theme/color/sequence-id.ts b/src/mol-theme/color/sequence-id.ts
index 66457c0b5..90f5038ec 100644
--- a/src/mol-theme/color/sequence-id.ts
+++ b/src/mol-theme/color/sequence-id.ts
@@ -101,6 +101,7 @@ export function SequenceIdColorTheme(ctx: ThemeDataContext, props: PD.Values<Seq
 
 export const SequenceIdColorThemeProvider: ColorTheme.Provider<SequenceIdColorThemeParams> = {
     label: 'Sequence Id',
+    category: ColorTheme.Category.Basic,
     factory: SequenceIdColorTheme,
     getParams: getSequenceIdColorThemeParams,
     defaultValues: PD.getDefaultValues(SequenceIdColorThemeParams),
diff --git a/src/mol-theme/color/shape-group.ts b/src/mol-theme/color/shape-group.ts
index 1c3166faf..d5b5ba87e 100644
--- a/src/mol-theme/color/shape-group.ts
+++ b/src/mol-theme/color/shape-group.ts
@@ -37,6 +37,7 @@ export function ShapeGroupColorTheme(ctx: ThemeDataContext, props: PD.Values<Sha
 
 export const ShapeGroupColorThemeProvider: ColorTheme.Provider<ShapeGroupColorThemeParams> = {
     label: 'Shape Group',
+    category: ColorTheme.Category.Advanced,
     factory: ShapeGroupColorTheme,
     getParams: getShapeGroupColorThemeParams,
     defaultValues: PD.getDefaultValues(ShapeGroupColorThemeParams),
diff --git a/src/mol-theme/color/uncertainty.ts b/src/mol-theme/color/uncertainty.ts
index d7511cb05..7e87f0127 100644
--- a/src/mol-theme/color/uncertainty.ts
+++ b/src/mol-theme/color/uncertainty.ts
@@ -64,6 +64,7 @@ export function UncertaintyColorTheme(ctx: ThemeDataContext, props: PD.Values<Un
 
 export const UncertaintyColorThemeProvider: ColorTheme.Provider<UncertaintyColorThemeParams> = {
     label: 'Uncertainty/Disorder',
+    category: ColorTheme.Category.Advanced,
     factory: UncertaintyColorTheme,
     getParams: getUncertaintyColorThemeParams,
     defaultValues: PD.getDefaultValues(UncertaintyColorThemeParams),
diff --git a/src/mol-theme/color/uniform.ts b/src/mol-theme/color/uniform.ts
index 6bc0e8bdc..e018b914d 100644
--- a/src/mol-theme/color/uniform.ts
+++ b/src/mol-theme/color/uniform.ts
@@ -37,6 +37,7 @@ export function UniformColorTheme(ctx: ThemeDataContext, props: PD.Values<Unifor
 
 export const UniformColorThemeProvider: ColorTheme.Provider<UniformColorThemeParams> = {
     label: 'Uniform',
+    category: ColorTheme.Category.Basic,
     factory: UniformColorTheme,
     getParams: getUniformColorThemeParams,
     defaultValues: PD.getDefaultValues(UniformColorThemeParams),
diff --git a/src/mol-theme/color/unit-index.ts b/src/mol-theme/color/unit-index.ts
index bb6bb9c5b..bcd5e7314 100644
--- a/src/mol-theme/color/unit-index.ts
+++ b/src/mol-theme/color/unit-index.ts
@@ -73,6 +73,7 @@ export function UnitIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<Unit
 
 export const UnitIndexColorThemeProvider: ColorTheme.Provider<UnitIndexColorThemeParams> = {
     label: 'Unit Index',
+    category: ColorTheme.Category.Advanced,
     factory: UnitIndexColorTheme,
     getParams: getUnitIndexColorThemeParams,
     defaultValues: PD.getDefaultValues(UnitIndexColorThemeParams),
diff --git a/src/mol-theme/size.ts b/src/mol-theme/size.ts
index 71c79b8d2..2283474f7 100644
--- a/src/mol-theme/size.ts
+++ b/src/mol-theme/size.ts
@@ -32,7 +32,7 @@ namespace SizeTheme {
     }
 
     export interface Provider<P extends PD.Params> extends ThemeProvider<SizeTheme<P>, P> { }
-    export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {}, isApplicable: () => true }
+    export const EmptyProvider: Provider<{}> = { label: '', category: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {}, isApplicable: () => true }
 
     export type Registry = ThemeRegistry<SizeTheme<any>>
     export function createRegistry() {
diff --git a/src/mol-theme/size/physical.ts b/src/mol-theme/size/physical.ts
index 4456ad209..8622967d1 100644
--- a/src/mol-theme/size/physical.ts
+++ b/src/mol-theme/size/physical.ts
@@ -58,6 +58,7 @@ export function PhysicalSizeTheme(ctx: ThemeDataContext, props: PD.Values<Physic
 
 export const PhysicalSizeThemeProvider: SizeTheme.Provider<PhysicalSizeThemeParams> = {
     label: 'Physical',
+    category: '',
     factory: PhysicalSizeTheme,
     getParams: getPhysicalSizeThemeParams,
     defaultValues: PD.getDefaultValues(PhysicalSizeThemeParams),
diff --git a/src/mol-theme/size/shape-group.ts b/src/mol-theme/size/shape-group.ts
index 2e2f867d5..b7eaa993f 100644
--- a/src/mol-theme/size/shape-group.ts
+++ b/src/mol-theme/size/shape-group.ts
@@ -36,6 +36,7 @@ export function ShapeGroupSizeTheme(ctx: ThemeDataContext, props: PD.Values<Shap
 
 export const ShapeGroupSizeThemeProvider: SizeTheme.Provider<ShapeGroupSizeThemeParams> = {
     label: 'Shape Group',
+    category: '',    
     factory: ShapeGroupSizeTheme,
     getParams: getShapeGroupSizeThemeParams,
     defaultValues: PD.getDefaultValues(ShapeGroupSizeThemeParams),
diff --git a/src/mol-theme/size/uncertainty.ts b/src/mol-theme/size/uncertainty.ts
index 5f9da065b..bbeec0777 100644
--- a/src/mol-theme/size/uncertainty.ts
+++ b/src/mol-theme/size/uncertainty.ts
@@ -54,6 +54,7 @@ export function UncertaintySizeTheme(ctx: ThemeDataContext, props: PD.Values<Unc
 
 export const UncertaintySizeThemeProvider: SizeTheme.Provider<UncertaintySizeThemeParams> = {
     label: 'Uncertainty/Disorder',
+    category: '',
     factory: UncertaintySizeTheme,
     getParams: getUncertaintySizeThemeParams,
     defaultValues: PD.getDefaultValues(UncertaintySizeThemeParams),
diff --git a/src/mol-theme/size/uniform.ts b/src/mol-theme/size/uniform.ts
index 4d82bef10..32b271874 100644
--- a/src/mol-theme/size/uniform.ts
+++ b/src/mol-theme/size/uniform.ts
@@ -32,6 +32,7 @@ export function UniformSizeTheme(ctx: ThemeDataContext, props: PD.Values<Uniform
 
 export const UniformSizeThemeProvider: SizeTheme.Provider<UniformSizeThemeParams> = {
     label: 'Uniform',
+    category: '',
     factory: UniformSizeTheme,
     getParams: getUniformSizeThemeParams,
     defaultValues: PD.getDefaultValues(UniformSizeThemeParams),
diff --git a/src/mol-theme/theme.ts b/src/mol-theme/theme.ts
index ce5350a5a..d654af18e 100644
--- a/src/mol-theme/theme.ts
+++ b/src/mol-theme/theme.ts
@@ -60,7 +60,8 @@ namespace Theme {
 //
 
 export interface ThemeProvider<T extends ColorTheme<P> | SizeTheme<P>, P extends PD.Params> {
-    readonly label: string
+    readonly label: string    
+    readonly category: string
     readonly factory: (ctx: ThemeDataContext, props: PD.Values<P>) => T
     readonly getParams: (ctx: ThemeDataContext) => P
     readonly defaultValues: PD.Values<P>
@@ -69,7 +70,7 @@ export interface ThemeProvider<T extends ColorTheme<P> | SizeTheme<P>, P extends
 }
 
 function getTypes(list: { name: string, provider: ThemeProvider<any, any> }[]) {
-    return list.map(e => [e.name, e.provider.label] as [string, string]);
+    return list.map(e => [e.name, e.provider.label, e.provider.category] as [string, string, string]);
 }
 
 export class ThemeRegistry<T extends ColorTheme<any> | SizeTheme<any>> {
@@ -79,16 +80,26 @@ export class ThemeRegistry<T extends ColorTheme<any> | SizeTheme<any>> {
 
     get default() { return this._list[0] }
     get list() { return this._list }
-    get types(): [string, string][] { return getTypes(this._list) }
+    get types(): [string, string, string][] { return getTypes(this._list) }
 
     constructor(builtInThemes: { [k: string]: ThemeProvider<T, any> }, private emptyProvider: ThemeProvider<T, any>) {
         Object.keys(builtInThemes).forEach(name => this.add(name, builtInThemes[name]))
     }
 
+    private sort() {
+        this._list.sort((a, b) => {
+            if (a.provider.category === b.provider.category) {
+                return a.provider.label < b.provider.label ? -1 : a.provider.label > b.provider.label ? 1 : 0;
+            }
+            return a.provider.category < b.provider.label ? -1 : 1;
+        });
+    }
+
     add<P extends PD.Params>(name: string, provider: ThemeProvider<T, P>) {
         this._list.push({ name, provider })
         this._map.set(name, provider)
         this._name.set(provider, name)
+        this.sort();
     }
 
     remove(name: string) {
diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts
index b2ccbedce..064528cf1 100644
--- a/src/mol-util/param-definition.ts
+++ b/src/mol-util/param-definition.ts
@@ -65,9 +65,9 @@ export namespace ParamDefinition {
     export interface Select<T extends string | number> extends Base<T> {
         type: 'select'
         /** array of (value, label) tuples */
-        options: readonly (readonly [T, string])[]
+        options: readonly (readonly [T, string] | [T, string, string])[]
     }
-    export function Select<T extends string | number>(defaultValue: T, options: readonly (readonly [T, string])[], info?: Info): Select<T> {
+    export function Select<T extends string | number>(defaultValue: T, options: readonly (readonly [T, string] | [T, string, string])[], info?: Info): Select<T> {
         return setInfo<Select<T>>({ type: 'select', defaultValue: checkDefaultKey(defaultValue, options), options }, info)
     }
 
@@ -201,7 +201,7 @@ export namespace ParamDefinition {
         select: Select<string>,
         map(name: string): Any
     }
-    export function Mapped<T>(defaultKey: string, names: [string, string][], map: (name: string) => Any, info?: Info): Mapped<NamedParams<T>> {
+    export function Mapped<T>(defaultKey: string, names: ([string, string] | [string, string, string])[], map: (name: string) => Any, info?: Info): Mapped<NamedParams<T>> {
         const name = checkDefaultKey(defaultKey, names);
         return setInfo<Mapped<NamedParams<T>>>({
             type: 'mapped',
@@ -406,7 +406,7 @@ export namespace ParamDefinition {
         return ret;
     }
 
-    function checkDefaultKey<T>(k: T, options: readonly (readonly [T, string])[]) {
+    function checkDefaultKey<T>(k: T, options: readonly (readonly [T, string] | [T, string, string])[]) {
         for (const o of options) {
             if (o[0] === k) return k;
         }
-- 
GitLab