All files / src/web config.js

97.43% Statements 76/78
96.87% Branches 31/32
100% Functions 25/25
100% Lines 64/64

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171                                        9x             9441x       62x         668x                   35x 35x 35x     35x   35x 35x 35x 35x   37x     35x 35x 35x 35x 491x 42x 42x     35x 35x   35x 31x 31x     35x 23x 23x 23x 16x   16x       8x     35x 4x   70x       3984x         226x 3169x               37x 6x 6x   31x 31x 31x       925x 66x         139x 139x 1949x 1949x 139x         139x           42x         43x 43x       47x 47x 47x 47x 47x             35x 35x 35x 35x      
import { getFilterForMode, persistenceSettings } from "./canvas.js";
import { MasterAnfsRom } from "../econet.js";
import {
    configForm,
    controlId,
    PersistenceField,
    readoutId,
    renderConfigForm,
    settingFields,
    showValue,
} from "./config-form.js";
import { Parameters } from "./parameters.js";
 
/**
 * The sideways ROMs the optional fittings need, in the order they claim banks.
 *
 * @param {{model: object, hasEconet: boolean, hasMusic5000: boolean, hasTeletextAdaptor: boolean}} settings
 * @returns {string[]}
 */
export function fittedRoms({ model, hasEconet, hasMusic5000, hasTeletextAdaptor }) {
    return [
        ...(hasEconet && model.isMaster ? [MasterAnfsRom] : []),
        ...(hasMusic5000 ? ["ample.rom"] : []),
        ...(hasTeletextAdaptor ? ["ats-3.0.rom"] : []),
    ];
}
 
const restartRequired = (parameters) => Object.keys(parameters).filter((name) => parameters[name].field?.restart);
 
/** @returns {boolean} whether any of the changed settings only take effect on a freshly built machine. */
export function needsRestart(changed, parameters = Parameters) {
    return restartRequired(parameters).some((name) => name in changed);
}
 
/** @returns {boolean} whether the saved settings differ from those the running machine was built with. */
export function restartPending(settings, running, parameters = Parameters) {
    return restartRequired(parameters).some((name) => settings[name] !== running[name]);
}
 
/**
 * The Settings dialog: shows the settings, applies the ones the machine can
 * follow live as they are picked, and saves the rest when it closes. Dispatches
 * "restart-required" after saving a change the running machine cannot follow.
 */
export class Config extends EventTarget {
    constructor(settings) {
        super();
        this.settings = settings;
        this.changed = {};
        // The setting the display in use declares, which is the mode asked for
        // unless that fell back; the page says which once the display exists.
        this.persistenceSetting = undefined;
 
        const form = configForm(settings.parameters);
        this.fields = settingFields(form);
        const configuration = document.getElementById("configuration");
        configuration
            .querySelector(".modal-body")
            .append(...renderConfigForm(form, (field, value) => this.pick(field, value)));
 
        // Built before anything can change, so this is what the running machine was built with.
        this.runningSettings = this.proposedSettings();
        this.setModel(settings.model);
        this.showSaved();
        for (const field of this.fields) {
            settings.on(field.setting, () => {
                showValue(field, this.proposedSettings()[field.setting]);
                this.showDependents();
            });
        }
        settings.on("displayMode", (displayMode) => this.setDisplayMode(displayMode));
        this.setDisplayMode(settings.displayMode);
 
        configuration.addEventListener("show.bs.modal", () => {
            this.changed = {};
            this.showSaved();
        });
 
        configuration.addEventListener("hide.bs.modal", () => {
            const changed = this.changed;
            this.changed = {};
            if (Object.keys(changed).length === 0) return;
            settings.set(changed);
            // changed records which controls were touched, so a value in it can be what is already running.
            if (
                needsRestart(changed, settings.parameters) &&
                restartPending(this.proposedSettings(), this.runningSettings, settings.parameters)
            )
                this.dispatchEvent(new Event("restart-required"));
        });
 
        document.getElementById(controlId(PersistenceField)).addEventListener("input", (e) => {
            if (this.persistenceSetting) settings.set({ [this.persistenceSetting]: parseFloat(e.currentTarget.value) });
        });
        for (const { setting } of persistenceSettings()) settings.on(setting, () => this.setPersistenceSlider());
    }
 
    saved(setting) {
        return this.settings.value(setting);
    }
 
    /** Every setting the dialog shows, as it would be saved if the dialog were closed now. */
    proposedSettings() {
        return Object.fromEntries(
            this.fields.map(({ setting }) => [
                setting,
                setting in this.changed ? this.changed[setting] : this.saved(setting),
            ]),
        );
    }
 
    pick(field, value) {
        if (field.live) {
            this.settings.set({ [field.setting]: value });
            return;
        }
        this.changed[field.setting] = value;
        showValue(field, value);
        this.showDependents();
    }
 
    showSaved() {
        for (const field of this.fields) showValue(field, this.saved(field.setting));
        this.showDependents();
    }
 
    /** The parts of the dialog that follow from the settings as they would be saved. */
    showDependents() {
        const proposed = this.proposedSettings();
        for (const field of this.fields) {
            if (field.enabledWhen) document.getElementById(controlId(field)).disabled = !field.enabledWhen(proposed);
            if (field.readout)
                document.getElementById(readoutId(field)).textContent = field.readout(
                    proposed[field.setting],
                    proposed,
                );
        }
        document
            .getElementById("restart-pending")
            .classList.toggle("d-none", !restartPending(proposed, this.runningSettings, this.settings.parameters));
    }
 
    setDisplayMode(mode) {
        this.setPersistenceInUse(getFilterForMode(mode).getDisplayConfig().persistence?.setting);
    }
 
    /** The slider edits the persistence of the display in use, and is disabled for one without it. */
    setPersistenceInUse(setting) {
        this.persistenceSetting = setting;
        this.setPersistenceSlider();
    }
 
    setPersistenceSlider() {
        const slider = document.getElementById(controlId(PersistenceField));
        const afterglowMs = this.persistenceSetting ? this.settings[this.persistenceSetting] : 0;
        slider.disabled = !this.persistenceSetting;
        slider.value = afterglowMs;
        document.getElementById(readoutId(PersistenceField)).textContent = this.persistenceSetting
            ? PersistenceField.readout(afterglowMs)
            : "not available";
    }
 
    /** Names the running machine everywhere the page shows it. */
    setModel(model) {
        for (const el of document.querySelectorAll(".bbc-model")) el.textContent = model.name;
        for (const el of document.querySelectorAll(".bbc-model-short")) el.textContent = model.shortName;
        const settingsLink = document.getElementById("model-settings");
        Iif (settingsLink) settingsLink.title = `${model.name}: emulation settings`;
    }
}