#target photoshop

/*
<javascriptresource>
<name>Rename Layers...</name>
<category>layers</category>
<enableinfo>true</enableinfo>
</javascriptresource>
*/

/*
    Rename Layers
    Renames every selected layer at once.

        append    hair + "_L"        ->  hair_L
        prepend   "front_" + hair    ->  front_hair
        replace   hair               ->  front_hair
        count     adds " #1", " #2", " #3" ... in Layers panel order

    Install: copy this file into Photoshop's Presets/Scripts folder and restart
    Photoshop. It then shows up as File > Scripts > Rename Layers...
    (or run it from anywhere with File > Scripts > Browse...).

    The whole rename is one undo step. The Background layer is left alone;
    turn it into a normal layer first if you want it renamed too.
    The dialog remembers the last name, mode and count setting.
*/

(function () {
    // true: the top layer in the Layers panel gets #1. false: the bottom layer does.
    var NUMBER_FROM_TOP = true;

    var HISTORY_NAME = "Rename Layers";
    var SETTINGS_KEY = "3d5b8e1a-6c47-4f2b-9e0d-7a1c2b4f6e83";
    var MODES = ["append", "prepend", "replace"];

    function s2t(s) { return stringIDToTypeID(s); }
    function c2t(c) { return charIDToTypeID(c); }

    // ---------- naming ----------

    function makeName(original, choice, number) {
        var name;
        if (choice.mode === "prepend") {
            name = choice.text + original;
        } else if (choice.mode === "replace") {
            name = choice.text;
        } else {
            name = original + choice.text;
        }
        if (choice.count) {
            name += " #" + number;
        }
        return name;
    }

    // Returns why the choice can't be applied, or "" when it can.
    function problemWith(choice) {
        if (choice.mode === "replace") {
            return /\S/.test(choice.text) ? "" : "Type the new name.";
        }
        return (choice.text !== "" || choice.count) ? "" : "Type a name or turn on count.";
    }

    // ---------- reading the selection ----------

    function selectedLayerIds() {
        var ref = new ActionReference();
        ref.putProperty(s2t("property"), s2t("targetLayersIDs"));
        ref.putEnumerated(s2t("document"), s2t("ordinal"), s2t("targetEnum"));
        var desc = null;
        try {
            desc = executeActionGet(ref);
        } catch (e) {
            desc = null;
        }
        if (desc && desc.hasKey(s2t("targetLayersIDs"))) {
            var ids = [];
            var list = desc.getList(s2t("targetLayersIDs"));
            for (var i = 0; i < list.count; i++) {
                ids.push(list.getReference(i).getIdentifier());
            }
            return ids;
        }
        return legacySelectedLayerIds();
    }

    // Older Photoshop versions only report the selection by stack index.
    function legacySelectedLayerIds() {
        var ids = [];
        var ref = new ActionReference();
        ref.putEnumerated(c2t("Dcmn"), c2t("Ordn"), c2t("Trgt"));
        var desc = executeActionGet(ref);
        if (desc.hasKey(s2t("targetLayers"))) {
            var offset = hasBackgroundLayer() ? 0 : 1;
            var list = desc.getList(s2t("targetLayers"));
            for (var i = 0; i < list.count; i++) {
                ids.push(layerIdAtIndex(list.getReference(i).getIndex() + offset));
            }
            return ids;
        }
        var active = new ActionReference();
        active.putProperty(s2t("property"), s2t("layerID"));
        active.putEnumerated(s2t("layer"), s2t("ordinal"), s2t("targetEnum"));
        ids.push(executeActionGet(active).getInteger(s2t("layerID")));
        return ids;
    }

    function hasBackgroundLayer() {
        try {
            app.activeDocument.backgroundLayer;
            return true;
        } catch (e) {
            return false;
        }
    }

    function layerIdAtIndex(index) {
        var ref = new ActionReference();
        ref.putProperty(s2t("property"), s2t("layerID"));
        ref.putIndex(s2t("layer"), index);
        return executeActionGet(ref).getInteger(s2t("layerID"));
    }

    function layerProperty(id, key) {
        var ref = new ActionReference();
        ref.putProperty(s2t("property"), s2t(key));
        ref.putIdentifier(s2t("layer"), id);
        return executeActionGet(ref);
    }

    function isBackground(id) {
        try {
            var desc = layerProperty(id, "background");
            return desc.hasKey(s2t("background")) && desc.getBoolean(s2t("background"));
        } catch (e) {
            return false;
        }
    }

    function layerInfo(id) {
        return {
            id: id,
            name: layerProperty(id, "name").getString(s2t("name")),
            index: layerProperty(id, "itemIndex").getInteger(s2t("itemIndex")),
            background: isBackground(id)
        };
    }

    // ---------- renaming ----------

    function renameLayer(id, name) {
        var ref = new ActionReference();
        ref.putIdentifier(s2t("layer"), id);
        var desc = new ActionDescriptor();
        desc.putReference(s2t("null"), ref);
        var props = new ActionDescriptor();
        props.putString(s2t("name"), name);
        desc.putObject(s2t("to"), s2t("layer"), props);
        executeAction(s2t("set"), desc, DialogModes.NO);
    }

    // Runs inside suspendHistory, so every rename lands in one history state.
    function applyJob(job) {
        for (var i = 0; i < job.items.length; i++) {
            try {
                renameLayer(job.items[i].id, job.items[i].newName);
            } catch (e) {
                job.failed.push(job.items[i].name);
            }
        }
    }

    // ---------- remembered settings ----------

    function isMode(mode) {
        for (var i = 0; i < MODES.length; i++) {
            if (MODES[i] === mode) {
                return true;
            }
        }
        return false;
    }

    function loadSettings() {
        var settings = { text: "", mode: "append", count: true };
        try {
            var desc = app.getCustomOptions(SETTINGS_KEY);
            if (desc.hasKey(s2t("name"))) {
                settings.text = desc.getString(s2t("name"));
            }
            if (desc.hasKey(s2t("mode")) && isMode(desc.getString(s2t("mode")))) {
                settings.mode = desc.getString(s2t("mode"));
            }
            if (desc.hasKey(s2t("count"))) {
                settings.count = desc.getBoolean(s2t("count"));
            }
        } catch (e) {
            // First run: nothing saved yet.
        }
        return settings;
    }

    function saveSettings(choice) {
        try {
            var desc = new ActionDescriptor();
            desc.putString(s2t("name"), choice.text);
            desc.putString(s2t("mode"), choice.mode);
            desc.putBoolean(s2t("count"), choice.count);
            app.putCustomOptions(SETTINGS_KEY, desc, true);
        } catch (e) {
            // Not being able to remember the settings shouldn't stop the rename.
        }
    }

    // ---------- dialog ----------

    function showDialog(settings, layers) {
        var w = new Window("dialog", "Rename Layers");
        w.orientation = "column";
        w.alignChildren = ["fill", "top"];
        w.margins = [18, 16, 18, 16];
        w.spacing = 12;

        // name [______________________]
        var nameRow = w.add("group");
        nameRow.orientation = "row";
        nameRow.alignChildren = ["left", "center"];
        nameRow.spacing = 10;
        nameRow.add("statictext", undefined, "name");
        var nameField = nameRow.add("edittext", undefined, settings.text);
        nameField.preferredSize.width = 320;
        nameField.alignment = ["fill", "center"];

        // (o) append  ( ) prepend  ( ) replace            count [x]
        var optionsRow = w.add("group");
        optionsRow.orientation = "stack";
        optionsRow.alignChildren = ["fill", "center"];

        var modeGroup = optionsRow.add("group");
        modeGroup.orientation = "row";
        modeGroup.alignment = ["left", "center"];
        modeGroup.spacing = 16;
        var appendButton = modeGroup.add("radiobutton", undefined, "append");
        var prependButton = modeGroup.add("radiobutton", undefined, "prepend");
        var replaceButton = modeGroup.add("radiobutton", undefined, "replace");

        var countGroup = optionsRow.add("group");
        countGroup.orientation = "row";
        countGroup.alignment = ["right", "center"];
        countGroup.spacing = 6;
        var countLabel = countGroup.add("statictext", undefined, "count");
        var countBox = countGroup.add("checkbox", undefined, "");

        // "hair" -> "front_hair #1"   (10 layers)
        var preview = w.add("statictext", undefined, " ");
        preview.alignment = ["fill", "center"];
        try {
            preview.graphics.foregroundColor = preview.graphics.newPen(preview.graphics.PenType.SOLID_COLOR, [0.6, 0.6, 0.6, 1], 1);
        } catch (e) {
            // Keep the default text colour.
        }

        var buttons = w.add("group");
        buttons.orientation = "row";
        buttons.alignment = ["right", "center"];
        var okButton;
        if ($.os.indexOf("Windows") !== -1) {
            okButton = buttons.add("button", undefined, "Rename", { name: "ok" });
            buttons.add("button", undefined, "Cancel", { name: "cancel" });
        } else {
            buttons.add("button", undefined, "Cancel", { name: "cancel" });
            okButton = buttons.add("button", undefined, "Rename", { name: "ok" });
        }

        appendButton.value = settings.mode === "append";
        prependButton.value = settings.mode === "prepend";
        replaceButton.value = settings.mode === "replace";
        countBox.value = settings.count;

        function currentChoice() {
            var mode = "append";
            if (prependButton.value) {
                mode = "prepend";
            } else if (replaceButton.value) {
                mode = "replace";
            }
            return { text: nameField.text, mode: mode, count: countBox.value };
        }

        function update() {
            var choice = currentChoice();
            var problem = problemWith(choice);
            okButton.enabled = problem === "";
            if (problem !== "") {
                preview.text = problem;
                return;
            }
            var first = layers[0];
            var text = "\u201C" + first.name + "\u201D  \u2192  \u201C" + makeName(first.name, choice, 1) + "\u201D";
            if (layers.length > 1) {
                text += "     (" + layers.length + " layers)";
            }
            preview.text = text;
        }

        nameField.onChanging = update;
        appendButton.onClick = update;
        prependButton.onClick = update;
        replaceButton.onClick = update;
        countBox.onClick = update;
        try {
            // Clicking the word "count" toggles the box too.
            countLabel.addEventListener("mousedown", function () {
                countBox.value = !countBox.value;
                update();
            });
        } catch (e) {
            // The box itself still works.
        }

        update();
        nameField.active = true;
        w.onShow = function () {
            nameField.active = true;
        };

        if (w.show() !== 1) {
            return null;
        }
        return currentChoice();
    }

    // ---------- main ----------

    function main() {
        if (!app.documents.length) {
            alert("Open a document and select the layers you want to rename.");
            return;
        }

        var ids = selectedLayerIds();
        var layers = [];
        var skippedBackground = false;
        for (var i = 0; i < ids.length; i++) {
            var info = layerInfo(ids[i]);
            if (info.background) {
                skippedBackground = true;
            } else {
                layers.push(info);
            }
        }
        if (!layers.length) {
            alert(skippedBackground
                ? "The Background layer can't be renamed. Turn it into a normal layer first, or select other layers."
                : "Select the layers you want to rename in the Layers panel.");
            return;
        }

        // A higher itemIndex sits higher in the Layers panel, groups included.
        layers.sort(function (a, b) {
            return NUMBER_FROM_TOP ? b.index - a.index : a.index - b.index;
        });

        var choice = showDialog(loadSettings(), layers);
        if (!choice) {
            return;
        }
        saveSettings(choice);

        var job = { items: [], failed: [] };
        for (var j = 0; j < layers.length; j++) {
            var newName = makeName(layers[j].name, choice, j + 1);
            if (newName !== layers[j].name) {
                job.items.push({ id: layers[j].id, name: layers[j].name, newName: newName });
            }
        }

        if (job.items.length) {
            $.global.__renameLayersJob = job;
            $.global.__renameLayersApply = applyJob;
            try {
                app.activeDocument.suspendHistory(HISTORY_NAME, "__renameLayersApply(__renameLayersJob)");
            } finally {
                $.global.__renameLayersJob = undefined;
                $.global.__renameLayersApply = undefined;
            }
        }

        var notes = [];
        if (job.failed.length) {
            var shown = job.failed.slice(0, 10).join(", ");
            if (job.failed.length > 10) {
                shown += " and " + (job.failed.length - 10) + " more";
            }
            notes.push("Couldn't rename: " + shown);
        }
        if (skippedBackground) {
            notes.push("The Background layer was left as it is.");
        }
        if (notes.length) {
            alert(notes.join("\n"));
        }
    }

    try {
        main();
    } catch (e) {
        alert("Rename Layers stopped: " + e.message);
    }
})();
