0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Blockly Python Code Generator(2)

Last updated at Posted at 2022-01-26

Blockly Python Code Generator(2)

前回(Blockly Python Code Generator)では、Pythonコードを表示するのみでしたが、今回はそれにファイルに保存する機能を追加します。

File API

File APIはHTML5で追加された機能です。
ウェブアプリケーションからのファイルの使用
作成したオブジェクトをファイルとしてダウンロードするためのURLを作成することができます。
const objectURL = window.URL.createObjectURL(fileObj);

上記で取得したURLは下記関数で開放します。
URL.revokeObjectURL(objectURL);

Blob

JavaScriptで生データを使用するためのオブジェクトとして、Blobが準備されています。
Blob

テキストを保存する場合は、以下のように設定します。
const blob = new Blob([txt], { type: 'text/plain' });

このデータはFile APIに渡すことができます。
const blob = new Blob([txt], { type: 'text/plain' });
const url = URL.createObjectURL(blob);

File 保存

File APIで作成した一時的なURLに対してクリックイベントを起こすことにより、ファイル保存のトリガを実行します。
ただし、JavaScriptで直接ローカルにファイル保存することはできません。
ユーザー確認が必要になります。

具体的には以下のような処理になります。

    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    document.body.appendChild(a);
    a.download = 'sample.py';
    a.href = url;
    a.click();
    a.remove();
    URL.revokeObjectURL(url);

まとめ

前回(Blockly Python Code Generator)のファイルをベースに変更します。
ファイル保存用のボタンを追加します。
(今回は機能を実装していませんが、XMLのSave/Loadボタンも作っています。)

HTML

index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>Blockly Sample</title>
    <link rel="stylesheet" href="./styles.css">
    <link rel="stylesheet" href="./code.css">
    <script src="../blockly/blockly_compressed.js"></script>
    <script src="../blockly/blocks_compressed.js"></script>
    <script src="../blockly/msg/js/en.js"></script>
    <script src="../blockly/python_compressed.js"></script>
    <script src="./code.js"></script>
</head>

<body>
    <table>
        <tr>
            <td>
                <h1>
                    <p>Blockly Code Generator</p>
                </h1>
            </td>
        </tr>
        <tr>
            <td>
                <table>
                    <tr id="tabRow" height="1em">
                        <td id="tab_blocks" class="tabon">Blocks</td>
                        <td class="tabmin tab_collapse">&nbsp;</td>
                        <td id="tab_python" class="taboff">Python</td>
                        <td class="tabmin tab_collapse">&nbsp;</td>
                        <td id="tab_xml" class="taboff">XML</td>
                        <td class="tabmax">
                            <button id="trashButton" class="notext" title="...">
                                <img src='../blockly/media/1x1.gif' class="trash icon21">
                            </button>
                            <button id="saveButton" class="text" title="srcSave">
                                Save
                            </button>
                            <button id="loadButton" class="text" title="srcLoad">
                                Load
                            </button>
                            <button id="writeButton" class="text" title="writePython">
                                Write
                            </button>
                        </td>
                    </tr>
                </table>
            </td>
        </tr>
        <tr>
            <td height="99%" colspan=2 id="content_area">
            </td>
        </tr>
    </table>
    <div id="blocklyDiv"></div>
    <div id="content_blocks" class="content"></div>
    <pre id="content_python" class="content prettyprint lang-py"></pre>
    <textarea id="content_xml" class="content" wrap="off"></textarea>
</body>

</html>

JavaScript

ボタン機能を追加し、ファイル保存の機能を追加しました。

code.js
var Code = {};

/**
 * Blockly's main workspace.
 * @type {Blockly.WorkspaceSvg}
 */
Code.workspace = null;

Code.loadBlocks = function (defaultXml) {
    try {
        var loadOnce = window.sessionStorage.loadOnceBlocks;
    } catch (e) {
        // Firefox sometimes throws a SecurityError when accessing sessionStorage.
        // Restarting Firefox fixes this, so it looks like a bug.
        var loadOnce = null;
    }
    if ('BlocklyStorage' in window && window.location.hash.length > 1) {
        // An href with #key trigers an AJAX call to retrieve saved blocks.
        BlocklyStorage.retrieveXml(window.location.hash.substring(1));
    } else if (loadOnce) {
        // Language switching stores the blocks during the reload.
        delete window.sessionStorage.loadOnceBlocks;
        var xml = Blockly.Xml.textToDom(loadOnce);
        Blockly.Xml.domToWorkspace(xml, Code.workspace);
    } else if (defaultXml) {
        // Load the editor with default starting blocks.
        var xml = Blockly.Xml.textToDom(defaultXml);
        Blockly.Xml.domToWorkspace(xml, Code.workspace);
    } else if ('BlocklyStorage' in window) {
        // Restore saved blocks in a separate thread so that subsequent
        // initialization is not affected from a failed load.
        window.setTimeout(BlocklyStorage.restoreBlocks, 0);
    }
};

Code.bindClick = function (el, func) {
    if (typeof el == 'string') {
        el = document.getElementById(el);
    }
    el.addEventListener('click', func, true);
    el.addEventListener('touchend', func, true);
};

Code.getBBox_ = function (element) {
    var height = element.offsetHeight;
    var width = element.offsetWidth;
    var x = 0;
    var y = 0;
    do {
        x += element.offsetLeft;
        y += element.offsetTop;
        element = element.offsetParent;
    } while (element);
    return {
        height: height,
        width: width,
        x: x,
        y: y
    };
};

Code.TABS_ = ['blocks', 'python', 'xml'];
Code.TABS_DISPLAY_ = [
    'Blocks', 'Python', 'XML',
];

Code.selected = 'blocks';

Code.tabClick = function (clickedName) {
    // If the XML tab was open, save and render the content.
    if (document.getElementById('tab_xml').classList.contains('tabon')) {
        var xmlTextarea = document.getElementById('content_xml');
        var xmlText = xmlTextarea.value;
        var xmlDom = null;
        try {
            xmlDom = Blockly.Xml.textToDom(xmlText);
        } catch (e) {
            var badXml = "XML のエラーです:\n%1\n\nXML の変更をやめるには「OK」、編集を続けるには「キャンセル」を選んでください。"
            var q =
                window.confirm(badXml.replace('%1', e));
            if (!q) {
                // Leave the user on the XML tab.
                return;
            }
        }
        if (xmlDom) {
            Code.workspace.clear();
            Blockly.Xml.domToWorkspace(xmlDom, Code.workspace);
        }
    }

    // Deselect all tabs and hide all panes.
    for (var i = 0; i < Code.TABS_.length; i++) {
        var name = Code.TABS_[i];
        var tab = document.getElementById('tab_' + name);
        tab.classList.add('taboff');
        tab.classList.remove('tabon');
        document.getElementById('content_' + name).style.visibility = 'hidden';
    }

    // Select the active tab.
    Code.selected = clickedName;
    var selectedTab = document.getElementById('tab_' + clickedName);
    selectedTab.classList.remove('taboff');
    selectedTab.classList.add('tabon');
    // Show the selected pane.
    document.getElementById('content_' + clickedName).style.visibility =
        'visible';
    Code.renderContent();

    Blockly.svgResize(Code.workspace);
};

Code.renderContent = function () {
    var content = document.getElementById('content_' + Code.selected);
    var saveButton = document.getElementById('saveButton');
    var loadButton = document.getElementById('loadButton');
    var writeButton = document.getElementById('writeButton');
    saveButton.style.display = "none";
    loadButton.style.display = "none";
    writeButton.style.display = "none";
    // Initialize the pane.
    if (content.id == 'content_xml') {
        saveButton.style.display = "";
        loadButton.style.display = "";
        var xmlTextarea = document.getElementById('content_xml');
        var xmlDom = Blockly.Xml.workspaceToDom(Code.workspace);
        var xmlText = Blockly.Xml.domToPrettyText(xmlDom);
        xmlTextarea.value = xmlText;
        xmlTextarea.focus();
    } else if (content.id == 'content_python') {
        writeButton.style.display = "";
        Code.attemptCodeGeneration(Blockly.Python);
    }
    else {
    }
};

/**
 * Attempt to generate the code and display it in the UI, pretty printed.
 * @param generator {!Blockly.Generator} The generator to use.
 */
Code.attemptCodeGeneration = function (generator) {
    var content = document.getElementById('content_' + Code.selected);
    content.textContent = '';
    if (Code.checkAllGeneratorFunctionsDefined(generator)) {
        var code = generator.workspaceToCode(Code.workspace);
        content.textContent = code;
    }
};

/**
 * Check whether all blocks in use have generator functions.
 * @param generator {!Blockly.Generator} The generator to use.
 */
Code.checkAllGeneratorFunctionsDefined = function (generator) {
    var blocks = Code.workspace.getAllBlocks(false);
    var missingBlockGenerators = [];
    for (var i = 0; i < blocks.length; i++) {
        var blockType = blocks[i].type;
        if (!generator[blockType]) {
            if (missingBlockGenerators.indexOf(blockType) == -1) {
                missingBlockGenerators.push(blockType);
            }
        }
    }

    var valid = missingBlockGenerators.length == 0;
    if (!valid) {
        var msg = 'The generator code for the following blocks not specified for ' +
            generator.name_ + ':\n - ' + missingBlockGenerators.join('\n - ');
        Blockly.alert(msg);  // Assuming synchronous. No callback.
    }
    return valid;
};

var onClickWrite = function () {
    var pythonArea = document.getElementById('content_python');
    const blob = new Blob([pythonArea.textContent], { type: 'text/plain' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    document.body.appendChild(a);
    a.download = 'sample.py';
    a.href = url;
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
}

Code.init = function () {
    var container = document.getElementById('content_area');
    var onresize = function (e) {
        var bBox = Code.getBBox_(container);
        for (var i = 0; i < Code.TABS_.length; i++) {
            var el = document.getElementById('content_' + Code.TABS_[i]);
            el.style.top = bBox.y + 'px';
            el.style.left = bBox.x + 'px';
            // Height and width need to be set, read back, then set again to
            // compensate for scrollbars.
            el.style.height = bBox.height + 'px';
            el.style.height = (2 * bBox.height - el.offsetHeight) + 'px';
            el.style.width = bBox.width + 'px';
            el.style.width = (2 * bBox.width - el.offsetWidth) + 'px';
        }
        // Make the 'Blocks' tab line up with the toolbox.
        if (Code.workspace && Code.workspace.getToolbox().width) {
            document.getElementById('tab_blocks').style.minWidth =
                (Code.workspace.getToolbox().width - 38) + 'px';
            // Account for the 19 pixel margin and on each side.
        }
    };
    window.addEventListener('resize', onresize, false);

    var writeButton = document.getElementById('writeButton');
    writeButton.addEventListener('click', onClickWrite, false);

    var toolbox = document.getElementById("toolbox");
    var options = {
        toolbox: toolbox,
        // scrollbars: true,
        grid:
        {
            spacing: 25,
            length: 3,
            colour: '#ccc',
            snap: true
        },
        media: '../Blockly/media/',
        zoom:
        {
            controls: true,
            wheel: true
        }
    };
    Code.workspace = Blockly.inject('content_blocks', options);

    Code.loadBlocks('');

    Code.tabClick(Code.selected);

    Code.bindClick('trashButton',
        function () { Code.discard(); Code.renderContent(); });

    for (var i = 0; i < Code.TABS_.length; i++) {
        var name = Code.TABS_[i];
        Code.bindClick('tab_' + name,
            function (name_) { return function () { Code.tabClick(name_); }; }(name));
    }
    onresize();
    Blockly.svgResize(Code.workspace);
}

/**
 * Discard all blocks from the workspace.
 */
Code.discard = function () {
    var count = Code.workspace.getAllBlocks(false).length;
    if (count < 2 ||
        window.confirm(Blockly.Msg['DELETE_ALL_BLOCKS'].replace('%1', count))) {
        Code.workspace.clear();
        if (window.location.hash) {
            window.location.hash = '';
        }
    }
};

Promise.all(
    ["toolbox.xml"].map(async file => {
        return fetch(file).then(
            (res) => {
                return res.text();
            }
        );
    })
).then((xmls) => {
    xmls.forEach((xml) => {
        var parser = new DOMParser();
        var doc = parser.parseFromString(xml, "application/xml");
        document.body.appendChild(doc.documentElement);
    });
}).then(() => {
    Code.init();
});

作成したPythonコードを保存するところまで実装できました。
XMLのSave/Loadを実装することで、ブロックの保存もできるようになると思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?