juanmard/icestudio

View on GitHub
services/utils.js

Summary

Maintainability
F
3 days
Test Coverage
angular
  .module('icestudio')
  .service(
    'utils',
    function (
      $rootScope,
      alerts,
      gettextCatalog,
      common,
      _package,
      window,
      nodeFs,
      nodeFse,
      nodePath,
      gui
    ) {
      'use strict';

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

      function disableEvent(event) {
        event.stopPropagation();
        event.preventDefault();
      }

      this.enableClickEvents = function () {
        document.removeEventListener('click', disableEvent, true);
      };

      this.disableClickEvents = function () {
        document.addEventListener('click', disableEvent, true);
      };

      this.enableKeyEvents = function () {
        document.removeEventListener('keyup', disableEvent, true);
        document.removeEventListener('keydown', disableEvent, true);
        document.removeEventListener('keypress', disableEvent, true);
      };

      this.disableKeyEvents = function () {
        document.addEventListener('keyup', disableEvent, true);
        document.addEventListener('keydown', disableEvent, true);
        document.addEventListener('keypress', disableEvent, true);
      };

      this.setLocale = function (locale) {
        locale = splitLocale(locale);
        var supported = getSupportedLanguages();
        var bestLang = bestLocale(locale, supported);
        gettextCatalog.setCurrentLanguage(bestLang);
        gettextCatalog.loadRemote(
          nodePath.join(common.LOCALE_DIR, bestLang, bestLang + '.json')
        );
        var collections = [common.defaultCollection]
          .concat(common.internalCollections)
          .concat(common.externalCollections);
        for (var c in collections) {
          var collection = collections[c];
          var filepath = nodePath.join(
            collection.path,
            'locale',
            bestLang,
            bestLang + '.json'
          );
          if (nodeFs.existsSync(filepath)) {
            gettextCatalog.loadRemote('file://' + filepath);
          }
        }
        return bestLang;
      };

      function splitLocale(locale) {
        var ret = {};
        var list = locale.split('_');
        if (list.length > 0) {
          ret.lang = list[0];
        }
        if (list.length > 1) {
          ret.country = list[1];
        }
        return ret;
      }

      function getSupportedLanguages() {
        var supported = [];
        nodeFs.readdirSync(common.LOCALE_DIR).forEach((element) => {
          if (
            nodeFs
              .lstatSync(nodePath.join(common.LOCALE_DIR, element))
              .isDirectory()
          ) {
            supported.push(splitLocale(element));
          }
        });
        return supported;
      }

      function bestLocale(locale, supported) {
        var i;
        // 1. Try complete match
        if (locale.country) {
          for (i = 0; i < supported.length; i++) {
            if (
              locale.lang === supported[i].lang &&
              locale.country === supported[i].country
            ) {
              return supported[i].lang + '_' + supported[i].country;
            }
          }
        }
        // 2. Try lang match
        for (i = 0; i < supported.length; i++) {
          if (locale.lang === supported[i].lang) {
            return (
              supported[i].lang +
              (supported[i].country ? '_' + supported[i].country : '')
            );
          }
        }
        // 3. Return default lang
        return 'en';
      }

      this.renderForm = function (specs, callback) {
        var content = [];
        content.push('<form><fieldset>');
        for (var i in specs) {
          var spec = specs[i];
          switch (spec.type) {
            case 'text':
              if (spec.label) {
                content.push(`<label>${spec.label}</label>`);
              }
              content.push(
                `<input class="ajs-input" type="text" id="form${i}"/>`
              );
              break;
            case 'checkbox':
              content.push(`<div class="checkbox">
              <label><input
                type="checkbox"
                ${spec.value ? 'checked ' : ''}
                id="form${i}"
              />${spec.label}</label>
            </div>`);
              break;
            case 'combobox':
              var options = spec.options
                .map(function (option) {
                  return `<option value="${
                    option.value
                  }" ${spec.value === option.value ? ' selected' : ''}>${option.label}</option>`;
                })
                .join('');
              content.push(`<div class="form-group">
              <label style="font-weight:normal">${spec.label}</label>
              <select class="form-control" id="form${i}">${options}</select>
            </div>`);
              break;
          }
        }
        content.push('</fieldset></form>');
        alerts.confirm({
          icon: specs[0].icon || 'question-circle',
          title: specs[0].title || 'Form',
          body: content.join('\n'),
          onok: (evt) => {
            var values = [];
            if (callback) {
              for (var i in specs) {
                var spec = specs[i];
                switch (spec.type) {
                  case 'text':
                  case 'combobox':
                    values.push($('#form' + i).val());
                    break;
                  case 'checkbox':
                    values.push($('#form' + i).prop('checked'));
                    break;
                }
              }
              callback(evt, values);
            }
          },
        });
        // Set default input values
        $('#form0').select();
        for (var i in specs) {
          var spec = specs[i];
          switch (spec.type) {
            case 'text':
            case 'combobox':
              $('#form' + i).val(spec.value);
              break;
            case 'checkbox':
              $('#form' + i).prop('checked', spec.value);
              break;
          }
        }
      };

      this.copySync = function (orig, dest) {
        if (!nodeFs.existsSync(orig)) {
          return false;
        }
        try {
          nodeFse.copySync(orig, dest);
          return true;
        } catch (e) {
          alertify.error(
            _tcStr('Error: {{error}}', {
              error: e.toString(),
            }),
            30
          );
        }
        return false;
      };

      this.findIncludedFiles = function (code) {
        var ret = [];
        var patterns = [
          /[\n|\s]\/\/\s*@include\s+([^\s]*\.(v|vh))(\n|\s)/g,
          /[\n|\s][^\/]?\"(.*\.list?)\"/g,
        ];
        for (var p in patterns) {
          var match;
          while ((match = patterns[p].exec(code))) {
            var file = match[1].replace(/ /g, '');
            if (ret.indexOf(file) === -1) {
              ret.push(file);
            }
          }
        }
        return ret;
      };

      this.openDialog = function (inputID, ext, callback) {
        var chooser = $(inputID);
        chooser.unbind('change');
        chooser.change(function () {
          if (callback) {
            callback($(this).val());
          }
          $(this).val('');
        });
        chooser.trigger('click');
      };

      this.saveDialog = function (inputID, ext, callback) {
        var chooser = $(inputID);
        chooser.unbind('change');
        chooser.change(function () {
          var filepath = $(this).val();
          if (!filepath.endsWith(ext)) {
            filepath += ext;
          }
          if (callback) {
            callback(filepath);
          }
          $(this).val('');
        });
        chooser.trigger('click');
      };

      this.updateWindowTitle = function (title) {
        window.get().title = title;
      };

      this.rootScopeSafeApply = function () {
        if (!$rootScope.$$phase) {
          $rootScope.$apply();
        }
      };

      this.parsePortLabel = function (data, pattern) {
        // e.g: name[x:y]
        var match,
          ret = {};
        var maxSize = 95;
        pattern = pattern || common.PATTERN_PORT_LABEL;
        match = pattern.exec(data);
        if (match && match[0] === match.input) {
          ret.name = match[1] ? match[1] : '';
          ret.rangestr = match[2];
          if (match[2]) {
            if (match[3] > maxSize || match[4] > maxSize) {
              alertify.warning(_tcStr('Maximum bus size: 96 bits'), 5);
              return null;
            } else {
              if (match[3] > match[4]) {
                ret.range = _.range(match[3], parseInt(match[4]) - 1, -1);
              } else {
                ret.range = _.range(match[3], parseInt(match[4]) + 1, +1);
              }
            }
          }
          return ret;
        }
        return null;
      };

      this.parseParamLabel = function (data, pattern) {
        // e.g: name
        var match,
          ret = {};
        pattern = pattern || common.PATTERN_PARAM_LABEL;
        match = pattern.exec(data);
        if (match && match[0] === match.input) {
          ret.name = match[1] ? match[1] : '';
          return ret;
        }
        return null;
      };

      const fastCopy = require('fast-copy');

      this.clone = function (data) {
        // Very slow in comparison but more stable for all types
        // of objects, if fails, rollback to JSON method or try strict
        // on fast-copy module
        //return  JSON.parse(JSON.stringify(data));
        return fastCopy(data);
      };

      const nodeSha1 = require('sha1');

      this.dependencyID = function (dependency) {
        if (dependency.package && dependency.design) {
          return nodeSha1(
            JSON.stringify(dependency.package) +
              JSON.stringify(dependency.design)
          );
        }
      };

      this.newWindow = function (filepath, local) {
        var params = false;

        if (typeof filepath !== 'undefined') {
          params = {
            filepath: filepath,
          };
        }

        if (typeof local !== 'undefined' && local === true) {
          if (params === false) {
            params = {};
          }
          params.local = 'local';
        }
        // To pass parameters to the new project window, we use de GET parameter "icestudio_argv"
        // that contains the same arguments that shell call, in this way the two calls will be
        // compatible.
        // If in the future you will add more paremeters to the new window , you should review
        // controllers/menu.js even if all parameters that arrive are automatically parse

        var url =
          'index.html' +
          (params === false
            ? ''
            : '?icestudio_argv=' + encodeURI(btoa(JSON.stringify(params))));
        // Create a new window and get it.
        // new-instance and new_instance are necesary for OS compatibility
        // to avoid crash on new window project after close parent
        // (little trick for nwjs bug).
        //url='index.html?icestudio_argv=fsdfsfa';

        gui.Window.open(url, {
          // new_instance: true,  //Deprecated for new nwjs versios
          //      'new_instance': true,  //Deprecated for new nwjs versios
          position: 'center',
          //        'toolbar': false,   //Deprecated for new nwjs versios
          width: 900,
          height: 600,
          show: true,
        });
      };

      this.coverPath = coverPath;

      function coverPath(filepath) {
        return '"' + filepath + '"';
      }

      this.mergeDependencies = function (type, block) {
        if (type in common.allDependencies) {
          return; // If the block is already in dependencies
        }
        // Merge the block's dependencies
        var deps = block.dependencies;
        for (var depType in deps) {
          if (!(depType in common.allDependencies)) {
            common.allDependencies[depType] = deps[depType];
          }
        }
        // Add the block as a dependency
        delete block.dependencies;
        common.allDependencies[type] = block;
      };

      const nodeCP = require('copy-paste');

      this.copyToClipboard = function (selection, graph) {
        var cells = selectionToCells(selection, graph);
        var clipboard = {
          icestudio: this.cellsToProject(cells, graph),
        };

        // Send the clipboard object the global clipboard as a string
        nodeCP.copy(JSON.stringify(clipboard), function () {
          // Success
        });
      };

      const nodeGetOS = require('getos');

      this.pasteFromClipboard = function (callback) {
        nodeCP.paste(function (err, text) {
          if (err) {
            if (common.LINUX) {
              // xclip installation message
              var cmd = '';
              var message = _tcStr('{{app}} is required.', {
                app: '<b>xclip</b>',
              });
              nodeGetOS(function (e, os) {
                if (!e) {
                  if (
                    os.dist.indexOf('Debian') !== -1 ||
                    os.dist.indexOf('Ubuntu Linux') !== -1 ||
                    os.dist.indexOf('Linux Mint') !== -1
                  ) {
                    cmd = 'sudo apt-get install xclip';
                  } else if (os.dist.indexOf('Fedora')) {
                    cmd = 'sudo dnf install xclip';
                  } else if (
                    os.dist.indexOf('RHEL') !== -1 ||
                    os.dist.indexOf('RHAS') !== -1 ||
                    os.dist.indexOf('Centos') !== -1 ||
                    os.dist.indexOf('Red Hat Linux') !== -1
                  ) {
                    cmd = 'sudo yum install xclip';
                  } else if (os.dist.indexOf('Arch Linux') !== -1) {
                    cmd = 'sudo pacman install xclip';
                  }
                  if (cmd) {
                    message +=
                      ' ' +
                      _tcStr('Please run: {{cmd}}', {
                        cmd: '<br><b><code>' + cmd + '</code></b>',
                      });
                  }
                }
                alertify.warning(message, 30);
              });
            }
          } else {
            // Parse the global clipboard
            var clipboard = JSON.parse(text);
            if (callback && clipboard && clipboard.icestudio) {
              callback(clipboard.icestudio);
            }
          }
        });
      };

      function selectionToCells(selection, graph) {
        var cells = [];
        var blocksMap = {};
        selection.each(function (block) {
          // Add block
          cells.push(block.attributes);
          // Map blocks
          blocksMap[block.id] = block;
          // Add connected wires
          var processedWires = {};
          var connectedWires = graph.getConnectedLinks(block);
          _.each(connectedWires, function (wire) {
            if (processedWires[wire.id]) {
              return;
            }

            var source = blocksMap[wire.get('source').id];
            var target = blocksMap[wire.get('target').id];

            if (source && target) {
              cells.push(wire.attributes);
              processedWires[wire.id] = true;
            }
          });
        });
        return cells;
      }

      this.cellsToProject = function (cells, opt) {
        // Convert a list of cells into the following sections of a project:
        // - design.graph
        // - dependencies

        var blocks = [];
        var wires = [];
        var p = {
          version: common.VERSION,
          design: {},
          dependencies: {},
        };

        opt = opt || {};

        for (var c = 0; c < cells.length; c++) {
          var cell = cells[c];

          if (
            cell.type === 'ice.Generic' ||
            cell.type === 'ice.Input' ||
            cell.type === 'ice.Output' ||
            cell.type === 'ice.Code' ||
            cell.type === 'ice.Info' ||
            cell.type === 'ice.Constant' ||
            cell.type === 'ice.Memory'
          ) {
            var block = {};
            block.id = cell.id;
            block.type = cell.blockType;
            block.data = cell.data;
            block.position = cell.position;
            if (
              cell.type === 'ice.Generic' ||
              cell.type === 'ice.Code' ||
              cell.type === 'ice.Info' ||
              cell.type === 'ice.Memory'
            ) {
              block.size = cell.size;
            }
            blocks.push(block);
          } else if (cell.type === 'ice.Wire') {
            var wire = {};
            wire.source = {
              block: cell.source.id,
              port: cell.source.port,
            };
            wire.target = {
              block: cell.target.id,
              port: cell.target.port,
            };
            wire.vertices = cell.vertices;
            wire.size = cell.size > 1 ? cell.size : undefined;
            wires.push(wire);
          }
        }

        p.design.board = common.selectedBoard.name;
        p.design.graph = {
          blocks: blocks,
          wires: wires,
        };

        // Update dependencies
        if (opt.deps !== false) {
          var types = this.findSubDependencies(p, common.allDependencies);
          for (var t in types) {
            p.dependencies[types[t]] = common.allDependencies[types[t]];
          }
        }

        return p;
      };

      this.findSubDependencies = function (dependency) {
        var subDependencies = [];
        if (dependency) {
          var blocks = dependency.design.graph.blocks;
          for (var i in blocks) {
            var type = blocks[i].type;
            if (type.indexOf('basic.') === -1) {
              subDependencies.push(type);
              var newSubDependencies = this.findSubDependencies(
                common.allDependencies[type]
              );
              subDependencies = subDependencies.concat(newSubDependencies);
            }
          }
          return _.unique(subDependencies);
        }
        return subDependencies;
      };

      this.hasInputRule = function (port, apply) {
        apply = apply === undefined ? true : apply;
        var _default;
        var rules = common.selectedBoard.rules;
        if (rules) {
          var allInitPorts = rules.input;
          if (allInitPorts) {
            for (var i in allInitPorts) {
              if (port === allInitPorts[i].port) {
                _default = allInitPorts[i];
                _default.apply = apply;
                break;
              }
            }
          }
        }
        return _.clone(_default);
      };

      this.hasLeftButton = function (evt) {
        return evt.which === 1;
      };

      this.hasMiddleButton = function (evt) {
        return evt.which === 2;
      };

      this.hasRightButton = function (evt) {
        return evt.which === 3;
      };

      this.hasButtonPressed = function (evt) {
        return evt.which !== 0;
      };

      this.hasShift = function (evt) {
        return evt.shiftKey;
      };

      this.hasCtrl = function (evt) {
        return evt.ctrlKey;
      };

      this.digestId = function (id) {
        if (id.indexOf('-') !== -1) {
          id = nodeSha1(id).toString();
        }
        return 'v' + id.substring(0, 6);
      };

      this.startWait = function () {
        angular.element('#menu').addClass('is-disabled');
        $('body').addClass('waiting');
      };

      this.endWait = function () {
        angular.element('#menu').removeClass('is-disabled');
        $('body').removeClass('waiting');
      };

      this.isFunction = function (functionToCheck) {
        return (
          functionToCheck &&
          {}.toString.call(functionToCheck) === '[object Function]'
        );
      };

      this.openUrlExternalBrowser = function (url) {
        gui.Shell.openExternal(url);
      };

      const DEFAULT_BOARD = 'icestick';

      this.selectBoard = _selectBoard;

      function _selectBoard(name) {
        try {
          name = name || DEFAULT_BOARD;
          common.selectedBoard = common.boards.find((x) => x.name === name);
          if (!common.selectedBoard) {
            console.error(`[srv.boards._selectBoard] board ${name} not found!`);
            return;
          }
          common.set('board', common.selectedBoard.name);
          common.selectedDevice = common.selectedBoard.info.device;
          common.pinoutInputHTML = generateHTMLOptions(
            common.selectedBoard.info['pinout'],
            'input'
          );
          common.pinoutOutputHTML = generateHTMLOptions(
            common.selectedBoard.info['pinout'],
            'output'
          );
          this.rootScopeSafeApply();
        } catch (err) {
          console.error('[srv.boards._readJSONFile]', err);
        }
      }

      function generateHTMLOptions(pinout, type) {
        var code = '<option></option>';
        for (var i in pinout) {
          if (pinout[i].type === type || pinout[i].type === 'inout') {
            code +=
              '<option value="' +
              pinout[i].value +
              '">' +
              pinout[i].name +
              '</option>';
          }
        }
        return code;
      }
    }
  );