Select categories in export dialog

Select Categories

When I was working on a library, I thought it would be really handy be able to select a whole category in the export dialog, so I decided to add it myself.

This userscript adds category names with a checkbox next to them in the export blocks dialog. When you click the checkbox, it will deselect the blocks in that category. If it doesn't do anything, then that means the blocks are used in other blocks that are selected (only in v8.0+).

This userscript actually also adds it to the unused blocks dialog.

Install

To use the userscript, you need a userscript browser extension, such as Tampermonkey. Then you can just click the link below to install the userscript.

https://ego-lay-atman-bay.github.io/snap-extensions/userscripts/select-categories.user.js

If you can't install browser extensions, then here's the source. You can run it in a javascript block, or create a bookmarklet with it.

Source
// ==UserScript==
// @name         Select Categories
// @namespace    https://ego-lay-atman-bay.github.io/snap-extensions
// @version      0.3
// @description  Adds the ability to select and deselect categories in the export and unused blocks dialogs.
// @author       ego-lay-atman-bay
// @match        https://snap.berkeley.edu/*/snap.html
// @icon         https://forum.snap.berkeley.edu/favicon/proxied?https%3A%2F%2Fd1eo0ig0pi5tcs.cloudfront.net%2Foptimized%2F2X%2Ff%2Ffec08d3829a26a75ae620be49835ef91b13ba8e9_2_32x32.png
// @grant        none
// ==/UserScript==
(function () {
    'use strict';

    function checkVersion() {
        if (typeof SnapVersion !== 'undefined') {
            let version = SnapVersion.split('.');
            return [version[0], version[1]].join('.');
        }
        else {
            return false;
        };
    };

    if (checkVersion() >= 7 || true) {

        function injectSelectCategories(func) {
            let split = func.split('lastCat,');
            split.splice(1, 0, 'lastCat, catCheckBox');
            func = split.join('');

            let start = func.indexOf('if (lastCat && (category !== lastCat))');

            if (start == -1) {
                return false;
            };

            let end = func.indexOf('}', start);

            let injectStr = "if (category !== lastCat) {\n                    y += padding;\n                    catCheckBox = new ToggleMorph(\n                        'checkbox',\n                        this,\n                        () => {\n                            var blocks = [];\n                            if (contains(this.blocks.map(b => b.category), category)) {\n                                this.blocks.forEach(block => {\n                                    if (block.category != category) {\n                                        blocks.push(block)\n                                    };\n                                });\n                                this.blocks = blocks;\n                            }\n                            else {\n                                this.body.contents.children.forEach(block => {\n                                    if (block instanceof ToggleMorph) {\n                                        if (block.element instanceof CustomReporterBlockMorph || block.element instanceof CustomCommandBlockMorph) {\n                                            if (block.element.category == category) {\n                                                if (!contains(this.blocks, block.element.definition)) {\n                                                    block.trigger()\n                                                };\n                                            };\n                                        };\n                                    };\n                                });\n                            };\n                            try {\n                                this.collectDependencies(); // v8.0+\n                            }\n                            catch (err) { // v7\n                                this.body.contents.children.forEach(checkBox => {\n                                    if (checkBox instanceof ToggleMorph) {\n                                        checkBox.refresh();\n                                    }\n                                });\n                            }\n\n                        },\n                        // category,\n                        null,\n                        () => contains(this.blocks.map(b => b.category), category),\n                        null,\n                        null                        \n                    )\n                    // catCheckBox.label.color = new Color(255, 255, 255, 1)\n                    // catCheckBox.label.fontSize = 12\n                    // catCheckBox.label.setWidth()\n                    // catCheckBox.label.setTop()\n                    // catCheckBox.element.setTop(-(catCheckBox.fullBounds().height()/2))\n                    catCheckBox.setPosition(new Point(\n                        x,\n                        y + (catCheckBox.top())\n                    ));\n                    palette.addContents(catCheckBox);\n                    txt = SpriteMorph.prototype.categoryText(category);\n                    txt.setPosition(new Point(x + catCheckBox.fullBounds().width() + padding, y));\n                    txt.refresh = function() {};\n                    palette.addContents(txt);\n                    y += catCheckBox.fullBounds().height()\n                    y += padding\n                }";

            var newFunc = [func.substring(0, start), injectStr, func.substring(end + 1, func.length)].join('');
            
            return newFunc;
        };

        console.log('adding selecting categories');

        let exportDialog = BlockExportDialogMorph.prototype.buildContents.toString();
        let unusedDialog = BlockRemovalDialogMorph.prototype.buildContents.toString();

        let newExportDialog = injectSelectCategories(exportDialog);
        let newUnusedDialog = injectSelectCategories(unusedDialog);

        if (newExportDialog) {
            const injectExport = new Function('BlockExportDialogMorph.prototype.buildContents = ' + newExportDialog);
            injectExport();
        };

        if (newUnusedDialog) {
            const injectUnused = new Function('BlockRemovalDialogMorph.prototype.buildContents = ' + newUnusedDialog);
            injectUnused();
        };
    };
})();

For use in mod

Anything surrounded by // ================= is the added stuff.

src/byob.js line 4438

if it's not there, search for BlockExportDialogMorph.prototype.buildContents = function in src/boyob.js.

BlockExportDialogMorph.prototype.buildContents = function () {
    var palette, x, y, block, checkBox, lastCat,
        padding = 4;

    // =================
    var catCheckBox
    // =================

    // create plaette
    palette = new ScrollFrameMorph(
        null,
        null,
        SpriteMorph.prototype.sliderColor
    );
    palette.color = SpriteMorph.prototype.paletteColor;
    palette.padding = padding;
    palette.isDraggable = false;
    palette.acceptsDrops = false;
    palette.contents.acceptsDrops = false;
    palette.fontSize = InputFieldMorph.prototype.fontSize;
    palette.contrast = InputFieldMorph.prototype.contrast;


    // populate palette
    x = palette.left() + padding;
    y = palette.top() + padding;
    SpriteMorph.prototype.allCategories().forEach(category => {
        this.blocks.forEach(definition => {
            if (definition.category === category) {
                // =================
                if (category !== lastCat) {
                    y += padding;
                    catCheckBox = new ToggleMorph(
                        'checkbox',
                        this,
                        () => {
                            var blocks = [];
                            if (contains(this.blocks.map(b => b.category), category)) {
                                this.blocks.forEach(block => {
                                    if (block.category != category) {
                                        blocks.push(block)
                                    };
                                });
                                this.blocks = blocks;
                            }
                            else {
                                this.body.contents.children.forEach(block => {
                                    if (block instanceof ToggleMorph) {
                                        if (block.element instanceof CustomReporterBlockMorph || block.element instanceof CustomCommandBlockMorph) {
                                            if (block.element.category == category) {
                                                if (!contains(this.blocks, block.element.definition)) {
                                                    block.trigger()
                                                };
                                            };
                                        };
                                    };
                                });
                            };
                            try {
                                this.collectDependencies(); // v8.0+
                            }
                            catch (err) { // v7
                                this.body.contents.children.forEach(checkBox => {
                                    if (checkBox instanceof ToggleMorph) {
                                        checkBox.refresh();
                                    }
                                });
                            }

                        },
                        // category,
                        null,
                        () => contains(this.blocks.map(b => b.category), category),
                        null,
                        null                        
                    )
                    // catCheckBox.label.color = new Color(255, 255, 255, 1)
                    // catCheckBox.label.fontSize = 12
                    // catCheckBox.label.setWidth()
                    // catCheckBox.label.setTop()
                    // catCheckBox.element.setTop(-(catCheckBox.fullBounds().height()/2))
                    catCheckBox.setPosition(new Point(
                        x,
                        y + (catCheckBox.top())
                    ));
                    palette.addContents(catCheckBox);
                    txt = SpriteMorph.prototype.categoryText(category);
                    txt.setPosition(new Point(x + catCheckBox.fullBounds().width() + padding, y));
                    txt.refresh = function() {}; // to avoid an error when refreshing checkboxes
                    palette.addContents(txt);
                    y += catCheckBox.fullBounds().height()
                    y += padding
                }

                // =================

                lastCat = category;
                block = definition.templateInstance();
                block.isToggleLabel = true; // mark as unrefreshable label
                checkBox = new ToggleMorph(
                    'checkbox',
                    this,
                    () => {
                        var idx = this.blocks.indexOf(definition);
                        if (idx > -1) {
                            this.blocks.splice(idx, 1);
                        } else {
                            this.blocks.push(definition);
                        }
                        this.collectDependencies();
                    },
                    null,
                    () => contains(this.blocks, definition),
                    null,
                    null,
                    this.target ? block : block.fullImage()
                );
                checkBox.setPosition(new Point(
                    x,
                    y + (checkBox.top() - checkBox.toggleElement.top())
                ));
                palette.addContents(checkBox);
                y += checkBox.fullBounds().height() + padding;
            }
        });
    });

    palette.scrollX(padding);
    palette.scrollY(padding);
    this.addBody(palette);

    this.addButton('ok', 'OK');
    this.addButton('cancel', 'Cancel');

    this.setExtent(new Point(220, 300));
    this.fixLayout();
};

If you're modding v8.0, you can just replace any of the try {} catch(err) {} with this.collectDependencies();. The catch (err) {} part is for v7.0.

If you're modding v8.1+, you'll also need to modify BlockRemovalDialogMorph.prototype.buildContents (unused dialog). Where and what to add is the same.

Compatability

This works on all versions 7 and up. If you're on 6.9 or below, it will not run.

Screenshots


If you find a bug, please let me know (here or on github).

And yes, this is a feature request in disguise.

I just realized, you can't see the text in flat design (or light theme).

edit: I just updated it to fix the color issue. The downside now, is that you can't click the text anymore to select, you need to click the checkbox.

Do you mean src/byob.js?

Yes

Update: I fixed the unused dialog not working in v8.1+.

https://ego-lay-atman-bay.github.io/snap-extensions/userscripts/select-categories.user.js