juanmard/icestudio

View on GitHub
controllers/menu.js

Summary

Maintainability
F
5 days
Test Coverage
/* eslint-disable camelcase */

angular
  .module('icestudio')
  .controller(
    'MenuCtrl',
    function (
      $log,
      $rootScope,
      $scope,
      $uibModal,
      _package,
      alerts,
      collections,
      common,
      gettextCatalog,
      graph,
      gui,
      project,
      shortcuts,
      tools,
      utils,
      nodeFs,
      nodePath,
      SVGO
    ) {
      'use strict';

      const _tcStr = function (str, args) {
        return gettextCatalog.getString(str, args);
      };

      if (!$scope.status) {
        $scope.status = {};
      }

      $scope.common = common;
      $scope.project = project;
      $scope.tools = tools;
      $scope.toolchain = tools.toolchain;
      $scope.workingdir = '';
      $scope.snapshotdir = '';

      $scope.openWindow = _openWindow;
      $scope.selectBoard = _selectBoard;
      $scope.openProjectDialog = _openProjectDialog;
      $scope.openProject = _openProject;
      $scope.addAsBlock = _addAsBlock;
      $scope.saveProject = _saveProject;
      $rootScope.saveProjectAs = _saveProjectAs;
      $scope.newProject = utils.newWindow;
      $scope.quit = _exit;
      $scope.fitContent = () => {
        graph.fitContent();
      };
      $scope.setProjectInformation = _setProjectInformation;
      $scope.showBoardOptions = _showBoardOptions;

      $scope.setPreferences = function () {
        $uibModal.open({
          animation: true,
          ariaLabelledBy: 'modal-title',
          ariaDescribedBy: 'modal-body',
          templateUrl: 'views/preferences.html',
          controller: 'PrefCtrl',
          scope: $scope,
          size: 'lg',
        });
      };

      $scope.selectedDeviceBoards = common.boards.filter(
        (board) => board.info.device === common.selectedDevice
      );

      $scope.selectDevice = function (device) {
        common.selectedDevice = device;
        $scope.selectedDeviceBoards = common.boards.filter(
          (board) => board.info.device === device
        );
        common.selectedProgrammer = null;
      };

      $scope.selectProgrammer = function _selectProgrammer(name) {
        common.selectedProgrammer = name;
      };

      // Convert the list of boards into a format suitable for 'menutree' directive
      $scope.boardMenu = common.devices.map(function (key) {
        return {
          name: key.name,
          children: common.boards
            .filter((x) => x.info.device === key.name)
            .map(function (x) {
              return {path: x.name, name: x.info.label};
            }),
        };
      });

      $scope.selectedDeviceBoards = common.boards.filter(
        (board) => board.info.device === common.selectedDevice
      );

      $scope.selectDevice = function (device) {
        common.selectedDevice = device;
        $scope.selectedDeviceBoards = common.boards.filter(
          (board) => board.info.device === device
        );
      };

      var zeroProject = true; // New project without changes
      var resultAlert = null;
      var winCommandOutput = null;

      var undoStack = {
        builds: [],
        changes: [],
      };

      var currentUndoStack = [];

      // Window events
      var win = gui.Window.get();
      win.on('close', function () {
        _exit();
      });

      // Darwin fix for shortcuts
      if (process.platform === 'darwin') {
        var mb = new gui.Menu({type: 'menubar'});
        mb.createMacBuiltin('Icestudio');
        win.menu = mb;
      }

      win.focus();

      // FIXME: all the args parsing below does NOT belong in the "menu controller"!

      // Parse GET url parmeters for window instance arguments all arguments will be embeded in icestudio_argv param (stringified JSON)
      // https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Objetos_globales/unescape
      // unescape is deprecated javascript function, should use decodeURI instead

      const loc = window.location.search;
      var queryStr =
        (loc.indexOf('?icestudio_argv=')
          ? decodeURI(loc)
          : '?icestudio_argv=' +
            atob(decodeURI(loc.replace('?icestudio_argv=', '')))) + '&';

      $log.debug('[cnt.menu] window.location.search:', window.location.search);
      $log.debug('[cnt.menu] queryStr:', queryStr);

      var argv = window.opener.opener ? [] : gui.App.argv;

      var val = queryStr.replace(/.*?[&\\?]icestudio_argv=(.*?)&.*/, '$1');
      if (val !== queryStr) {
        // If there are url params, compatibilize it with shell call
        var params = JSON.parse(decodeURI(val));
        $log.debug('[cnt.menu] params:', params);
        if (params) {
          for (var idx in params) {
            argv.push(params[idx]);
          }
        }
      }

      $log.debug('[cnt.menu] argv:', argv);

      var local = false;
      for (var arg of argv) {
        if (nodeFs.existsSync(arg)) {
          project.open(arg);
        } else {
          if (arg === 'local') {
            local = true;
            break;
          }
        }
      }

      if (_isEditable(project.path) || !local) {
        updateWorkingdir(project.path);
      } else {
        project.path = '';
      }

      // Plugins
      $scope.plugins = ICEpm.plugins;

      function _isEditable(fpath) {
        const cond = [_isInDefault(fpath, false), _isInInternal(fpath, false)];
        const editable = !cond[0] && !cond[1];
        $log.debug('[cnt.menu._isEditable] fpath:', fpath);
        $log.debug(
          '[cnt.menu._isEditable] _isInDefault:',
          common.DEFAULT_COLLECTION_DIR,
          cond[0]
        );
        $log.debug(
          '[cnt.menu._isEditable] _isInInternal:',
          common.INTERNAL_COLLECTIONS_DIR,
          cond[1]
        );
        $log.debug('[cnt.menu._isEditable] editable?', editable);
        return editable;
      }

      function _isInCollection(fpath, name, cpath) {
        return name
          ? fpath.startsWith(nodePath.join(cpath, name))
          : fpath.startsWith(cpath);
      }

      function _isInDefault(fpath, name) {
        return _isInCollection(fpath, name, common.DEFAULT_COLLECTION_DIR);
      }

      function _isInInternal(fpath, name) {
        return _isInCollection(fpath, name, common.INTERNAL_COLLECTIONS_DIR);
      }

      function _isInExternal(fpath) {
        return _isInCollection(fpath, name, common.get('externalCollections'));
      }

      //-- File

      $scope.export = _export;

      shortcuts.method('newProject', utils.newWindow);
      shortcuts.method('openProject', _openProjectDialog);
      shortcuts.method('saveProject', _saveProject);
      shortcuts.method('saveProjectAs', _saveProjectAs);
      shortcuts.method('quit', $scope.quit);

      function _openProjectDialog() {
        utils.openDialog('#input-open-project', '.ice', function (filepath) {
          // This is the first action, open the project in the same window
          if (zeroProject) {
            updateWorkingdir(filepath);
            project.open(filepath);
            return;
          }
          // If the file path is different, open the project in a new window
          if (project.changed || !equalWorkingFilepath(filepath)) {
            utils.newWindow(filepath);
            return;
          }
          // FIXME: should not fail silently, but produce a meaningful warning
        });
      }

      function _openProject(filepath) {
        if (zeroProject) {
          // This is the first action, open the project in the same window
          updateWorkingdir(_isEditable(filepath) ? filepath : '');
          project.open(filepath, true);
          return;
        }
        utils.newWindow(filepath, true);
      }

      function _saveProject() {
        if (common.isEditingSubmodule) {
          alerts.alert({
            icon: 'save',
            title: _tcStr('Saving submodules is not supported yet'),
            body: _tcStr(
              "However, you can export this submodule through 'File > Save as...'"
            ),
          });
          return;
        }
        var filepath = project.path;
        if (filepath) {
          project.save(filepath, function () {
            reloadCollectionsIfRequired(filepath);
          });
          resetChangedStack();
        } else {
          _saveProjectAs();
        }
      }

      function _saveProjectAs() {
        function _saveProjectAsDialog() {
          utils.saveDialog('#input-save-project', '.ice', function (filepath) {
            updateWorkingdir(filepath);
            project.save(filepath, function () {
              reloadCollectionsIfRequired(filepath);
            });
            resetChangedStack();
          });
        }

        if (common.isEditingSubmodule) {
          alerts.confirm({
            icon: 'hdd-o',
            title: _tcStr('Export submodule'),
            body: _tcStr(
              'You are about to export a submodule, NOT the full design. Do you want to continue?'
            ),
            onok: function () {
              _saveProjectAsDialog();
            },
            invokeOnCloseOff: false,
          });
        } else {
          _saveProjectAsDialog();
        }
      }

      function reloadCollectionsIfRequired(filepath) {
        if (_isInInternal(filepath, false)) {
          collections.loadInternalCollections();
        }
        if (_isInExternal(filepath, false)) {
          collections.loadExternalCollections();
        }
      }

      function _addAsBlock() {
        var notification = true;
        utils.openDialog('#input-add-as-block', '.ice', function (filepaths) {
          filepaths = filepaths.split(';');
          for (var i in filepaths) {
            project.addBlockFile(filepaths[i], notification);
          }
        });
      }

      function _export(etype) {
        switch (etype) {
          case 'v':
            exportFromCompiler('verilog', 'Verilog', '.v');
            break;
          case 'pcf':
            exportFromCompiler('pcf', 'PCF', '.pcf');
            break;
          case 'tb':
            exportFromCompiler('testbench', 'Testbench', '.v');
            break;
          case 'gtkw':
            exportFromCompiler('gtkwave', 'GTKWave', '.gtkw');
            break;
          case 'blif':
            exportFromBuilder('blif', 'BLIF', '.blif');
            break;
          case 'asc':
            exportFromBuilder('asc', 'ASC', '.asc');
            break;
          case 'bin':
            exportFromBuilder('bin', 'Bitstream', '.bin');
            break;
          default:
            $log.error(`Unknown export type ${etype}!`);
        }
      }

      function exportFromCompiler(id, name, ext) {
        checkGraph()
          .then(function () {
            // TODO: export list files
            utils.saveDialog('#input-export-' + id, ext, function (filepath) {
              // Save the compiler result
              var data = project.compile(id)[0].content;
              common
                .saveFile(filepath, data)
                .then(function () {
                  alertify.success(
                    _tcStr('{{name}} exported', {
                      name: name,
                    })
                  );
                })
                .catch(function (error) {
                  alertify.error(error, 30);
                });
              // Update the working directory
              updateWorkingdir(filepath);
            });
          })
          .catch(function () {});
      }

      function exportFromBuilder(id, name, ext) {
        checkGraph()
          .then(function () {
            return tools.buildCode();
          })
          .then(function () {
            resetBuildStack();
          })
          .then(function () {
            utils.saveDialog('#input-export-' + id, ext, function (filepath) {
              // Copy the built file
              if (
                utils.copySync(
                  nodePath.join(common.BUILD_DIR, 'hardware' + ext),
                  filepath
                )
              ) {
                alertify.success(
                  _tcStr('{{name}} exported', {
                    name: name,
                  })
                );
              }
              // Update the working directory
              updateWorkingdir(filepath);
            });
          })
          .catch(function () {});
      }

      function updateWorkingdir(filepath) {
        $scope.workingdir = nodePath.dirname(filepath) + nodePath.sep;
      }

      function equalWorkingFilepath(filepath) {
        return $scope.workingdir + project.name + '.ice' === filepath;
      }

      function _exit() {
        function __exit() {
          //win.hide();
          win.close(true);
        }

        if (!project.changed) {
          __exit();
          return;
        }
        alerts.confirm({
          icon: 'warning',
          title: _tcStr('Do you want to close the application?'),
          body: _tcStr('Your changes will be lost if you don’t save them'),
          onok: function () {
            __exit();
          },
        });
      }

      //-- Edit

      $scope.undoGraph = () => {
        graph.undo();
      };
      $scope.redoGraph = () => {
        graph.redo();
      };
      $scope.cutSelected = () => {
        graph.cutSelected();
      };
      $scope.copySelected = () => {
        graph.copySelected();
      };
      $scope.pasteSelected = _pasteSelected;
      $scope.pasteAndCloneSelected = _pasteAndCloneSelected;
      $scope.selectAll = _selectAll;

      shortcuts.method('undoGraph', $scope.undoGraph);
      shortcuts.method('redoGraph', $scope.redoGraph);
      shortcuts.method('redoGraph2', $scope.redoGraph);
      shortcuts.method('cutSelected', $scope.cutSelected);
      shortcuts.method('copySelected', $scope.copySelected);
      shortcuts.method('pasteAndCloneSelected', $scope.pasteAndCloneSelected);
      shortcuts.method('pasteSelected', $scope.pasteSelected);
      shortcuts.method('selectAll', $scope.selectAll);
      shortcuts.method('fitContent', $scope.fitContent);

      var paste = true;

      function _pasteSelected() {
        if (paste) {
          paste = false;
          graph.pasteSelected();
          setTimeout(function () {
            paste = true;
          }, 250);
        }
      }

      function _pasteAndCloneSelected() {
        if (paste) {
          graph.pasteAndCloneSelected();
        }
      }

      function _selectAll() {
        checkGraph()
          .then(function () {
            graph.selectAll();
          })
          .catch(function () {});
      }

      $(document).on('infoChanged', function (evt, newValues) {
        var values = getProjectInformation();
        if (!_.isEqual(values, newValues)) {
          graph.setInfo(values, newValues, project);
          alertify.message(
            _tcStr('Project information updated') +
              '.<br>' +
              _tcStr('Click here to view'),
            5
          ).callback = function (isClicked) {
            if (isClicked) {
              _setProjectInformation();
            }
          };
        }
      });

      function _setProjectInformation() {
        var values = getProjectInformation();

        var i;
        var content = [];
        var messages = [
          _tcStr('Name'),
          _tcStr('Version'),
          _tcStr('Description'),
          _tcStr('Author'),
        ];
        var n = messages.length;
        var image = values[4];
        var blankImage =
          'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
        content.push('<form><fieldset>');
        for (i in messages) {
          content.push(`<label>${messages[i]}</label>
  <input class="ajs-input" id="input${i}" type="text" value="${values[i]}">`);
        }
        const img = image ? 'data:image/svg+xml,' + image : blankImage;
        content.push(`<label>${_tcStr('Image')}</label>
          <div class="btn-group btn-group-sm" role="group" aria-label="Image buttons" style="display: inline-flex;">
            <label
            for="input-open-svg"
            type="button" class="btn btn-default"
          ><i class="fa fa-fw fa-folder-open-o" aria-hidden="true"></i> ${_tcStr(
            'Load...'
          )}</label>
          ${
            image
              ? `<label
              id="save-svg"
              for="input-save-svg"
              type="button" class="btn btn-default"
            ><i class="fa fa-fw fa-hdd-o" aria-hidden="true"></i> ${_tcStr(
              'Save as...'
            )}</label>
            <label
              id="reset-svg"
              type="button" class="btn btn-default"
            ><i class="fa fa-fw fa-trash" aria-hidden="true"></i> ${_tcStr(
              'Reset'
            )}</label>`
              : ''
          }
          </div>
          <div class="project-img-container"><img id="preview-svg" src="${img}" style="pointer-events:none"></div>
          <input id="input-open-svg" type="file" accept=".svg" class="hidden">
          <input id="input-save-svg" type="file" accept=".svg" class="hidden" nwsaveas="image.svg">
        </fieldset></form>`);
        // Restore values
        for (i in messages) {
          $('#input' + i).val(values[i]);
        }
        $('#preview-svg').attr('src', img);

        function registerSave() {
          // Save SVG
          var label = $('#save-svg');
          if (!image) {
            label.addClass('disabled');
            label.attr('for', '');
            return;
          }
          label.removeClass('disabled');
          label.attr('for', 'input-save-svg');
          $('#input-save-svg')
            .unbind('change')
            .change(function () {
              var filepath = $(this).val();
              if (!filepath.endsWith('.svg')) {
                filepath += '.svg';
              }
              nodeFs.writeFile(filepath, decodeURI(image), function (err) {
                if (err) {
                  throw err;
                }
              });
              $(this).val('');
            });
        }

        // Restore onshow
        var prevOnshow = alertify.confirm().get('onshow') || function () {};

        alertify.confirm().set('onshow', function () {
          prevOnshow();

          // Open SVG
          $('#input-open-svg')
            .unbind('change')
            .change(function () {
              nodeFs.readFile($(this).val(), 'utf8', function (err, data) {
                if (err) {
                  throw err;
                }
                SVGO.optimize(data, function (result) {
                  image = encodeURI(result.data);
                  registerSave();
                  $('#preview-svg').attr('src', 'data:image/svg+xml,' + image);
                });
              });
              $(this).val('');
            });

          registerSave();

          // Reset SVG
          $('#reset-svg').click(function () {
            image = '';
            registerSave();
            $('#preview-svg').attr('src', blankImage);
          });
        });

        alerts.confirm({
          icon: 'tag',
          title: _tcStr('Project Information'),
          body: content.join('\n'),
          onok: function () {
            var newValues = [];
            for (var i = 0; i < n; i++) {
              newValues.push($('#input' + i).val());
            }
            newValues.push(image);
            if (!_.isEqual(values, newValues)) {
              if (
                common.isEditingSubmodule &&
                common.submoduleId &&
                common.allDependencies[common.submoduleId]
              ) {
                graph.setBlockInfo(values, newValues, common.submoduleId);
              } else {
                graph.setInfo(values, newValues, project);
              }
              alertify.success(_tcStr('Project information updated'));
            }
          },
        });
      }

      function getProjectInformation() {
        var p =
          common.submoduleId && common.allDependencies[common.submoduleId]
            ? common.allDependencies[common.submoduleId].package
            : project.get('package');
        return [p.name, p.version, p.description, p.author, p.image];
      }

      $scope.toggleBoardRules = function () {
        graph.setBoardRules(!common.get('boardRules'));
        alertify.success(
          _tcStr(
            'Board rules ' + (common.get('boardRules') ? 'enabled' : 'disabled')
          )
        );
      };

      //-- Board options

      var boardDialog = undefined;

      // Create a new dialog based on 'alert';
      // so that 'Board options' window is not affected by other alerts
      // See https://alertifyjs.com/factory.html
      if (!alertify.boardWindow) {
        alertify.dialog(
          'boardWindow',
          function factory() {
            return {
              main: function (content) {
                this.setContent(content);
              },
              build: function () {
                this.setHeader($('#boardoptshead')[0]);
                boardDialog = this.elements.dialog;
                boardDialog.classList.add('board-window');
              },
            };
          },
          false,
          'alert'
        );
      }

      function _showBoardOptions() {
        if (alertify.boardWindow().isOpen()) {
          alertify.boardWindow().close();
          return;
        }
        if (common.selectedBoard) {
          common.selectedDevice = common.selectedBoard.info.device;
        }
        alertify
          .boardWindow($('#boardopts')[0])
          .setting({
            frameless: true,
            autoReset: false,
            modal: false,
            pinnable: false,
            closable: true,
            closableByDimmer: false,
            movable: true,
            moveBounded: true,
            maximizable: true,
            resizable: true,
          })
          .resizeTo(600, 100);
      }

      $scope.toggleBoardWindowSize = function (open) {
        if (open) {
          boardDialog.classList.add('is-dropdown-open');
        } else {
          boardDialog.classList.remove('is-dropdown-open');
        }
      };

      $scope.showPCF = function () {
        gui.Window.open(
          'viewers/plain/pcf.html?board=' + common.selectedBoard.name,
          {
            title: common.selectedBoard.info.label + ' - PCF',
            focus: true,
            //toolbar: false,
            resizable: true,
            width: 700,
            height: 700,
            min_width: 300,
            min_height: 300,
            icon: 'images/icestudio-logo.png',
          }
        );
      };

      $scope.svgPinoutAvailable = function () {
        if (common.selectedBoard) {
          return nodeFs.existsSync(
            nodePath.join(
              'resources',
              'boards',
              common.selectedBoard.name,
              'pinout.svg'
            )
          );
        }
        return false;
      };
      $scope.showSvgPinout = function () {
        var board = common.selectedBoard;
        if (this.svgPinoutAvailable()) {
          gui.Window.open('viewers/svg/pinout.html?board=' + board.name, {
            title: common.selectedBoard.info.label + ' - Pinout',
            focus: true,
            //toolbar: false,
            resizable: true,
            width: 500,
            height: 700,
            min_width: 300,
            min_height: 300,
            icon: 'images/icestudio-logo.png',
          });
        } else {
          alertify.warning(
            _tcStr('{{board}} pinout not defined', {
              board: `<b>${board.info.label}</b>`,
            }),
            5
          );
        }
      };

      $scope.showDatasheet = function () {
        var board = common.selectedBoard;
        if (board.info.datasheet) {
          gui.Shell.openExternal(board.info.datasheet);
        } else {
          alertify.error(
            _tcStr('{{board}} datasheet not defined', {
              board: `<b>${board.info.label}</b>`,
            }),
            5
          );
        }
      };

      $scope.showBoardRules = function () {
        var board = common.selectedBoard;
        var rules = JSON.stringify(board.rules);
        if (rules !== '{}') {
          var encRules = encodeURIComponent(rules);
          gui.Window.open('viewers/table/rules.html?rules=' + encRules, {
            title: common.selectedBoard.info.label + ' - Rules',
            focus: true,
            // toolbar: false,
            resizable: false,
            width: 500,
            height: 500,
            min_width: 300,
            min_height: 300,
            icon: 'images/icestudio-logo.png',
          });
        } else {
          alertify.error(
            _tcStr('{{board}} rules not defined', {
              board: `<b>${board.info.label}</b>`,
            }),
            5
          );
        }
      };

      function _openWindow(url, title) {
        $log.log(url);
        return gui.Window.open(url, {
          title: title,
          focus: true,
          //toolbar: false,
          resizable: true,
          width: 700,
          height: 400,
          min_width: 300,
          min_height: 300,
          icon: 'images/icestudio-logo.png',
        });
      }

      $scope.showCommandOutput = function () {
        winCommandOutput = _openWindow(
          'viewers/plain/output.html?content=' +
            encodeURIComponent(common.commandOutput),
          _tcStr('Command output')
        );
      };

      $(document).on('commandOutputChanged', function (evt, commandOutput) {
        if (winCommandOutput) {
          try {
            winCommandOutput.window.location.href =
              'viewers/plain/output.html?content=' +
              encodeURIComponent(commandOutput);
          } catch (e) {
            winCommandOutput = null;
          }
        }
      });

      //-- Boards

      $(document).on('boardChanged', function (evt, board) {
        if (common.selectedBoard.name !== board.name) {
          graph.selectBoard(board);
          common.set('board', common.selectedBoard.name);
        }
      });

      function _selectBoard(name) {
        let board = undefined;
        for (const val of common.boards) {
          if (val.name === name) {
            board = val;
            break;
          }
        }

        if (!common.selectedBoard || graph.isEmpty()) {
          _selectBoardNotify(board);
          return;
        }

        if (common.selectedBoard.name !== name) {
          alerts.confirm({
            icon: 'microchip',
            title: _tcStr('Do you want to change to {{name}} board?', {
              name: `<b>${board.info.label}</b>`,
            }),
            body: _tcStr('The current FPGA I/O configuration will be lost.'),
            onok: function () {
              _selectBoardNotify(board);
            },
          });
        }

        function _selectBoardNotify(board) {
          graph.selectBoard(board, true);
          common.setBoard(common.selectedBoard);
          var prog = board.info.prog;
          if (!prog.includes(common.selectedProgrammer)) {
            if (!prog.length === 1) {
              common.selectedProgrammer = null;
              return;
            }
            common.selectedProgrammer = prog[0];
            common.setProgrammer(common.selectedProgrammer);
          }
        }
      }

      //-- Tools

      $scope.verifyCode = function () {
        checkGraph()
          .then(function () {
            return tools.verifyCode(
              _tcStr('Start verification'),
              _tcStr('Verification done')
            );
          })
          .catch(function () {});
      };

      var submoduleActionAlertBody = _tcStr(
        'Inside submodules, <strong><i class="fa fa-fw fa-check"></i> Verify</strong> is supported only.'
      );

      $scope.buildCode = function () {
        if (common.isEditingSubmodule) {
          alerts.alert({
            icon: 'gear',
            title: _tcStr('Building submodules is not supported yet'),
            body: submoduleActionAlertBody,
          });
          return;
        }

        checkGraph()
          .then(function () {
            return tools.buildCode(_tcStr('Start build'), _tcStr('Build done'));
          })
          .then(function () {
            resetBuildStack();
          })
          .catch(function () {});
      };

      $scope.uploadCode = function () {
        if (common.isEditingSubmodule) {
          alerts.alert({
            icon: 'rocket',
            title: _tcStr('Uploading submodules is not supported yet'),
            body: submoduleActionAlertBody,
          });
          return;
        }

        checkGraph()
          .then(function () {
            return tools.uploadCode(
              _tcStr('Start upload'),
              _tcStr('Upload done')
            );
          })
          .then(function () {
            resetBuildStack();
          })
          .catch(function () {});
      };

      function checkGraph() {
        return new Promise(function (resolve, reject) {
          if (!graph.isEmpty()) {
            resolve();
          } else {
            if (resultAlert) {
              resultAlert.dismiss(true);
            }
            resultAlert = alertify.warning(_tcStr('Add a block to start'), 5);
            reject();
          }
        });
      }

      $scope.openUrl = function (url, $event) {
        $event.preventDefault();
        utils.openUrlExternalBrowser(url);
        return false;
      };

      const canCheckVersion =
        _package.repository !== undefined && _package.sha !== undefined;

      $scope.about = function () {
        alerts.alert({
          icon: 'heart-o',
          title: 'Icestudio, visual editor for Verilog designs',
          body: $('#about')[0],
          onok: function () {
            if (canCheckVersion) {
              // checkForNewVersion
              $.getJSON(
                _package.repository.replace(
                  'github.com',
                  'api.github.com/repos'
                ) +
                  '/tags' +
                  '?_tsi=' +
                  new Date().getTime(),
                function (result) {
                  if (result) {
                    const latest = result
                      .find((x) => x.name === 'nightly')
                      .commit.sha.substring(0, 8);
                    const msg =
                      latest === _package.sha
                        ? 'Icestudio is up to date!'
                        : `Current: ${_package.sha}
                    <br/>
                    Latest: ${latest}<br/>
                    <a class="action-open-url-external-browser" href="${_package.repository}/releases" target="_blank">Go to GitHub Releases</a>`;
                    alertify.notify(
                      `<div class="new-version-notifier-box">
                      <div class="new-version-notifier-box--text">
                        ${msg}
                      </div>
                    </div>`,
                      'notify',
                      10
                    );
                  }
                }
              );
            }
          },
          label: canCheckVersion
            ? _tcStr('Check for updates...')
            : _tcStr('Close'),
          invokeOnCloseOff: false,
        });
      };

      // Events

      $(document).on('stackChanged', function (evt, undoStack) {
        currentUndoStack = undoStack;
        var undoStackString = JSON.stringify(undoStack);
        project.changed = JSON.stringify(undoStack.changes) !== undoStackString;
        project.updateTitle();
        zeroProject = false;
        common.hasChangesSinceBuild =
          JSON.stringify(undoStack.builds) !== undoStackString;
        utils.rootScopeSafeApply();
      });

      function resetChangedStack() {
        undoStack.changes = currentUndoStack;
        project.changed = false;
        project.updateTitle();
      }

      function resetBuildStack() {
        undoStack.builds = currentUndoStack;
        common.hasChangesSinceBuild = false;
        utils.rootScopeSafeApply();
      }

      $(document).on('keydown', function (event) {
        event.stopImmediatePropagation();
        if (
          shortcuts.execute(event, {
            prompt: false,
            disabled: !graph.isEnabled(),
          }).preventDefault
        ) {
          event.preventDefault();
        }
      });

      function takeSnapshot() {
        win.capturePage(function (img) {
          var base64Data = img.replace(
            /^data:image\/(png|jpg|jpeg);base64,/,
            ''
          );
          saveSnapshot(base64Data);
        }, 'png');
      }

      function saveSnapshot(base64Data) {
        utils.saveDialog('#input-save-snapshot', '.png', function (filepath) {
          nodeFs.writeFile(filepath, base64Data, 'base64', function (err) {
            $scope.snapshotdir = nodePath.dirname(filepath) + nodePath.sep;
            $scope.$apply();
            if (!err) {
              alertify.success(
                _tcStr('Image {{name}} saved', {
                  name: `<b>${common.basename(filepath)}</b>`,
                })
              );
            } else {
              throw err;
            }
          });
        });
      }

      // -- Tools

      shortcuts.method('verifyCode', $scope.verifyCode);
      shortcuts.method('buildCode', $scope.buildCode);
      shortcuts.method('uploadCode', $scope.uploadCode);

      // -- Misc

      shortcuts.method('stepUp', graph.stepUp);
      shortcuts.method('stepDown', graph.stepDown);
      shortcuts.method('stepLeft', graph.stepLeft);
      shortcuts.method('stepRight', graph.stepRight);

      shortcuts.method('removeSelected', graph.removeSelected);
      shortcuts.method('back', function () {
        if (graph.isEnabled()) {
          graph.removeSelected();
        } else {
          $rootScope.breadcrumbsBack();
        }
      });

      shortcuts.method('takeSnapshot', takeSnapshot);
    }
  );