From ba848fe84b809d75d5ee7b6e5f20b1053df19b3b Mon Sep 17 00:00:00 2001 From: Carlos Venegas Date: Tue, 22 Oct 2024 19:27:03 +0200 Subject: [PATCH] Fix lock file when try to save file that doesn't exists --- app/scripts/services/utils.js | 3134 ++++++++++++++++----------------- 1 file changed, 1567 insertions(+), 1567 deletions(-) diff --git a/app/scripts/services/utils.js b/app/scripts/services/utils.js index bf8e36db..7fc87e30 100644 --- a/app/scripts/services/utils.js +++ b/app/scripts/services/utils.js @@ -1,1608 +1,1608 @@ 'use strict'; angular.module('icestudio') - .service('utils', function ($rootScope, - gettextCatalog, - common, - blocks, - forms, - _package, - window, - nodeFs, - nodeFse, - nodePath, - nodeChildProcess, - nodeExtract, - nodeSha1, - nodeCP, - nodeGetOS, - nodeLangInfo, - SVGO, - fastCopy, - shelljs, - sparkMD5, - fsLock) { - - let _pythonExecutableCached = null; - let _pythonPipExecutableCached = null; - - // Get the system pip executable - // It is available in the common.ENV_PIP object - this.getPythonPipExecutable = function () { - - if (!_pythonExecutableCached) { - this.getPythonExecutable(); - } - if (!_pythonPipExecutableCached) { - _pythonPipExecutableCached = common.ENV_PIP; - } - - return _pythonPipExecutableCached; - }; - - //------------------------------------------------ - //-- Get the system python executable - //-- - this.getPythonExecutable = function () { - - //-- If the executable was not obtained before... - if (!_pythonExecutableCached) { - - //-- The possible executables are stored in this Array - const possibleExecutables = []; - - if (typeof common.PYTHON_ENV !== 'undefined' && - common.PYTHON_ENV.length > 0) { - - possibleExecutables.push(common.PYTHON_ENV); - - } //-- Possible python executables in Windows - else if (common.WIN32) { - possibleExecutables.push('py.exe -3'); - possibleExecutables.push('python.exe'); - - } //-- Python executables in Linux/Mac - else { - possibleExecutables.push('python3'); - possibleExecutables.push('python'); - } + .service('utils', function ($rootScope, + gettextCatalog, + common, + blocks, + forms, + _package, + window, + nodeFs, + nodeFse, + nodePath, + nodeChildProcess, + nodeExtract, + nodeSha1, + nodeCP, + nodeGetOS, + nodeLangInfo, + SVGO, + fastCopy, + shelljs, + sparkMD5, + fsLock) { + + let _pythonExecutableCached = null; + let _pythonPipExecutableCached = null; + + // Get the system pip executable + // It is available in the common.ENV_PIP object + this.getPythonPipExecutable = function () { + + if (!_pythonExecutableCached) { + this.getPythonExecutable(); + } + if (!_pythonPipExecutableCached) { + _pythonPipExecutableCached = common.ENV_PIP; + } - //-- Move through all the possible executables - //-- checking if they are executable - for (let executable of possibleExecutables) { - iceConsole.log("Trying executable: " + executable); - if (isPython3(executable)) { - _pythonExecutableCached = executable; - const pythonExecutablePath = getExecutablePath(executable); - if (pythonExecutablePath) { - iceConsole.log(`Using Python 3 executable: ${pythonExecutablePath}`); - } else { - iceConsole.log(`Using Python 3 executable: ${executable}`); + return _pythonPipExecutableCached; + }; + + //------------------------------------------------ + //-- Get the system python executable + //-- + this.getPythonExecutable = function () { + + //-- If the executable was not obtained before... + if (!_pythonExecutableCached) { + + //-- The possible executables are stored in this Array + const possibleExecutables = []; + + if (typeof common.PYTHON_ENV !== 'undefined' && + common.PYTHON_ENV.length > 0) { + + possibleExecutables.push(common.PYTHON_ENV); + + } //-- Possible python executables in Windows + else if (common.WIN32) { + possibleExecutables.push('py.exe -3'); + possibleExecutables.push('python.exe'); + + } //-- Python executables in Linux/Mac + else { + possibleExecutables.push('python3'); + possibleExecutables.push('python'); + } + + //-- Move through all the possible executables + //-- checking if they are executable + for (let executable of possibleExecutables) { + iceConsole.log("Trying executable: " + executable); + if (isPython3(executable)) { + _pythonExecutableCached = executable; + const pythonExecutablePath = getExecutablePath(executable); + if (pythonExecutablePath) { + iceConsole.log(`Using Python 3 executable: ${pythonExecutablePath}`); + } else { + iceConsole.log(`Using Python 3 executable: ${executable}`); + } + break; + } + } } - break; - } - } - } - return _pythonExecutableCached; - }; + return _pythonExecutableCached; + }; + + //--------------------------------------------------------- + //-- Check if the given file is a python3 interpreter + //-- + function isPython3(executable) { - //--------------------------------------------------------- - //-- Check if the given file is a python3 interpreter - //-- - function isPython3(executable) { + //-- Add the '-V' flag for reading the python version + executable += ' -V'; - //-- Add the '-V' flag for reading the python version - executable += ' -V'; + try { - try { + //-- Run the executable + const result = nodeChildProcess.execSync(executable); - //-- Run the executable - const result = nodeChildProcess.execSync(executable); + //-- Check the output. Return true if it is python3 + if (result !== false && result !== null) { - //-- Check the output. Return true if it is python3 - if (result !== false && result !== null) { + const pythonVersion = /Python 3\.(\d+)\.(\d+)/g.exec(result.toString()); + return (pythonVersion !== null && pythonVersion.length === 3 && + parseInt(pythonVersion[1]) >= 7); + } - const pythonVersion = /Python 3\.(\d+)\.(\d+)/g.exec(result.toString()); - return (pythonVersion !== null && pythonVersion.length === 3 && - parseInt(pythonVersion[1]) >= 7); + } catch (e) { + console.error(e); + } + return false; } - } catch (e) { - console.error(e); - } - return false; - } - - //--------------------------------------------------------- - //-- Get the path of an executable using the `which` shelljs module - //-- - //-- INPUTS: - //-- -executable: string for an executable - //-- OUTPUTS: - //-- string: the path to the executable - //-- null: executable not found - function getExecutablePath(executable) { - // split executable at first space to account for e.g. `python.exe -3` - executable = executable.split(" ")[0]; - return shelljs.which(executable); - } - - - this.extractZip = function (source, destination, callback) { - nodeExtract(source, { - dir: destination - }, function (error) { - if (error) { - callback(true); - } else { - callback(); + //--------------------------------------------------------- + //-- Get the path of an executable using the `which` shelljs module + //-- + //-- INPUTS: + //-- -executable: string for an executable + //-- OUTPUTS: + //-- string: the path to the executable + //-- null: executable not found + function getExecutablePath(executable) { + // split executable at first space to account for e.g. `python.exe -3` + executable = executable.split(" ")[0]; + return shelljs.which(executable); } - }); - }; - - 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); - }; - - //-------------------------------------------------------------- - //-- Execute the given system command - //-- INPUTS: - //-- -command: array of string containing the commands to - //-- execute along with the arguments - //-- -callback: Function called when the command executed is done - //-- -notifyerror: Show a GUI notification if there is an error - //-- -callbackAsync: Automatic callback if async.serial calls it - //------------------------------------------------------------------- - this.executeCommand = function (command, - callback, - notifyerror = true, - callbackAsync = undefined) { - - //-- Construct a string with the full command - let cmd = command.join(' '); - let _this = this; - - //-- Show the command in the DEBUG log - iceConsole.log(`>>>> utils.executeCommand => ${cmd}\n`); - - //-- Array for storing the arguments - let args = []; - - //-- Get the arguments, if any - if (command.length > 0) { - args = command.slice(1); - } - - //-- Execute the command in background!! - let proccess = nodeChildProcess.spawn(command[0], args, { shell: true }); - - //-- String with the latest output to pass to the callback function - let output = ""; - - //-- When there are outputs available from the command... - proccess.stdout.on('data', function (data) { - - //-- Show the output in the log - iceConsole.log(`>>(OUTPUT): ${data}\n`); - - common.commandOutput = command.join(' ') + '\n\n' + data; - $(document).trigger('commandOutputChanged', [common.commandOutput]); - - //-- Store the output string in the output variable - //-- to pass to the callback function - output = data; - }); - - //-- If there are errors ... - proccess.stderr.on('data', function (data) { - - //-- Show them in the log file - iceConsole.log(`>>(ERROR): ${data}\n`); - - common.commandOutput = command.join(' ') + '\n\n' + data; - $(document).trigger('commandOutputChanged', [common.commandOutput]); - }); - - proccess.on('exit', function (code) { - - - if (code !== 0) { - _this.enableKeyEvents(); - _this.enableClickEvents(); - - iceConsole.log("----!!!! ERROR !!!! -----"); - iceConsole.log("CMD: " + command); - - //-- Error executing the command - //-- Show the error notification - if (notifyerror) { - alertify.error('Error executing command ' + command, 30); - } - //-- Comand finished with errors. Call the callback function - if (typeof callback !== 'undefined' && callback !== null) { - callback(true, output); - } - if (typeof callbackAsync !== 'undefined') { - callbackAsync(); - } - } else { - //-- Command finished with NO errors. Call the callback function - if (typeof callback !== 'undefined' && callback !== null) { - callback(false, output); - } - if (typeof callbackAsync !== 'undefined') { - callbackAsync(); - } - } - }); - - }; - - //------------------------------------------ - //-- Return standard (English) string for Apio version - // - this.printApioVersion = function (version) { - let msg = ""; - - switch (version) { - case common.APIO_VERSION_LATEST_STABLE: - msg = "Apio LATEST STABLE version"; - break; - - case common.APIO_VERSION_STABLE: - msg = "Apio STABLE version"; - break; - - case common.APIO_VERSION_DEV: - msg = "Apio DEVELOPMENT VERSION"; - break; - - default: - msg = "UNKNOWN Apio Version (ERROR)"; - break; - } - - return msg; - }; - - //------------------------------------------ - //-- Create the python virtual environment - //-- with the command python -m venv venv - //------------------------------------------ - this.createVirtualenv = function (callback) { - - //-- Check if the .icestudio folder exist - if (!nodeFs.existsSync(common.ICESTUDIO_DIR)) { - - //-- Create the .icestudio folder - nodeFs.mkdirSync(common.ICESTUDIO_DIR); - } - - //-- Check if the venv folder exist - if (!nodeFs.existsSync(common.ENV_DIR)) { - - //-- python -m venv venv - var command = [this.getPythonExecutable(), '-m venv', coverPath(common.ENV_DIR)]; - //-- Check if extra parameter is needed for windows... - if (common.WIN32) { - //command.push('--always-copy'); - } - this.executeCommand(command, null, true, callback); - - } else { - //-- The virtual environment already existed - callback(); - } - }; - - - //----------------------------------------------------------------- - //-- Check if there is internet connection - //-- - this.isOnline = function (callback, error) { - - if (navigator.onLine) { - - callback(); - } else { - error(); - callback(true); - } - - }; - - //----------------------------------------------------- - //-- Repair OS permissions. - //-- - // - this.repairPermissions = function (callback) { - - if (iceStudio.env.DARWIN === true) { - this.executeCommand(['osascript -e \'do shell script "cd ~/.icestudio;sudo find . -exec xattr -d com.apple.quarantine {} \\\\;" with administrator privileges\''], null, true, callback); - } else { - //-- Comand finished with errors. Call the callback function - if (typeof callback !== 'undefined' && callback !== null) { - callback(); - } - } - }; - - - //----------------------------------------------------- - //-- Install the Apio toolchain. The version to install is taken - //-- from the common.APIO_VERSION object - //-- - //-- Installing the apio stable: - //-- Ej. pip install -U apio[extra packages]==0.6.0 - // - //-- Installing the apio latest stable: - //-- Ej. pip install -U apio[extra packages] - // - //-- Installing the apio dev: - //-- Ej. pip install -U git+https://github.com/FPGAwars/apio.git@develop#egg=apio - // - this.installOnlineApio = function (callback) { - - console.log("InstallOnlineApio: " + this.printApioVersion(common.APIO_VERSION)); - - //-- Get the pip executable - let pipExec = this.getPythonPipExecutable(); - //-- Place the executable between quotes ("") just in case there - //-- is a path with spaces in their names - const executable = coverPath(pipExec); - - //-- Get the pip parameters needed for installing apio - //-- The needed apio version is also added - const params = this.getApioParameters(); - console.log(pipExec, executable, params); - //-- Run the pip command! - this.executeCommand([executable, params], null, true, callback); - console.log('Finished InstallOnlineApio'); - - }; - - - //------------------------------------------------------------------------ - //-- Return the parameters needed for pip for installing - //-- the apio toolchains. The version to install is read - //-- from the common.APIO_VERSION global object - //-- - this.getApioParameters = function () { - - //-- Get the extra python packages to install - let extraPackages = _package.apio.extras || []; - let extraPackagesString = ""; - - //-- Get the pip string with the version - //-- Stable: "==0.6.0" - //-- Latest stable and dev: "" - let versionString = ""; - - if (common.APIO_VERSION === common.APIO_VERSION_STABLE) { - - //-- The stable version to installed is read from the - // icestudio app/package.json: - //-- apio.min object! - versionString = "==" + _package.apio.min; - - //-- Get the extraPackages. Only used when installing the stable - //-- version - //-- Bug in Windows: If the extra packages are used during the apio - //-- upgrading (installing the latest stable), apio is not upgraded - extraPackagesString = "[" + extraPackages.toString() + "]"; - } - - //-- Get the apio package name: - //-- Stable and latest stable: "apio" - //-- dev: "git+https://github.com/FPGAwars/apio.git@develop#egg=apio" - let apio = (common.APIO_VERSION === common.APIO_VERSION_DEV) ? - common.APIO_PIP_VCS : - "apio"; - - //-- Get the pip params for installing apio - const params = "install -U " + apio + extraPackagesString + - versionString; - - console.log("--> DEBUG: Params passed to pip: " + params); - - return params; - }; - - //------------------------------------------------------------------ - //-- Install an Apio package - //-- apio install - //-- - this.apioInstall = function (pkg, callback) { - - //-- common.APIO_CMD contains the command for executing APIO - this.executeCommand([common.APIO_CMD, 'install', pkg], null, true, callback); - }; - - //-- The toolchains are NOT disabled by default - this.toolchainDisabled = false; - - //------------------------------------------------------------------------ - //-- Get the command that should be used for executing the apio toolchain - //-- This command includes the full path to apio executable, as well as - //-- the setting of the APIO_HOME_DIR environment variable - this.getApioExecutable = function () { - - //-- Check if the ICESTUDIO_APIO env variable is set with the apio - //-- toolchain to use or if it has been set on the package.json file - let candidateApio = process.env.ICESTUDIO_APIO ? - process.env.ICESTUDIO_APIO : - _package.apio.external; - - //-- There is an alternative apio toolchain ready - if (nodeFs.existsSync(candidateApio)) { - - if (!this.toolchainDisabled) { - // Show message only on start - alertify.message(gettextCatalog.getString('Using external Apio: {{name}}', { - name: candidateApio - }), 5); - } - this.toolchainDisabled = true; - return coverPath(candidateApio); - } - - //-- There are no external apio toolchain. Use the one installed - //-- by icestudio - this.toolchainDisabled = false; - - //-- The apio command to execute is located in the - //-- common.APIO_CMD global object - return common.APIO_CMD; - }; - - - //----------------------------------------------------------------------- - //-- Remove the toolchains and related folders - //-- - this.removeToolchain = function () { - - //-- Remove the Virtual environment - this.deleteFolderRecursive(common.ENV_DIR); - - //-- Remove APIO - this.deleteFolderRecursive(common.APIO_HOME_DIR); - - //-- Remove the cache dir (temporal) - this.deleteFolderRecursive(common.CACHE_DIR); - }; - - this.removeCollections = function () { - this.deleteFolderRecursive(common.INTERNAL_COLLECTIONS_DIR); - }; - - this.deleteFolderRecursive = function (path) { - if (nodeFs.existsSync(path)) { - nodeFs.readdirSync(path).forEach(function (file /*, index*/) { - var curPath = nodePath.join(path, file); - if (nodeFs.lstatSync(curPath).isDirectory()) { // recursive - this.deleteFolderRecursive(curPath); - } else { // delete file - nodeFs.unlinkSync(curPath); - } - }.bind(this)); - nodeFs.rmdirSync(path); - } - }; - - this.sep = nodePath.sep; - - this.basename = basename; - - function basename(filepath) { - let b = nodePath.basename(filepath); - return b.substr(0, b.lastIndexOf('.')); - } - - this.dirname = function (filepath) { - return nodePath.dirname(filepath); - }; - - this.filepath2buildpath = function (filepath) { - - let b = nodePath.basename(filepath); - let localdir = filepath.substr(0, filepath.lastIndexOf(b)); - let dirname = b.substr(0, b.lastIndexOf('.')); - let path = nodePath.join(localdir, 'ice-build'); - // If we want to remove spaces - // return nodePath.join(path,dirname).replace(/ /g, '_'); - return nodePath.join(path, dirname); - }; - - - //---------------------------------------------------- - //-- Read the profile file - //-- - this.readFile = function (filepath) { - - return new Promise(function (resolve, reject) { - if (nodeFs.existsSync(common.PROFILE_PATH)) { - nodeFs.readFile(filepath, "utf8", - function (err, content) { - if (err) { - reject(err.toString()); - } else { - - var data = false; - data = isJSON(content); - - if (data) { - resolve(data); + this.extractZip = function (source, destination, callback) { + nodeExtract(source, { + dir: destination + }, function (error) { + if (error) { + callback(true); } else { - reject(); + callback(); } - } }); - } else { - resolve({}); - } - }); - }; - - this.saveFile = async function (filepath, data) { - return new Promise(async function (resolve, reject) { - try { - // Verify if file exists - if (!fs.existsSync(filepath)) { - // If not exists we need to create empty file to block it until write it - fs.writeFileSync(filepath, ''); - } - - // Try to get the file ownership - const release = await fsLock.lock(filepath, { retries: 10 }); // Retry 10 times if is locked - - var content = data; - if (typeof data !== 'string') { - content = JSON.stringify(data, null, 2); - } - - fs.writeFile(filepath, content, async function (err) { - if (err) { - await release(); - reject(err.toString()); - } else { - await release(); - resolve(); + }; + + function disableEvent(event) { + event.stopPropagation(); + event.preventDefault(); } - }); - - } catch (error) { - reject('Error while locking the file: ' + error.toString()); - } - }); -}; - - function isJSON(content) { - try { - return JSON.parse(content); - } catch (e) { - return false; - } - } - - this.setLocale = function (locale, callback) { - // Update current locale format - locale = splitLocale(locale); - // Load supported languages - var supported = getSupportedLanguages(); - // Set the best matching language - var bestLang = bestLocale(locale, supported); - gettextCatalog.setCurrentLanguage(bestLang); - // Application strings - gettextCatalog.loadRemote(nodePath.join(common.LOCALE_DIR, bestLang, bestLang + '.json')); - // Collections strings - 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); + + 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); + }; + + //-------------------------------------------------------------- + //-- Execute the given system command + //-- INPUTS: + //-- -command: array of string containing the commands to + //-- execute along with the arguments + //-- -callback: Function called when the command executed is done + //-- -notifyerror: Show a GUI notification if there is an error + //-- -callbackAsync: Automatic callback if async.serial calls it + //------------------------------------------------------------------- + this.executeCommand = function (command, + callback, + notifyerror = true, + callbackAsync = undefined) { + + //-- Construct a string with the full command + let cmd = command.join(' '); + let _this = this; + + //-- Show the command in the DEBUG log + iceConsole.log(`>>>> utils.executeCommand => ${cmd}\n`); + + //-- Array for storing the arguments + let args = []; + + //-- Get the arguments, if any + if (command.length > 0) { + args = command.slice(1); + } + + //-- Execute the command in background!! + let proccess = nodeChildProcess.spawn(command[0], args, { shell: true }); + + //-- String with the latest output to pass to the callback function + let output = ""; + + //-- When there are outputs available from the command... + proccess.stdout.on('data', function (data) { + + //-- Show the output in the log + iceConsole.log(`>>(OUTPUT): ${data}\n`); + + common.commandOutput = command.join(' ') + '\n\n' + data; + $(document).trigger('commandOutputChanged', [common.commandOutput]); + + //-- Store the output string in the output variable + //-- to pass to the callback function + output = data; + }); + + //-- If there are errors ... + proccess.stderr.on('data', function (data) { + + //-- Show them in the log file + iceConsole.log(`>>(ERROR): ${data}\n`); + + common.commandOutput = command.join(' ') + '\n\n' + data; + $(document).trigger('commandOutputChanged', [common.commandOutput]); + }); + + proccess.on('exit', function (code) { + + + if (code !== 0) { + _this.enableKeyEvents(); + _this.enableClickEvents(); + + iceConsole.log("----!!!! ERROR !!!! -----"); + iceConsole.log("CMD: " + command); + + //-- Error executing the command + //-- Show the error notification + if (notifyerror) { + alertify.error('Error executing command ' + command, 30); + } + + //-- Comand finished with errors. Call the callback function + if (typeof callback !== 'undefined' && callback !== null) { + callback(true, output); + } + if (typeof callbackAsync !== 'undefined') { + callbackAsync(); + } + } else { + //-- Command finished with NO errors. Call the callback function + if (typeof callback !== 'undefined' && callback !== null) { + callback(false, output); + } + if (typeof callbackAsync !== 'undefined') { + callbackAsync(); + } + + } + }); + + }; + + //------------------------------------------ + //-- Return standard (English) string for Apio version + // + this.printApioVersion = function (version) { + let msg = ""; + + switch (version) { + case common.APIO_VERSION_LATEST_STABLE: + msg = "Apio LATEST STABLE version"; + break; + + case common.APIO_VERSION_STABLE: + msg = "Apio STABLE version"; + break; + + case common.APIO_VERSION_DEV: + msg = "Apio DEVELOPMENT VERSION"; + break; + + default: + msg = "UNKNOWN Apio Version (ERROR)"; + break; + } + + return msg; + }; + + //------------------------------------------ + //-- Create the python virtual environment + //-- with the command python -m venv venv + //------------------------------------------ + this.createVirtualenv = function (callback) { + + //-- Check if the .icestudio folder exist + if (!nodeFs.existsSync(common.ICESTUDIO_DIR)) { + + //-- Create the .icestudio folder + nodeFs.mkdirSync(common.ICESTUDIO_DIR); + } + + //-- Check if the venv folder exist + if (!nodeFs.existsSync(common.ENV_DIR)) { + + //-- python -m venv venv + var command = [this.getPythonExecutable(), '-m venv', coverPath(common.ENV_DIR)]; + //-- Check if extra parameter is needed for windows... + if (common.WIN32) { + //command.push('--always-copy'); + } + this.executeCommand(command, null, true, callback); + + } else { + //-- The virtual environment already existed + callback(); + } + }; + + + //----------------------------------------------------------------- + //-- Check if there is internet connection + //-- + this.isOnline = function (callback, error) { + + if (navigator.onLine) { + + callback(); + } else { + error(); + callback(true); + } + + }; + + //----------------------------------------------------- + //-- Repair OS permissions. + //-- + // + this.repairPermissions = function (callback) { + + if (iceStudio.env.DARWIN === true) { + this.executeCommand(['osascript -e \'do shell script "cd ~/.icestudio;sudo find . -exec xattr -d com.apple.quarantine {} \\\\;" with administrator privileges\''], null, true, callback); + } else { + //-- Comand finished with errors. Call the callback function + if (typeof callback !== 'undefined' && callback !== null) { + callback(); + } + } + }; + + + //----------------------------------------------------- + //-- Install the Apio toolchain. The version to install is taken + //-- from the common.APIO_VERSION object + //-- + //-- Installing the apio stable: + //-- Ej. pip install -U apio[extra packages]==0.6.0 + // + //-- Installing the apio latest stable: + //-- Ej. pip install -U apio[extra packages] + // + //-- Installing the apio dev: + //-- Ej. pip install -U git+https://github.com/FPGAwars/apio.git@develop#egg=apio + // + this.installOnlineApio = function (callback) { + + console.log("InstallOnlineApio: " + this.printApioVersion(common.APIO_VERSION)); + + //-- Get the pip executable + let pipExec = this.getPythonPipExecutable(); + //-- Place the executable between quotes ("") just in case there + //-- is a path with spaces in their names + const executable = coverPath(pipExec); + + //-- Get the pip parameters needed for installing apio + //-- The needed apio version is also added + const params = this.getApioParameters(); + console.log(pipExec, executable, params); + //-- Run the pip command! + this.executeCommand([executable, params], null, true, callback); + console.log('Finished InstallOnlineApio'); + + }; + + + //------------------------------------------------------------------------ + //-- Return the parameters needed for pip for installing + //-- the apio toolchains. The version to install is read + //-- from the common.APIO_VERSION global object + //-- + this.getApioParameters = function () { + + //-- Get the extra python packages to install + let extraPackages = _package.apio.extras || []; + let extraPackagesString = ""; + + //-- Get the pip string with the version + //-- Stable: "==0.6.0" + //-- Latest stable and dev: "" + let versionString = ""; + + if (common.APIO_VERSION === common.APIO_VERSION_STABLE) { + + //-- The stable version to installed is read from the + // icestudio app/package.json: + //-- apio.min object! + versionString = "==" + _package.apio.min; + + //-- Get the extraPackages. Only used when installing the stable + //-- version + //-- Bug in Windows: If the extra packages are used during the apio + //-- upgrading (installing the latest stable), apio is not upgraded + extraPackagesString = "[" + extraPackages.toString() + "]"; + } + + //-- Get the apio package name: + //-- Stable and latest stable: "apio" + //-- dev: "git+https://github.com/FPGAwars/apio.git@develop#egg=apio" + let apio = (common.APIO_VERSION === common.APIO_VERSION_DEV) ? + common.APIO_PIP_VCS : + "apio"; + + //-- Get the pip params for installing apio + const params = "install -U " + apio + extraPackagesString + + versionString; + + console.log("--> DEBUG: Params passed to pip: " + params); + + return params; + }; + + //------------------------------------------------------------------ + //-- Install an Apio package + //-- apio install + //-- + this.apioInstall = function (pkg, callback) { + + //-- common.APIO_CMD contains the command for executing APIO + this.executeCommand([common.APIO_CMD, 'install', pkg], null, true, callback); + }; + + //-- The toolchains are NOT disabled by default + this.toolchainDisabled = false; + + //------------------------------------------------------------------------ + //-- Get the command that should be used for executing the apio toolchain + //-- This command includes the full path to apio executable, as well as + //-- the setting of the APIO_HOME_DIR environment variable + this.getApioExecutable = function () { + + //-- Check if the ICESTUDIO_APIO env variable is set with the apio + //-- toolchain to use or if it has been set on the package.json file + let candidateApio = process.env.ICESTUDIO_APIO ? + process.env.ICESTUDIO_APIO : + _package.apio.external; + + //-- There is an alternative apio toolchain ready + if (nodeFs.existsSync(candidateApio)) { + + if (!this.toolchainDisabled) { + // Show message only on start + alertify.message(gettextCatalog.getString('Using external Apio: {{name}}', { + name: candidateApio + }), 5); + } + this.toolchainDisabled = true; + return coverPath(candidateApio); + } + + //-- There are no external apio toolchain. Use the one installed + //-- by icestudio + this.toolchainDisabled = false; + + //-- The apio command to execute is located in the + //-- common.APIO_CMD global object + return common.APIO_CMD; + }; + + + //----------------------------------------------------------------------- + //-- Remove the toolchains and related folders + //-- + this.removeToolchain = function () { + + //-- Remove the Virtual environment + this.deleteFolderRecursive(common.ENV_DIR); + + //-- Remove APIO + this.deleteFolderRecursive(common.APIO_HOME_DIR); + + //-- Remove the cache dir (temporal) + this.deleteFolderRecursive(common.CACHE_DIR); + }; + + this.removeCollections = function () { + this.deleteFolderRecursive(common.INTERNAL_COLLECTIONS_DIR); + }; + + this.deleteFolderRecursive = function (path) { + if (nodeFs.existsSync(path)) { + nodeFs.readdirSync(path).forEach(function (file /*, index*/) { + var curPath = nodePath.join(path, file); + if (nodeFs.lstatSync(curPath).isDirectory()) { // recursive + this.deleteFolderRecursive(curPath); + } else { // delete file + nodeFs.unlinkSync(curPath); + } + }.bind(this)); + nodeFs.rmdirSync(path); + } + }; + + this.sep = nodePath.sep; + + this.basename = basename; + + function basename(filepath) { + let b = nodePath.basename(filepath); + return b.substr(0, b.lastIndexOf('.')); } - } - if (callback) { - setTimeout(function () { - callback(); - }, 50); - } - // Return the best language - 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(function (element /*, index*/) { - var curPath = nodePath.join(common.LOCALE_DIR, element); - if (nodeFs.lstatSync(curPath).isDirectory()) { - supported.push(splitLocale(element)); + + this.dirname = function (filepath) { + return nodePath.dirname(filepath); + }; + + this.filepath2buildpath = function (filepath) { + + let b = nodePath.basename(filepath); + let localdir = filepath.substr(0, filepath.lastIndexOf(b)); + let dirname = b.substr(0, b.lastIndexOf('.')); + let path = nodePath.join(localdir, 'ice-build'); + // If we want to remove spaces + // return nodePath.join(path,dirname).replace(/ /g, '_'); + return nodePath.join(path, dirname); + }; + + + //---------------------------------------------------- + //-- Read the profile file + //-- + this.readFile = function (filepath) { + + return new Promise(function (resolve, reject) { + if (nodeFs.existsSync(common.PROFILE_PATH)) { + nodeFs.readFile(filepath, "utf8", + function (err, content) { + if (err) { + reject(err.toString()); + } else { + + var data = false; + data = isJSON(content); + + if (data) { + resolve(data); + } else { + reject(); + } + } + }); + } else { + resolve({}); + } + }); + }; + + this.saveFile = async function (filepath, data) { + return new Promise(async function (resolve, reject) { + try { + // Verify if file exists + if (!nodeFs.existsSync(filepath)) { + // If not exists we need to create empty file to block it until write it + nodeFs.writeFileSync(filepath, ''); + } + + // Try to get the file ownership + const release = await fsLock.lock(filepath, { retries: 10 }); // Retry 10 times if is locked + + var content = data; + if (typeof data !== 'string') { + content = JSON.stringify(data, null, 2); + } + + nodeFs.writeFile(filepath, content, async function (err) { + if (err) { + await release(); + reject(err.toString()); + } else { + await release(); + resolve(); + } + }); + + } catch (error) { + reject('Error while locking the file: ' + error.toString()); + } + }); + }; + + function isJSON(content) { + try { + return JSON.parse(content); + } catch (e) { + return false; + } } - }); - 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; - } + + this.setLocale = function (locale, callback) { + // Update current locale format + locale = splitLocale(locale); + // Load supported languages + var supported = getSupportedLanguages(); + // Set the best matching language + var bestLang = bestLocale(locale, supported); + gettextCatalog.setCurrentLanguage(bestLang); + // Application strings + gettextCatalog.loadRemote(nodePath.join(common.LOCALE_DIR, bestLang, bestLang + '.json')); + // Collections strings + 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); + } + } + if (callback) { + setTimeout(function () { + callback(); + }, 50); + } + // Return the best language + 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; } - } - // 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 : ''); + + function getSupportedLanguages() { + var supported = []; + nodeFs.readdirSync(common.LOCALE_DIR).forEach(function (element /*, index*/) { + var curPath = nodePath.join(common.LOCALE_DIR, element); + if (nodeFs.lstatSync(curPath).isDirectory()) { + supported.push(splitLocale(element)); + } + }); + return supported; } - } - // 3. Return default lang - return 'en'; - } - - - this.projectinfoprompt = function (values, callback) { - var i; - var content = []; - var messages = [ - gettextCatalog.getString('Name'), - gettextCatalog.getString('Version'), - gettextCatalog.getString('Description'), - gettextCatalog.getString('Author') - ]; - var n = messages.length; - var image = values[4]; - var blankImage = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; - content.push('
'); - for (i in messages) { - if (i > 0) { - //content.push('
'); + + 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'; } - content.push('

' + messages[i] + '

'); - content.push(' '); - } - content.push('

' + gettextCatalog.getString('Image') + '

'); - content.push(' '); - content.push(' '); - content.push('
'); - if (image) { - let embeded = '
'; - /* if (image.startsWith('%3Csvg')) { + + + this.projectinfoprompt = function (values, callback) { + var i; + var content = []; + var messages = [ + gettextCatalog.getString('Name'), + gettextCatalog.getString('Version'), + gettextCatalog.getString('Description'), + gettextCatalog.getString('Author') + ]; + var n = messages.length; + var image = values[4]; + var blankImage = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; + content.push('
'); + for (i in messages) { + if (i > 0) { + //content.push('
'); + } + content.push('

' + messages[i] + '

'); + content.push(' '); + } + content.push('

' + gettextCatalog.getString('Image') + '

'); + content.push(' '); + content.push(' '); + content.push('
'); + if (image) { + let embeded = '
'; + /* if (image.startsWith('%3Csvg')) { embeded += decodeURI(image); } else if (image.startsWith(' 0) { - hash = sparkMD5.hash(tmpImage); - tmpImageSrc = virtualBlock.svgFile(hash, tmpImage); - embeded = `${embeded}`; - } + let tmpImage = ''; + let tmpImageSrc = ''; + let hash = ''; + if (image.startsWith('%3Csvg')) { + tmpImage = decodeURI(image); + } + else if (image.startsWith(' 0) { + hash = sparkMD5.hash(tmpImage); + tmpImageSrc = virtualBlock.svgFile(hash, tmpImage); + embeded = `${embeded}`; + } - embeded += ''; + embeded += ''; - content.push(embeded); + content.push(embeded); - } else { + } else { - content.push('
'); - } - content.push('
'); - content.push('
'); - content.push(' '); - content.push(' '); - content.push(' '); - content.push('
'); - content.push('
'); - // Restore values - for (i = 0; i < n; i++) { - $('#input' + i).val(values[i]); - } + content.push('
'); + } + content.push('
'); + content.push('
'); + content.push(' '); + content.push(' '); + content.push(' '); + content.push('
'); + content.push('
'); + // Restore values + for (i = 0; i < n; i++) { + $('#input' + i).val(values[i]); + } - var prevOnshow = alertify.confirm().get('onshow') || function () { }; + var prevOnshow = alertify.confirm().get('onshow') || function () { }; + + alertify.confirm() + .set('onshow', function () { + prevOnshow(); + registerOpen(); + registerSave(); + registerReset(); + }); + + function registerOpen() { + // Open SVG + var chooserOpen = $('#input-open-svg'); + chooserOpen.unbind('change'); + chooserOpen.change(function ( /*evt*/) { + var filepath = $(this).val(); + + nodeFs.readFile(filepath, 'utf8', function (err, data) { + if (err) { + throw err; + } + optimizeSVG(data, function (result) { + image = encodeURI(result.data); + registerSave(); + + $('#preview-svg-wrapper').html(result.data); + }); + }); + $(this).val(''); + }); + } - alertify.confirm() - .set('onshow', function () { - prevOnshow(); - registerOpen(); - registerSave(); - registerReset(); - }); + function optimizeSVG(data, callback) { + SVGO.optimize(data, callback); + } + + function registerSave() { + // Save SVG + var label = $('#save-svg'); + if (image) { + label.removeClass('disabled'); + label.attr('for', 'input-save-svg'); + var chooserSave = $('#input-save-svg'); + chooserSave.unbind('change'); + chooserSave.change(function ( /*evt*/) { + if (image) { + var filepath = $(this).val(); + if (!filepath.endsWith('.svg')) { + filepath += '.svg'; + } + nodeFs.writeFile(filepath, decodeURI(image), function (err) { + if (err) { + throw err; + } + }); + $(this).val(''); + } + }); + } else { + label.addClass('disabled'); + label.attr('for', ''); + } + } + + function registerReset() { + // Reset SVG + var reset = $('#reset-svg'); + reset.click(function ( /*evt*/) { + image = ''; + registerSave(); + $('#preview-svg-wrapper').empty(); + }); + } + + alertify.confirm(content.join('\n')) + .set('onok', function (evt) { + var values = []; + for (var i = 0; i < n; i++) { + values.push($('#input' + i).val()); + } + values.push(image); + if (callback) { + callback(evt, values); + } + // Restore onshow + alertify.confirm().set('onshow', prevOnshow); + }) + .set('oncancel', function ( /*evt*/) { + // Restore onshow + alertify.confirm().set('onshow', prevOnshow); + }); + }; + + this.selectBoardPrompt = function (callback) { + + // Disable user events + this.disableKeyEvents(); + + // Hide Cancel button + $('.ajs-cancel').addClass('hidden'); + + //-- Create the form + let form = new forms.FormSelectBoard(); - function registerOpen() { - // Open SVG - var chooserOpen = $('#input-open-svg'); - chooserOpen.unbind('change'); - chooserOpen.change(function ( /*evt*/) { - var filepath = $(this).val(); + //-- Display the form + form.display((evt) => { - nodeFs.readFile(filepath, 'utf8', function (err, data) { - if (err) { - throw err; + //-- Process the information in the form + form.process(evt); + + //-- Read the selected board + let selectedBoard = form.values[0]; + + if (selectedBoard) { + + evt.cancel = false; + + //-- Execute the callback + if (callback) { + callback(selectedBoard); + } + + // Enable user events + this.enableKeyEvents(); + } + }); + + }; + + this.copySync = function (orig, dest) { + var ret = true; + try { + if (nodeFs.existsSync(orig)) { + nodeFse.copySync(orig, dest); + } else { + // Error: file does not exist + ret = false; + } + } catch (e) { + alertify.error(gettextCatalog.getString('Error: {{error}}', { + error: e.toString() + }), 30); + ret = false; } - optimizeSVG(data, function (result) { - image = encodeURI(result.data); - registerSave(); + return ret; + }; + + 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; + }; - $('#preview-svg-wrapper').html(result.data); + //----------------------------------------------------------------------- + //-- Return a text in bold HTML + //-- Input: + //-- * text: String to convert to Bold + //-- Returns: + //-- * The HTML text in bold + //----------------------------------------------------------------------- + this.bold = function (text) { + return `${text}`; + }; + + //----------------------------------------------------------------------- + //-- Open the Dialog for choosing a file + //-- + //-- INPUTS: + //-- * inputID (String): Html selector of the file chooser input + //-- + //-- * callback(filepath): It is called when the user has pressed the + //-- ok button. The chosen file is passed as a parameter + //----------------------------------------------------------------------- + this.openDialog = function (inputID, callback) { + + //-- Get the file chooser element (from the DOM) + let chooser = $(inputID); + + //-- Remove any previously event attached + chooser.unbind('change'); + + //-- Attach a new callback function + chooser.change(function () { + + //-- It is executed when the user has selected the file + //-- Read the filepath entered by the user + let filepath = $(this).val(); + + //-- Execute the callback (if it was given) + if (callback) { + callback(filepath); + } + + //-- Remove the current select filename from the chooser + $(this).val(''); }); - }); - $(this).val(''); - }); - } - - function optimizeSVG(data, callback) { - SVGO.optimize(data, callback); - } - - function registerSave() { - // Save SVG - var label = $('#save-svg'); - if (image) { - label.removeClass('disabled'); - label.attr('for', 'input-save-svg'); - var chooserSave = $('#input-save-svg'); - chooserSave.unbind('change'); - chooserSave.change(function ( /*evt*/) { - if (image) { - var filepath = $(this).val(); - if (!filepath.endsWith('.svg')) { - filepath += '.svg'; - } - nodeFs.writeFile(filepath, decodeURI(image), function (err) { - if (err) { - throw err; + + //-- Activate the File chooser! (The element is shown, it waits for + //-- the user to enter the file, and the callback is executed) + chooser.trigger('click'); + }; + + + + this.saveDialog = function (inputID, ext, callback) { + var chooser = $(inputID); + chooser.unbind('change'); + chooser.change(function ( /*evt*/) { + var filepath = $(this).val(); + if (!filepath.endsWith(ext)) { + filepath += ext; + } + if (callback) { + callback(filepath); } - }); - $(this).val(''); + $(this).val(''); + }); + chooser.trigger('click'); + }; + + this.updateWindowTitle = function (title) { + window.get().title = title; + }; + + this.rootScopeSafeApply = function () { + if (!$rootScope.$$phase) { + $rootScope.$apply(); } - }); - } else { - label.addClass('disabled'); - label.attr('for', ''); - } - } - - function registerReset() { - // Reset SVG - var reset = $('#reset-svg'); - reset.click(function ( /*evt*/) { - image = ''; - registerSave(); - $('#preview-svg-wrapper').empty(); - }); - } + }; - alertify.confirm(content.join('\n')) - .set('onok', function (evt) { - var values = []; - for (var i = 0; i < n; i++) { - values.push($('#input' + i).val()); - } - values.push(image); - if (callback) { - callback(evt, values); - } - // Restore onshow - alertify.confirm().set('onshow', prevOnshow); - }) - .set('oncancel', function ( /*evt*/) { - // Restore onshow - alertify.confirm().set('onshow', prevOnshow); - }); - }; + 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(gettextCatalog.getString('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.selectBoardPrompt = function (callback) { + 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; + }; - // Disable user events - this.disableKeyEvents(); + //----------------------------------------------------------------------- + //-- clone. Return a deep copy of the given input object data + //-- * data: Input object to copy + //-- * Returns: A copy of the input object + //----------------------------------------------------------------------- + this.clone = function (data) { + + //-- Implementation using the fast-copy npm package: + //-- More info: https://www.npmjs.com/package/fast-copy + return fastCopy(data); + + //-- Alternative implementation: + // 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)); + }; - // Hide Cancel button - $('.ajs-cancel').addClass('hidden'); + this.dependencyID = function (dependency) { + if (dependency.package && dependency.design) { + return nodeSha1(JSON.stringify(dependency.package) + + JSON.stringify(dependency.design)); + } + }; - //-- Create the form - let form = new forms.FormSelectBoard(); + //----------------------------------------------------------------------- + //-- Create a new ICESTUDIO window + //-- + //-- INPUTS: + //-- * filepath: (optional) Icestudio file to open in the new window + //----------------------------------------------------------------------- + this.newWindow = function (filepath) { - //-- Display the form - form.display((evt) => { + console.log("(DEBUG): Executing app/scripts/services/utils.js --> newWindow()"); - //-- Process the information in the form - form.process(evt); + //-- If there are parameters to pass or not + //-- No parameters by default + let hasParams = false; - //-- Read the selected board - let selectedBoard = form.values[0]; + //-- URL with no parameters + let url = 'index.html'; - if (selectedBoard) { + //-- Create the arguments + //-- The filepath was given: pass it as an argument + if (filepath) { - evt.cancel = false; + //-- There are params in the URL + hasParams = true; - //-- Execute the callback - if (callback) { - callback(selectedBoard); - } + //-- Create the object params + //-- Currently it only contains one element, but in the future + //-- it can be increased + let params = { + 'filepath': filepath + }; - // Enable user events - this.enableKeyEvents(); - } - }); - - }; - - this.copySync = function (orig, dest) { - var ret = true; - try { - if (nodeFs.existsSync(orig)) { - nodeFse.copySync(orig, dest); - } else { - // Error: file does not exist - ret = false; - } - } catch (e) { - alertify.error(gettextCatalog.getString('Error: {{error}}', { - error: e.toString() - }), 30); - ret = false; - } - return ret; - }; - - 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; - }; - - //----------------------------------------------------------------------- - //-- Return a text in bold HTML - //-- Input: - //-- * text: String to convert to Bold - //-- Returns: - //-- * The HTML text in bold - //----------------------------------------------------------------------- - this.bold = function (text) { - return `${text}`; - }; - - //----------------------------------------------------------------------- - //-- Open the Dialog for choosing a file - //-- - //-- INPUTS: - //-- * inputID (String): Html selector of the file chooser input - //-- - //-- * callback(filepath): It is called when the user has pressed the - //-- ok button. The chosen file is passed as a parameter - //----------------------------------------------------------------------- - this.openDialog = function (inputID, callback) { - - //-- Get the file chooser element (from the DOM) - let chooser = $(inputID); - - //-- Remove any previously event attached - chooser.unbind('change'); - - //-- Attach a new callback function - chooser.change(function () { - - //-- It is executed when the user has selected the file - //-- Read the filepath entered by the user - let filepath = $(this).val(); - - //-- Execute the callback (if it was given) - if (callback) { - callback(filepath); - } + //-- Convert the params to json + let jsonParams = JSON.stringify(params); + + //-- Encode the params into Base64 format + let paramsBase64 = Buffer.from(jsonParams).toString('base64'); - //-- Remove the current select filename from the chooser - $(this).val(''); - }); + //-- Create the URL query with the icestudio_argv param + let icestudioArgv = '?icestudio_argv=' + paramsBase64; - //-- Activate the File chooser! (The element is shown, it waits for - //-- the user to enter the file, and the callback is executed) - chooser.trigger('click'); - }; + //-- Create the final URL, with parameters + url += icestudioArgv; + } + //-- Get the Window configuration from the package.json + let window = this.clone(_package.window); + //-- Set some needed properties: + window['new_instance'] = true; + window['show'] = true; - this.saveDialog = function (inputID, ext, callback) { - var chooser = $(inputID); - chooser.unbind('change'); - chooser.change(function ( /*evt*/) { - var filepath = $(this).val(); - if (!filepath.endsWith(ext)) { - filepath += ext; - } - if (callback) { - callback(filepath); + //-- The URL has this syntax: + // + //-- index.html?icestudio_argv=encoded_value + //-- + //-- Where encoded value is something like: + //-- eyJmaWxlcGF0aCI6Ii9ob21lL29iaWp1YW4vRGV2ZW... + + //----------------------------------------------------------- + //-- Open the new window + //-- More information: + //-- https://nwjs.readthedocs.io/en/latest/References/Window/ + //-- #windowopenurl-options-callback + //----------------------------------------------------------- + //nw.Window.open(url, window); + console.log("(DEBUG) Calling: nw.Window.open(url)"); + console.log(`Url: ${url}`); + nw.Window.open(url); + + + }; + + + //-- Place the path inside quotes. It is important for managing filepaths + //-- that contains spaces in their names + this.coverPath = coverPath; + + function coverPath(filepath) { + return '"' + 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(gettextCatalog.getString('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); + + 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; + }; + + 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 + }); + }; + + this.pasteFromClipboard = function (profile, callback) { + var _this = this; + nodeCP.paste(function (err, text) { + if (err) { + if (common.LINUX) { + // xclip installation message + var cmd = ''; + var message = gettextCatalog.getString('{{app}} is required.', { + app: 'xclip' + }); + 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 += ' ' + gettextCatalog.getString('Please run: {{cmd}}', { + cmd: '
' + cmd + '' + }); + } + } + alertify.warning(message, "30"); + }); + } + } else { + // Parse the global clipboard + var clipboard = JSON.parse(text); + if (callback && clipboard && clipboard.icestudio) { + const block = clipboard.icestudio; + if (block.version === common.VERSION) { + _this.approveProjectBlock(profile, block).then((result) => { + if (result === 'cancel') { + console.log('cancelPaste'); + return; + } + + callback(block); + }); + } else { + alertify.error(gettextCatalog.getString('Cannot paste from a different project format ({{version}})', { version: block.version }), 5); + } + } + } + }); + }; + + this.duplicateSelected = function (selection, graph, callback) { + let cells = selectionToCells(selection, graph); + let content = this.cellsToProject(cells, graph); + if (callback && content) { + callback(content); + } + }; + + 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; } - 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; - }; - - //----------------------------------------------------------------------- - //-- clone. Return a deep copy of the given input object data - //-- * data: Input object to copy - //-- * Returns: A copy of the input object - //----------------------------------------------------------------------- - this.clone = function (data) { - - //-- Implementation using the fast-copy npm package: - //-- More info: https://www.npmjs.com/package/fast-copy - return fastCopy(data); - - //-- Alternative implementation: - // 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)); - }; - - this.dependencyID = function (dependency) { - if (dependency.package && dependency.design) { - return nodeSha1(JSON.stringify(dependency.package) + - JSON.stringify(dependency.design)); - } - }; - - //----------------------------------------------------------------------- - //-- Create a new ICESTUDIO window - //-- - //-- INPUTS: - //-- * filepath: (optional) Icestudio file to open in the new window - //----------------------------------------------------------------------- - this.newWindow = function (filepath) { - - console.log("(DEBUG): Executing app/scripts/services/utils.js --> newWindow()"); - - //-- If there are parameters to pass or not - //-- No parameters by default - let hasParams = false; - - //-- URL with no parameters - let url = 'index.html'; - - //-- Create the arguments - //-- The filepath was given: pass it as an argument - if (filepath) { - - //-- There are params in the URL - hasParams = true; - - //-- Create the object params - //-- Currently it only contains one element, but in the future - //-- it can be increased - let params = { - 'filepath': filepath + + 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; }; - //-- Convert the params to json - let jsonParams = JSON.stringify(params); - - //-- Encode the params into Base64 format - let paramsBase64 = Buffer.from(jsonParams).toString('base64'); - - //-- Create the URL query with the icestudio_argv param - let icestudioArgv = '?icestudio_argv=' + paramsBase64; - - //-- Create the final URL, with parameters - url += icestudioArgv; - } - - //-- Get the Window configuration from the package.json - let window = this.clone(_package.window); - - //-- Set some needed properties: - window['new_instance'] = true; - window['show'] = true; - - //-- The URL has this syntax: - // - //-- index.html?icestudio_argv=encoded_value - //-- - //-- Where encoded value is something like: - //-- eyJmaWxlcGF0aCI6Ii9ob21lL29iaWp1YW4vRGV2ZW... - - //----------------------------------------------------------- - //-- Open the new window - //-- More information: - //-- https://nwjs.readthedocs.io/en/latest/References/Window/ - //-- #windowopenurl-options-callback - //----------------------------------------------------------- - //nw.Window.open(url, window); - console.log("(DEBUG) Calling: nw.Window.open(url)"); - console.log(`Url: ${url}`); - nw.Window.open(url); - - - }; - - - //-- Place the path inside quotes. It is important for managing filepaths - //-- that contains spaces in their names - 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]; + this.findSubDependencies = function (dependency) { + var subDependencies = []; + if (dependency) { + for (var i in dependency.design.graph.blocks) { + var type = dependency.design.graph.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; + }; + + // Check for Advanced block being opened or imported: If it has tri-state, and user does not have + // Advanced profile setting for tri-state, user needs to approve or cancel + // + // Return 'cancel' or return 'ok' or a variant of 'ok' + this.approveProjectBlock = function (profile, block, isLoad) { + if (profile.get('allowInoutPorts') || common.allowProjectInoutPorts) { + return Promise.resolve('ok'); + } + + const hasInoutPorts = checkIsAnyInout(block); + if (!hasInoutPorts) { + return Promise.resolve('ok'); + } + + // user can approve by either updating profile 'allowInoutPorts' or setting + // flag common.allowProjectInoutPorts + const prompt = (isLoad ? + gettextCatalog.getString('You are loading a design that uses \"tri-state\".') : + gettextCatalog.getString('You are importing a block that uses \"tri-state\".')) + + ' ' + + gettextCatalog.getString('Tri-state (aka high-Z, bidirectional, or inout) ports are not recommended in standard designs.

You will be asked to update your Preferences (Advanced user setting) or you can just open this design on a preview basis.

Continue?'); + return new Promise((resolve) => { + alertify.confirm(prompt, () => { + resolve('ok'); + }, () => { + resolve('cancel'); + }); + }) + .then((result) => { + if (result === 'cancel') { + return result; + } + + return new Promise((resolve) => { + alertify.set('confirm', 'defaultFocus', 'cancel'); + alertify.confirm(gettextCatalog.getString('Click \"Yes\" to allow tri-state and update Preferences:
   Advanced features → Allow tri-state connections

Click \"This time\" to view tri-state for this design only.'), () => { + profile.set('allowInoutPorts', true); + alertify.warning(gettextCatalog.getString('Changed Preferences: Allow tri-state connections')); + resolve('ok_advanced'); + }, () => { + common.allowProjectInoutPorts = true; + alertify.warning(gettextCatalog.getString('Viewing tri-state')); + resolve('ok_this_time'); + }).set('labels', { + ok: gettextCatalog.getString('Yes'), + cancel: gettextCatalog.getString('This time') + }); + }) + .then((result) => { + alertify.set('confirm', 'defaultFocus', 'ok'); + alertify.set('confirm', 'labels', { + ok: gettextCatalog.getString('OK'), + cancel: gettextCatalog.getString('Cancel') + }); + return result; + }); + }); + }; + + function checkIsAnyInout(project) { + if (_checkIsAnyInout(project)) { + return true; + } + for (var d in project.dependencies) { + if (_checkIsAnyInout(project.dependencies[d])) { + return true; + } + } + + function _checkIsAnyInout(_project) { + for (var i in _project.design.graph.blocks) { + var block = _project.design.graph.blocks[i]; + switch (block.type) { + case blocks.BASIC_INPUT: + case blocks.BASIC_OUTPUT: + if (block.data.inout) { + return true; + } + break; + case blocks.BASIC_CODE: + if (block.data.ports.inoutLeft && block.data.ports.inoutLeft.length) { + return true; + } + if (block.data.ports.inoutRight && block.data.ports.inoutRight.length) { + return true; + } + break; + default: + // Generic block + break; + } + } + return false; + } + return false; } - } - // Add the block as a dependency - delete block.dependencies; - common.allDependencies[type] = block; - }; - - 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 - }); - }; - - this.pasteFromClipboard = function (profile, callback) { - var _this = this; - nodeCP.paste(function (err, text) { - if (err) { - if (common.LINUX) { - // xclip installation message - var cmd = ''; - var message = gettextCatalog.getString('{{app}} is required.', { - app: 'xclip' - }); - 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'; + + 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; + } + } } - if (cmd) { - message += ' ' + gettextCatalog.getString('Please run: {{cmd}}', { - cmd: '
' + cmd + '' - }); + } + 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; + }; + + //------------------------------------------------------ + //-- Load the profile file + this.loadProfile = function (profile, callback) { + profile.load(function () { + if (callback) { + callback(); } - } - alertify.warning(message, "30"); }); - } - } else { - // Parse the global clipboard - var clipboard = JSON.parse(text); - if (callback && clipboard && clipboard.icestudio) { - const block = clipboard.icestudio; - if (block.version === common.VERSION) { - _this.approveProjectBlock(profile, block).then((result) => { - if (result === 'cancel') { - console.log('cancelPaste'); - return; - } + }; - callback(block); - }); + this.loadLanguage = function (profile, callback) { + var lang = profile.get('language'); + if (lang) { + this.setLocale(lang, callback); } else { - alertify.error(gettextCatalog.getString('Cannot paste from a different project format ({{version}})', { version: block.version }), 5); + // If lang is empty, use the system language + nodeLangInfo(function (err, sysLang) { + if (!err) { + profile.set('language', this.setLocale(sysLang, callback)); + } + }.bind(this)); } - } - } - }); - }; - - this.duplicateSelected = function (selection, graph, callback) { - let cells = selectionToCells(selection, graph); - let content = this.cellsToProject(cells, graph); - if (callback && content) { - callback(content); - } - }; - - 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; - } + }; + + this.digestId = function (id) { + if (id.indexOf('-') !== -1) { + id = nodeSha1(id).toString(); + } + return 'v' + id.substring(0, 6); + }; + + this.beginBlockingTask = function () { + angular.element('#menu').addClass('is-disabled'); + $('body').addClass('waiting'); + }; - var source = blocksMap[wire.get('source').id]; - var target = blocksMap[wire.get('target').id]; + this.endBlockingTask = function () { - if (source && target) { - cells.push(wire.attributes); - processedWires[wire.id] = true; - } + $('body').trigger('Graph::updateWires'); + angular.element('#menu').removeClass('is-disabled'); + $('body').removeClass('waiting'); + }; + + this.isFunction = function (functionToCheck) { + return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]'; + }; + + this.openDevToolsUI = function () { + nw.Window.get().showDevTools(); + }; + + //----------------------------------------------------------- + //-- Open a given url in an external browser + //-- + //-- INPUTS: + //-- * url (String): The URL to show + //----------------------------------------------------------- + this.openUrlExternalBrowser = function (url) { + + nw.Shell.openExternal(url); + + }; + + // RENDERFORM "color-dropdown" functions + // show/hide dropdown list + $(document).on("mousedown", ".lb-dropdown-title", function () { + if ($('.lb-dropdown-menu').hasClass('show')) { + closeDropdown(); + } else { + openDropdown(); + } }); - }); - 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) { - for (var i in dependency.design.graph.blocks) { - var type = dependency.design.graph.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; - }; - - // Check for Advanced block being opened or imported: If it has tri-state, and user does not have - // Advanced profile setting for tri-state, user needs to approve or cancel - // - // Return 'cancel' or return 'ok' or a variant of 'ok' - this.approveProjectBlock = function (profile, block, isLoad) { - if (profile.get('allowInoutPorts') || common.allowProjectInoutPorts) { - return Promise.resolve('ok'); - } - - const hasInoutPorts = checkIsAnyInout(block); - if (!hasInoutPorts) { - return Promise.resolve('ok'); - } - - // user can approve by either updating profile 'allowInoutPorts' or setting - // flag common.allowProjectInoutPorts - const prompt = (isLoad ? - gettextCatalog.getString('You are loading a design that uses \"tri-state\".') : - gettextCatalog.getString('You are importing a block that uses \"tri-state\".')) + - ' ' + - gettextCatalog.getString('Tri-state (aka high-Z, bidirectional, or inout) ports are not recommended in standard designs.

You will be asked to update your Preferences (Advanced user setting) or you can just open this design on a preview basis.

Continue?'); - return new Promise((resolve) => { - alertify.confirm(prompt, () => { - resolve('ok'); - }, () => { - resolve('cancel'); + $(document).on("mouseleave", ".lb-dropdown-menu", function () { + closeDropdown(); }); - }) - .then((result) => { - if (result === 'cancel') { - return result; - } - - return new Promise((resolve) => { - alertify.set('confirm', 'defaultFocus', 'cancel'); - alertify.confirm(gettextCatalog.getString('Click \"Yes\" to allow tri-state and update Preferences:
   Advanced features → Allow tri-state connections

Click \"This time\" to view tri-state for this design only.'), () => { - profile.set('allowInoutPorts', true); - alertify.warning(gettextCatalog.getString('Changed Preferences: Allow tri-state connections')); - resolve('ok_advanced'); - }, () => { - common.allowProjectInoutPorts = true; - alertify.warning(gettextCatalog.getString('Viewing tri-state')); - resolve('ok_this_time'); - }).set('labels', { - ok: gettextCatalog.getString('Yes'), - cancel: gettextCatalog.getString('This time') - }); - }) - .then((result) => { - alertify.set('confirm', 'defaultFocus', 'ok'); - alertify.set('confirm', 'labels', { - ok: gettextCatalog.getString('OK'), - cancel: gettextCatalog.getString('Cancel') - }); - return result; + $(document).on("mouseenter", ".ajs-button", function () { + closeDropdown(); }); - }); - }; - - function checkIsAnyInout(project) { - if (_checkIsAnyInout(project)) { - return true; - } - for (var d in project.dependencies) { - if (_checkIsAnyInout(project.dependencies[d])) { - return true; - } - } - - function _checkIsAnyInout(_project) { - for (var i in _project.design.graph.blocks) { - var block = _project.design.graph.blocks[i]; - switch (block.type) { - case blocks.BASIC_INPUT: - case blocks.BASIC_OUTPUT: - if (block.data.inout) { - return true; - } - break; - case blocks.BASIC_CODE: - if (block.data.ports.inoutLeft && block.data.ports.inoutLeft.length) { - return true; - } - if (block.data.ports.inoutRight && block.data.ports.inoutRight.length) { - return true; - } - break; - default: - // Generic block - break; - } - } - return false; - } - return false; - } - - 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; - } - } + // get selected option + $(document).on("mousedown", ".lb-dropdown-option", function () { + let selected = this; + $('.lb-dropdown-title').html("" + selected.dataset.name + ""); + closeDropdown(); + }); + + function openDropdown() { + $('.lb-dropdown-menu').addClass('show'); } - } - 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; - }; - - //------------------------------------------------------ - //-- Load the profile file - this.loadProfile = function (profile, callback) { - profile.load(function () { - if (callback) { - callback(); + function closeDropdown() { + $('.lb-dropdown-menu').removeClass('show'); } - }); - }; - - this.loadLanguage = function (profile, callback) { - var lang = profile.get('language'); - if (lang) { - this.setLocale(lang, callback); - } else { - // If lang is empty, use the system language - nodeLangInfo(function (err, sysLang) { - if (!err) { - profile.set('language', this.setLocale(sysLang, callback)); - } - }.bind(this)); - } - }; - - this.digestId = function (id) { - if (id.indexOf('-') !== -1) { - id = nodeSha1(id).toString(); - } - return 'v' + id.substring(0, 6); - }; - - this.beginBlockingTask = function () { - angular.element('#menu').addClass('is-disabled'); - $('body').addClass('waiting'); - }; - - this.endBlockingTask = function () { - - $('body').trigger('Graph::updateWires'); - angular.element('#menu').removeClass('is-disabled'); - $('body').removeClass('waiting'); - }; - - this.isFunction = function (functionToCheck) { - return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]'; - }; - - this.openDevToolsUI = function () { - nw.Window.get().showDevTools(); - }; - - //----------------------------------------------------------- - //-- Open a given url in an external browser - //-- - //-- INPUTS: - //-- * url (String): The URL to show - //----------------------------------------------------------- - this.openUrlExternalBrowser = function (url) { - - nw.Shell.openExternal(url); - - }; - - // RENDERFORM "color-dropdown" functions - // show/hide dropdown list - $(document).on("mousedown", ".lb-dropdown-title", function () { - if ($('.lb-dropdown-menu').hasClass('show')) { - closeDropdown(); - } else { - openDropdown(); - } - }); - $(document).on("mouseleave", ".lb-dropdown-menu", function () { - closeDropdown(); - }); - $(document).on("mouseenter", ".ajs-button", function () { - closeDropdown(); - }); - // get selected option - $(document).on("mousedown", ".lb-dropdown-option", function () { - let selected = this; - $('.lb-dropdown-title').html("" + selected.dataset.name + ""); - closeDropdown(); - }); - function openDropdown() { - $('.lb-dropdown-menu').addClass('show'); - } - function closeDropdown() { - $('.lb-dropdown-menu').removeClass('show'); - } - - this.renderForm = function (specs, callback) { - var content = []; - content.push('
'); - for (var i in specs) { - var spec = specs[i]; - switch (spec.type) { - case 'text': - content.push('\ + this.renderForm = function (specs, callback) { + var content = []; + content.push('
'); + for (var i in specs) { + var spec = specs[i]; + switch (spec.type) { + case 'text': + content.push('\

' + spec.title + '

\ \ '); - break; - case 'checkbox': - content.push('\ + break; + case 'checkbox': + content.push('\
\ \
\ '); - break; - case 'combobox': - var options = spec.options.map(function (option) { - var selected = spec.value === option.value ? ' selected' : ''; - return ''; - }).join(''); - content.push('\ + break; + case 'combobox': + var options = spec.options.map(function (option) { + var selected = spec.value === option.value ? ' selected' : ''; + return ''; + }).join(''); + content.push('\
\ \ \
\ '); - break; - case 'color-dropdown': - content.push('\ + break; + case 'color-dropdown': + content.push('\
\ \
\ @@ -1644,54 +1644,54 @@ angular.module('icestudio')
\
\ '); - break; - } - } - content.push('
'); - - alertify.confirm(content.join('\n')) - .set('onok', function (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; - case 'color-dropdown': - values.push($('.lb-selected-color').data('color')); - break; - } + break; + } } - callback(evt, values); - } - }) - .set('oncancel', function ( /*evt*/) { }); - - // Restore input values - setTimeout(function () { - $('#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; - case 'color-dropdown': - $('.lb-dropdown-title').html("Fuchsia"); - break; - } - } - }, 50); - }; + content.push('
'); + + alertify.confirm(content.join('\n')) + .set('onok', function (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; + case 'color-dropdown': + values.push($('.lb-selected-color').data('color')); + break; + } + } + callback(evt, values); + } + }) + .set('oncancel', function ( /*evt*/) { }); + + // Restore input values + setTimeout(function () { + $('#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; + case 'color-dropdown': + $('.lb-dropdown-title').html("Fuchsia"); + break; + } + } + }, 50); + }; - }); + });