| Scripts/interface-reordering-interfaceLib.js | "use strict";
this.name = "InterfaceReordering_InterfaceLib";
this.author = "Alnivel";
this.copyright = "2022 Alnivel";
this.version = "0.4";
this.licence = "CC BY-NC-SA 4.0";
(function() {
    let ILib = this;
    this.__BaseController = function __BaseController() {}
    this.__BaseController.prototype.onRun = function onRun(callback) {
        this.callbackOnRun = callback;
        return this;
    }
    this.__BaseController.prototype.onUpdate = function onUpdate(callback) {
        this.callbackOnUpdate = callback;
        return this;
    }
    this.__BasePagedController = function __BasePagedController(screenParameters) {
        __BasePagedController_init(screenParameters);
    }
    this.__BasePagedController.prototype = Object.create(__BaseController.prototype);
    this.__BasePagedController.prototype.__BasePagedController_init = function (screenParameters)  {
        this.screenParameters = screenParameters;
        this.headerLines = [];
        this.contentLines = [];
        this.footerLines = [];
        this.headerPrefix = "00HEADER";
        this.contentPrefix = "20CONTENT";
        this.footerPrefix = "40FOOTER";
        this.currentPage = 0;
        this.pages = 0;
        this.selectedChoicesKey = undefined;
    }
    this.__BasePagedController.prototype.addToHeader = function addToHeader(line) {
        const headerLinesCount = this.headerLines.length;
        const key = (headerLinesCount < 10 ? ("0" + headerLinesCount) : headerLinesCount) + "_NO_KEY";
        this.headerLines.push({ key: key, line: line });
    }
    this.__BasePagedController.prototype.addToContent = function addToContent(line) {
        const contentLinesCount = this.contentLines.length;
        const key = (contentLinesCount < 10 ? ("0" + contentLinesCount) : contentLinesCount) + "_NO_KEY";
        this.contentLines.push({ key: key, line: line });
    }
    this.__BasePagedController.prototype.addToFooter = function addToFooter(line) {
        const footerLinesCount = this.footerLines.length;
        const key = (footerLinesCount < 10 ? ("0" + footerLinesCount) : footerLinesCount) + "_NO_KEY";
        this.footerLines.push({ key: key, line: line });
    }
    /*
        Remove line from ...
        this.__BasePagedScreenController.prototype.removeLineFromContent = function removeFromContent(line) {
        let removePos = -1;
        let i = this.contentLines.length;
        while (i--) {
            if (this.contentLines[i].line === line) {
                removePos = i;
                break;
            }
        }
        if (removePos === -1)
            return false;
        else {
            this.contentLines.splice(removePos, 1);
            return true;
        }
    }
    */
    this.__BasePagedController.prototype.clearHeader = function clearHeader() {
        this.headerLines.length = 0;
    }
    this.__BasePagedController.prototype.clearContent = function clearHeader() {
        this.contentLines.length = 0;
    }
    this.__BasePagedController.prototype.clearFooter = function clearHeader() {
        this.footerLines.length = 0;
    }
    /***
     * Update every line in header and fill "choises" parameter with them  
     * @param {object} choises
     * @returns {number} Header height in lines
     */
    this.__BasePagedController.prototype.updateHeader = function updateHeader(choises) {
        const linesForHeader = this.headerLines.length;
        for (let i = 0; i < linesForHeader; i++) {
            let pair = this.headerLines[i];
            let key = this.headerPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
            choises[key] = pair.line.update();
        }
        return linesForHeader;
    }
    /**
     * Update content lines on specified page and fill "choises" parameter with them
     * @param {object} choises
     * @param {number} linesPerPage Maximum number of lines per page
     * @param {number} page Page number
     * @returns {number} Content height on specified page in lines
     */
    this.__BasePagedController.prototype.updateContent = function updateContent(choises, linesPerPage, page) {
        let linesStartIndex = linesPerPage * page;
        let linesEndIndex = linesStartIndex + linesPerPage;
        let contentLinesCount = this.contentLines.length;
        let linesEndIndexLimited = contentLinesCount < linesEndIndex ? contentLinesCount : linesEndIndex;
        for (let i = linesStartIndex; i < linesEndIndexLimited; i++) {
            let pair = this.contentLines[i];
            let key = this.contentPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
            choises[key] = pair.line.update();
        }
        return linesEndIndexLimited - linesStartIndex;
    }
    /***
     * Update every line in footer and fill "choises" parameter with them  
     * @param {object} choises
     * @returns {number} Footer height in lines
     */
    this.__BasePagedController.prototype.updateFooter = function updateFooter(choises) {
        const linesForFooter = this.footerLines.length;
        
        for (let i = 0; i < linesForFooter; i++) {
            let pair = this.footerLines[i];
            let key = this.footerPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
            choises[key] = pair.line.update();
        }
        return linesForFooter;
    }
    this.__BasePagedController.prototype.proccessChoises = function proccessChoises(choise) {
        for (let i = 0; i < this.headerLines.length; i++) {
            let pair = this.headerLines[i];
            let key = this.headerPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
            let line = pair.line;
            if (choise === key) {
                return line.select && line.select(); // if true returned then don`t redraw this screen
            }
        }
        
        for (let i = 0; i < this.contentLines.length; i++) {
            let pair = this.contentLines[i];
            let key = this.contentPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
            let line = pair.line;
            if (choise === key) {
                return line.select && line.select();
            }
        }
        for (let i = 0; i < this.footerLines.length; i++) {
            let pair = this.footerLines[i];
            let key = this.footerPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
            let line = pair.line;
            if (choise === key) {
                return line.select && line.select();
            }
        }
        // Restarting the screen, if necessary, is performed by the child 
        /* 
        this.screenParameters.choices = this.getPage(this.currentPage);
        this.screenParameters.initialChoicesKey = initialChoicesKey;
        mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
        */
    }
    this.__BasePagedController.prototype.runScreen = function runScreen() {
        log("Alnivel_InterfaceLib_Message", "Started " + this.screenParameters.title + " mission screen");
        if(this.callbackOnRun)
            this.callbackOnRun();
        
        this.currentPage = 0;
        this.screenParameters.choices = this.getPage(0);
        mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
    }
    this.__BasePagedController.prototype.returnToScreen = function returnToScreen() {
        if(this.currentPage === undefined || this.currentPage > this.pages)
            this.currentPage = 0;
        log("Alnivel_InterfaceLib_Message", "Returned to " + this.screenParameters.title + " mission screen on page " + this.currentPage);
        if(this.callbackOnRun)
            this.callbackOnRun();
        
        this.screenParameters.choices = this.getPage(this.currentPage);
        this.screenParameters.initialChoicesKey = this.selectedChoicesKey;
        mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
    }
    /**
     * Create paged screen that can be filled with line objects.
     * Each time the line is selected, it will re-render, 
     * unless the Return option is selected (then will be called callbackOnReturn)
     * or the onSelect callback of the selected line returns true
     * @param {*} screenParameters 
     * @param {Function} [callbackOnReturn]
     */
    this.PagedScreenController = function PagedScreenController(screenParameters, callbackOnReturn) {
        this.__BasePagedController_init(screenParameters); // Don`t know how to get base constructor properly
        this.callbackOnReturn = callbackOnReturn ?  callbackOnReturn : null;
    }
    this.PagedScreenController.prototype = Object.create(__BasePagedController.prototype);
    this.PagedScreenController.prototype.onReturn = function onReturn(callback) {
        this.callbackOnReturn = callback;
        return this;
    }
    this.PagedScreenController.prototype.updateControls = function updateControls(choises, pages, page) {
        const spacer = { text: "" };
        
        if (pages > 1) {
            choises["70_SPACER"] = spacer;
            choises["71_PAGE_NUM"] = {
                text: "Page " + (1 + page) + " of " + pages,
                unselectable: true
            }
            choises["73_NEXT_PAGE"] = {
                text: "Next page",
                unselectable: page + 1 === pages
            };
            choises["74_PREV_PAGE"] = {
                text: "Previous page",
                unselectable: page === 0
            };
        } else {
            choises["70_SPACER"] = spacer;
            choises["71_SPACER"] = spacer;
            choises["73_SPACER"] = spacer;
            choises["74_SPACER"] = spacer;
        }
        choises["80_RETURN"] = { text: "Return" };
        return 5;
    }
    this.PagedScreenController.prototype.getPage = function getPage(page) {
        if(this.callbackOnUpdate)
            this.callbackOnUpdate();
        const screenHeight = (player.ship.hudAllowsBigGui ? 27 : 21);
        const choises = {};
        const headerHeight = this.updateHeader(choises);
        const footerHeight = this.updateFooter(choises);
        const controlsHeight = 5;
        const maxContentHeight = screenHeight - headerHeight - footerHeight - controlsHeight;
        const pages = this.pages = Math.ceil(this.contentLines.length / maxContentHeight);
        const contentHeight = this.updateContent(choises, maxContentHeight, page);
        const remainHeight = maxContentHeight - contentHeight;
        log("Alnivel_InterfaceLib_Message", "Remain lines after content " + remainHeight + " from " + maxContentHeight)
        /*** Fill empty ***/
        const spaceToFill = remainHeight < maxContentHeight? remainHeight : maxContentHeight;
        const spacer = { text: "" };
        for (let i = 0; i < spaceToFill; i++) {
            let key = "21EMPTY_" + (i < 10 ? ("0" + i) : i) + "_EMPTY";
            choises[key] = spacer;
        }
        /*** Controlls ***/
        this.updateControls(choises, pages, page);
        return choises;
    }
    this.PagedScreenController.prototype.getPageWithLineIndex = function getPageWithLineIndex(lineIndex) {
        if(lineIndex === null || lineIndex === undefined || 
            lineIndex === -1 || lineIndex >= this.contentLines.length ) {
            // TODO: add warning
            return this.getPage(0);
        }
        const screenHeight = (player.ship.hudAllowsBigGui ? 27 : 21);
        const choises = {};
        const headerHeight = this.updateHeader(choises);
        const footerHeight = this.updateFooter(choises);
        const controlsHeight = 5;
        const maxContentHeight = screenHeight - headerHeight - footerHeight - controlsHeight;
        const pages = this.pages = Math.ceil(this.contentLines.length / maxContentHeight);
        const page = this.currentPage = Math.floor(lineIndex / maxContentHeight);
        const contentHeight = this.updateContent(choises, maxContentHeight, page);
        const remainHeight = maxContentHeight - contentHeight;
        log("Alnivel_InterfaceLib_Message", "Remain lines after content " + remainHeight + " from " + maxContentHeight)
        /*** Fill empty ***/
        const spaceToFill = remainHeight < maxContentHeight? remainHeight : maxContentHeight;
        const spacer = { text: "" };
        for (let i = 0; i < spaceToFill; i++) {
            let key = "21EMPTY_" + (i < 10 ? ("0" + i) : i) + "_EMPTY";
            choises[key] = spacer;
        }
        /*** Controlls ***/
        this.updateControls(choises, pages, page);
        return choises;
    }
    this.PagedScreenController.prototype.getPageWithLine = function getPageWithLine(line) {
        const lineIndex = this.contentLines.findIndex(function (pair) {
            return pair.line === line;
        });
        return this.getPageWithLineIndex(lineIndex);
    }
    this.PagedScreenController.prototype.onChoiceSelected = function onChoiceSelected(choise) {
        if (choise === "80_RETURN") {
            if (this.callbackOnReturn) this.callbackOnReturn();
            return;
        }
        this.selectedChoicesKey = choise;
        if (choise === "74_PREV_PAGE") {
            this.currentPage--;
            if (this.currentPage === 0)
                this.selectedChoicesKey = "73_NEXT_PAGE";
        }
        else if (choise === "73_NEXT_PAGE") {
            this.currentPage++;
            if (this.currentPage + 1 === this.pages)
                this.selectedChoicesKey = "74_PREV_PAGE";
        }
        else {
            const stopRestartingScreen = this.proccessChoises(choise);
            
            if(stopRestartingScreen)
            {
                log("Alnivel_InterfaceLib_Message", "Screen \"" + this.screenParameters.title + "\" restarting has been stopped by key " + choise)
                return;
            }
                
        }
        this.screenParameters.choices = this.getPage(this.currentPage);
        this.screenParameters.initialChoicesKey = this.selectedChoicesKey;
        mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
    }
    this.PagedScreenController.prototype.runPage = function runPage(page, noUpdate) {
        log("Alnivel_InterfaceLib_Message", "Started " + this.screenParameters.title + " mission screen");
        if(!noUpdate && this.callbackOnRun)
            this.callbackOnRun();
        else
            this.screenParameters.initialChoicesKey = this.selectedChoicesKey;
        
        if(this.currentPage === undefined || this.currentPage > this.pages)
            this.currentPage = 0;
        else
            this.currentPage = page;
        this.screenParameters.choices = this.getPage(this.currentPage);
        mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
    }
    this.PagedScreenController.prototype.runPageWithLine = function runPageWithLine(line, noUpdate) {
        log("Alnivel_InterfaceLib_Message", "Started " + this.screenParameters.title + " mission screen");
        if(!noUpdate && this.callbackOnRun)
            this.callbackOnRun();
        else
            this.screenParameters.initialChoicesKey = this.selectedChoicesKey;
        
        this.currentPage = 0;
        this.screenParameters.choices = this.getPageWithLine(line);
        mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
    }
    this.PagedScreenController.prototype.runPageWithLineIndex = function runPageWithLineIndex(lineIndex, noUpdate) {
        log("Alnivel_InterfaceLib_Message", "Started " + this.screenParameters.title + " mission screen");
        if(!noUpdate && this.callbackOnRun)
            this.callbackOnRun();
        else
            this.screenParameters.initialChoicesKey = this.selectedChoicesKey;
        
        this.currentPage = 0;
        this.screenParameters.choices = this.getPageWithLineIndex(lineIndex);
        mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
    }
    this.$limitText = function $limitText(text, limitWidth) {
        const ellipsis = "…";
        const hairSpace = String.fromCharCode(31);
        const hairSpaceWidth = defaultFont.measureString(hairSpace);
        let tmp = text;
        while (defaultFont.measureString(tmp) > limitWidth) {
            tmp = tmp.substring(0, tmp.length - 2) + ellipsis;
        }
        
        const padsNeeded = Math.floor((limitWidth - defaultFont.measureString(tmp)) / hairSpaceWidth);
        if (padsNeeded > 1)
            tmp = tmp + new Array(padsNeeded).join(hairSpace);
        return tmp;
    }
    /**
     * 
     * @param {string} text 
     * @param {"left"|"right"} padSide 
     * @param {number} desiredWidth 
     * @returns Padded text 
     */
    this.$padTextOnSide = function $padTextOnSide(text, padSide, desiredWidth) {
        const hairSpace = String.fromCharCode(31);
        const hairSpaceWidth = defaultFont.measureString(hairSpace);
        const textWidth = defaultFont.measureString(text);
        const padsNeeded = Math.floor((desiredWidth - textWidth) / hairSpaceWidth);
        let resultText;
        if (padsNeeded > 1)
            if (padSide == "left")
                resultText = new Array(padsNeeded).join(hairSpace) + text;
            else
                resultText = text + new Array(padsNeeded).join(hairSpace);
        else
            resultText = this.$limitText(text, desiredWidth);
        return resultText;
    }
    this.$padTextAround = function $padTextAround(text, desiredWidth) {
        const hairSpace = String.fromCharCode(31);
        const hairSpaceWidth = defaultFont.measureString(hairSpace);
        const textWidth = defaultFont.measureString(text);
        const padsNeeded = Math.floor((desiredWidth - textWidth) / (2 * hairSpaceWidth));
        let resultText;
        if (padsNeeded > 1) {
            let sidePadding = new Array(padsNeeded).join(hairSpace);
            resultText = sidePadding + text + sidePadding;
        }
        else
            resultText = this.$limitText(text, desiredWidth);
        return resultText;
    }
    this.$repeatPattern = function $repeatPattern(pattern, desiredWidth) {
        const patternWidth = defaultFont.measureString(pattern);
        const repeatsNeeded = Math.ceil(desiredWidth / patternWidth);
        let tmp = new Array(repeatsNeeded).join(pattern);
        while (defaultFont.measureString(tmp) > desiredWidth) {
            tmp = tmp.substring(0, tmp.length - 1);
        }
        return tmp
    }
    this.__LineWithCallbacksPrototype = {
        /**
         * Set callback that will be called when line is redrawing. 
         * The arguments for the function are same as for select callback, 
         * but it can return dictionary whose 
         * key-value pairs will be setted as choice parameters
         * @param {Function} updateCallback 
         * @returns Line object
         */
        onUpdate: function onUpdate(updateCallback) {
            this.updateCallback = updateCallback;
            return this;
        },
        /**
         * Set callback that will be called when line is selected. 
         * The arguments for the function depends on line type.
         * Is callback return true than screen will not redrawed. 
         * It can be used to change to another mission screen.
         * @param {*} selectCallback 
         * @returns Line object
         */
        onSelected: function onSelected(selectCallback) {
            this.selectCallback = selectCallback;
            return this;
        }
    }
    // TODO: Add lines hiding?
    /**
     * Create empty line object
     */
    this.EmptyLine = function () {}
    this.EmptyLine.prototype.update = function update() {
        return { text: "" };
    }
    /**
     * Create line object that can`t be selected
     * @param {string} text 
     */
    this.LabelLine = function (text, alignment) {
        this.choiceObject = {
            text: ILib.$padTextAround(text, 32),
            unselectable: true,
            color: "whiteColor",
            alignment: alignment? alignment : "CENTER"
        }
    }
    this.LabelLine.prototype = Object.create(__LineWithCallbacksPrototype);
    this.LabelLine.prototype.update = function update() {
        if (this.updateCallback) {
            let updated = this.updateCallback();
            const updatedKeys = Object.keys(updated);
            for (let i = 0; i < updatedKeys.length; i++) {
                const key = updatedKeys[i];
                const value = updated[key];
                if (value !== undefined &&
                    key !== "unselectable") {
                    if (key === "text")
                        this.choiceObject[key] = ILib.$padTextOnSide(value, "right", 32);
                    else if (key === "color")
                        this.choiceObject[key] = value === "default" ? "whiteColor" : value;
                    else
                        this.choiceObject[key] = value;
                }
            }
        }
        return this.choiceObject;
    };
    /**
     * Create line object that can be selected.
     * When selected onSelected callback will be called without any arguments
     * @param {string} text 
     */
    this.ButtonLine = function (text) {
        this.choiceObject = {
            text: ILib.$padTextOnSide(text, "right", 32),
            color: "cyanColor",
            alignment: "LEFT"
        };
        this.selectableColor = "cyanColor";
        this.unselectableColor = "darkGrayColor";
    }
    this.ButtonLine.prototype = Object.create(__LineWithCallbacksPrototype);
    this.ButtonLine.prototype.update = function update() {
        if (this.updateCallback) {
            let updated = this.updateCallback();
            const updatedKeys = Object.keys(updated);
            for (let i = 0; i < updatedKeys.length; i++) {
                const key = updatedKeys[i];
                const value = updated[key];
                if (value !== undefined) {
                    if (key === "color")
                        this.selectableColor = value === "default" ? "cyanColor" : value;
                    else if (key === "text")
                        this.choiceObject[key] = ILib.$padTextOnSide(value, "right", 32);
                    else
                        this.choiceObject[key] = value;
                }
            }
            if (this.choiceObject.unselectable)
                this.choiceObject.color = this.unselectableColor;
            else
                this.choiceObject.color = this.selectableColor;
        }
        return this.choiceObject;
    }
    this.ButtonLine.prototype.select = function select() {
        if (this.selectCallback)
            return this.selectCallback();
    }
    /**
     * Create line object that toggles between true and false when selected.
     * When selected onSelected callback will be called with new state as argument
     * @param {string} text 
     * @param {Boolean} initialState 
     */
    this.ToggleLine = function (text, initialState) {
        this.choiceObject = {
            color: "cyanColor"
        };
        this.selectableColor = "cyanColor";
        this.unselectableColor = "darkGrayColor";
        this.text = text;
        this.state = !!initialState;
    }
    this.ToggleLine.prototype = Object.create(__LineWithCallbacksPrototype);
    this.ToggleLine.prototype.update = function update() {
        if (this.updateCallback) {
            let updated = this.updateCallback(this.state);
            const updatedKeys = Object.keys(updated);
            for (let i = 0; i < updatedKeys.length; i++) {
                const key = updatedKeys[i];
                const value = updated[key];
                if (value !== undefined) {
                    if (key === "color")
                        this.selectableColor = value === "default" ? "cyanColor" : value;
                    else if (key === "text")
                        this.text = value;
                    else if (key === "state")
                        this.state = !!value;
                    else
                        this.choiceObject[key] = value;
                }
            }
            if (this.choiceObject.unselectable)
                this.choiceObject.color = this.unselectableColor;
            else
                this.choiceObject.color = this.selectableColor;
        }
        this.choiceObject.text =
            ILib.$padTextOnSide(this.text, "right", 24) +
            ILib.$padTextAround(" < " + (this.state ? "On" : "Off") + " > ", 8);
        return this.choiceObject;
    }
    this.ToggleLine.prototype.select = function select() {
        this.state = !this.state;
        if (this.selectCallback)
            return this.selectCallback(this.state);
    }
    /**
     * Create line object that cycles between the specified values when selected.
     * When selected onSelected callback will be called with new index and value as arguments
     * @param {string} text
     * @param {Array|Object.<string, any>} items If it is an array the values are 
     * the same as the labels, otherwise it is treated as a dictionary of labels to values
     * @param {any} [initialItem] 
     */
    this.CycleLine = function (text, items, initialItem) {
        const itemsIsArray = Array.isArray(items);
        const itemKeys = itemsIsArray ? items : Object.keys(items);
        let itemValues;
        if (itemsIsArray)
            itemValues = items
        else { // Object.values(items);
            itemValues = Array(itemKeys.length)
            for (let i = 0; i < itemKeys.length; i++)
                itemValues[i] = items[itemKeys[i]]
        }
        let initialIndex = itemKeys.indexOf(initialItem);
        if (initialIndex === -1)
            initialIndex = 0;
        this.choiceObject = {
            color: "cyanColor",
        };
        this.items = itemKeys;
        this.values = itemValues;
        this.currentIndex = initialIndex;
        this.text = text;
        this.selectableColor = "cyanColor";
        this.unselectableColor = "darkGrayColor";
    }
    this.CycleLine.prototype = Object.create(__LineWithCallbacksPrototype);
    this.CycleLine.prototype.update = function update() {
        if (this.updateCallback) {
            let updated = this.updateCallback(this.currentIndex, this.values[this.currentIndex]);
            const updatedKeys = Object.keys(updated);
            for (let i = 0; i < updatedKeys.length; i++) {
                const key = updatedKeys[i];
                const value = updated[key];
                if (value !== undefined) {
                    if (key === "color")
                        this.selectableColor = value === "default" ? "cyanColor" : value;
                    else if (key === "text")
                        this.text = value;
                    else if (key === "currentIndex") {
                        this.currentIndex = value;
                    }
                    else
                        this.choiceObject[key] = value;
                }
            }
            if (this.choiceObject.unselectable)
                this.choiceObject.color = this.unselectableColor;
            else
                this.choiceObject.color = this.selectableColor;
        }
        const currentItem = this.items[this.currentIndex];
        this.choiceObject.text =
            ILib.$padTextOnSide(this.text, "right", 24) +
            ILib.$padTextAround(" < " + currentItem + " > ", 8);
        return this.choiceObject;
    }
    this.CycleLine.prototype.select = function select() {
        this.currentIndex = (this.currentIndex + 1) % this.values.length;
        if (this.selectCallback)
            return this.selectCallback(this.currentIndex, this.values[this.currentIndex]);
    }
}).call(this); | 
                
                    | Scripts/interface-reordering-main.js | "use strict";
this.name = "InterfaceReordering";
this.author = "Alnivel";
this.copyright = "2022 Alnivel";
this.version = "0.2";
this.licence = "CC BY-NC-SA 4.0";
this.$interfacePrefixes = {/* key: prefix */ };
this.$idToStationMap = [/* stationId: station */];
this.$stationIdToInterfacesMap = [/* stationId: { key: interfaceDefinition }*/];
/// Replace orginal method with one which modify original interface category 
/// to visualy similar string that will be placed acording to interfaceOrder
///
/// If something add interface directly in the body of the script, the set  
/// interface will be above all others and can't be reordered until it will be reset
///
/// Well, it could be worse
this.$originalSetInterface = Station.prototype.setInterface;
(function substituteSetInterface() {
    let idToStationMap = this.$idToStationMap;
    let stationIdToInterfacesMap = this.$stationIdToInterfacesMap;
    let script = this;
    let originalSetInterface = this.$originalSetInterface;
    Station.prototype.setInterface = function (key, interfaceDefinition) {
        let stationId = idToStationMap.indexOf(this);
        if (stationId === -1) {
            stationId = idToStationMap.length;
            idToStationMap.push(this);
            stationIdToInterfacesMap[stationId] = {};
        }
        let seenInterfaces = stationIdToInterfacesMap[stationId];
        if (interfaceDefinition !== null) {
            let reorderedDefinition = {};
            for (let idKey in interfaceDefinition) {
                reorderedDefinition[idKey] = interfaceDefinition[idKey];
            }
            // // A way to get the name of the calling script
            // let stack = new Error().stack;
            // let callerScript = stack.substring(
            //     stack.indexOf("\"(\"") + 3, 
            //     stack.indexOf("\",[object Object])")
            // );
            //            
            // interfaceDefinition.InterfaceReordering_callerScript = callerScript;
            // //
            // I hope someone doesn't use the same key as an existing category
            let prefix = script.$interfacePrefixes[key] ||
                script.$interfacePrefixes[interfaceDefinition.category] ||
                script.$interfacePrefixes["InterfaceReordering_OtherPrefix"];
            reorderedDefinition.category = prefix + interfaceDefinition.category;
            originalSetInterface.call(this, key, reorderedDefinition);
            seenInterfaces[key] = interfaceDefinition;
        }
        else {
            originalSetInterface.call(this, key, null);
            if (seenInterfaces[key])
                seenInterfaces[key] = null;
        }
    }
}).call(this);
this.startUp = function () {
    const storedInterfaceOrder = JSON.parse(missionVariables.interfaceReordering_interfaceOrder);
    if (storedInterfaceOrder)
        this.$interfaceOrder = storedInterfaceOrder;
    const hasOtherEntry = this.$interfaceOrder.some(function (orderItem) {
        return orderItem.key === "InterfaceReordering_OTHER";
    })
    if (!hasOtherEntry) {
        this.$interfaceOrder.push({ key: "InterfaceReordering_OTHER", name: "<Other interfaces>", type: "" });
    }
    // copy loaded order to variable that used by user interface
    this.$unsavedInterfaceOrder.length = 0;
    Array.prototype.push.apply(this.$unsavedInterfaceOrder, this.$interfaceOrder);
    this.$updateInterfaceOrder(this.$interfaceOrder);
}
this.interfaceInitialized = false;
this.startUpComplete = this.shipDockedWithStation = function () {
    if (!this.interfaceInitialized) {
        this.$initializeInterfaceScreens();
        this.interfaceInitialized = true;
    }
    const interfaceMain = this.$screens.main;
    const station = player.ship.dockedStation;
    station.setInterface("InterfaceReordering_ManageOrder", {
        title: "Reorder interfaces",
        category: "Ship Systems",
        summary: "Change the order of interfaces",
        callback: interfaceMain.runScreen.bind(interfaceMain)
    });
}
this.playerWillSaveGame = function () {
    missionVariables.interfaceReordering_interfacePrefixes = JSON.stringify(this.$interfacePrefixes);
    missionVariables.interfaceReordering_interfaceOrder = JSON.stringify(this.$interfaceOrder);
};
// Clear known stations and interfaces before 
// any interfaces from new system will be set
this.shipWillEnterWitchspace = function () {
    this.$idToStationMap.length = 0;
    this.$stationIdToInterfacesMap.length = 0;
};
// Settings interface //
this.$screens = {};
this.$interfaceOrder = [
    { key: "InterfaceReordering_OTHER", name: "<Other interfaces>", type: "" },
    /* Other entries looks like
    {key: "<interfaceKey>", name:"<interfaceName>",  type:"Interface"},
    {key: "<categoryName>", name:"<categoryName>",  type:"Category"},
    */
];
this.$unsavedInterfaceOrder = []; // changes in order stored here until they saved 
this.$initializeInterfaceScreens = function $initializeInterfaceScreens() {
    /*** Binding script variables to function scope ***/
    const ILib = worldScripts["InterfaceReordering_InterfaceLib"];
    const mainScript = this;
    const screens = this.$screens;
    const stationIdToInterfacesMap = this.$stationIdToInterfacesMap;
    const idToStationMap = this.$idToStationMap;
    const updateInterfaceOrder = this.$updateInterfaceOrder.bind(this);
    const interfaceOrder = this.$unsavedInterfaceOrder;
    let selectedOrderItemKey = null;
    let selectedOrderItemIndex = null;
    const station = player.ship.dockedStation
    const stationId = idToStationMap.indexOf(station);
    const interfacesOnStation = stationId === -1 ? {} : stationIdToInterfacesMap[stationId];
    /*** ***/
    let interfaceOrderWasChanged = false;
    const saveChangesAndResetSelected = function () {
        selectedOrderItemKey = null;
        selectedOrderItemIndex = null;
        if (interfaceOrderWasChanged) {
            updateInterfaceOrder(interfaceOrder);
            interfaceOrderWasChanged = false;
        }
    };
    const cancelChangesAndResetSelected = function () {
        selectedOrderItemKey = null;
        selectedOrderItemIndex = null;
        if (interfaceOrderWasChanged) {        
            interfaceOrder.length = 0;
            Array.prototype.push.apply(interfaceOrder, mainScript.$interfaceOrder)
            interfaceOrderWasChanged = false;
        }
        this.screenParameters.initialChoicesKey = undefined;
    }
    const resetOrderList = function () {
        selectedOrderItemIndex = null;
        selectedOrderItemKey = null;
        interfaceOrder.length = 0;
        interfaceOrder.push({ key: "InterfaceReordering_OTHER", name: "<Other interfaces>", type: "" });
        interfaceOrderWasChanged = true;
        screens.interfaceOrderList.runPageWithLineIndex(0);
    };
    const formatToReorderingListColumns = function (no, name, type) {
        return "" +
            ILib.$padTextAround(no, 4) +
            ILib.$padTextOnSide(name, "right", 22) +                
            ILib.$padTextAround("", 1) +
            ILib.$padTextOnSide(type, "right", 5)
    }
    const updateListItem = function () {
        let item = interfaceOrder[this.data_index];
        let isSelected = this.data_index === selectedOrderItemIndex;
        //log("InterfaceReordering_Message", "Updated line to \"" + "Name " + item.name + " Type " + item.type + "\"");
        return {
            text: formatToReorderingListColumns(this.data_index + 1, item.name, item.type),
            color: isSelected ? "yellowColor" : "default",
            alignment: "LEFT"
        }
    }
    const selectListItem = function () {
        let item = interfaceOrder[this.data_index];
        selectedOrderItemKey = item.key;
        selectedOrderItemIndex = this.data_index;
        screens.moveItem.screenParameters.initialChoicesKey = undefined;
        screens.moveItem.runPageWithLineIndex(this.data_index);
        return true;
    }
    const spacerLine = new ILib.EmptyLine();
    const delimiterLine = new ILib.LabelLine(ILib.$repeatPattern("-", 32));
    const interfaceOrderListHeaderLine = new ILib.LabelLine(formatToReorderingListColumns("#", "Name", "Type"));
    const interfaceOrderListScreenParameters = {
        title: "Reorder interfaces",
        screenID: "InterfaceReordering_Main",
        allowInterrupt: true,
        exitScreen: "GUI_SCREEN_INTERFACES"
    }
    const interfaceOrderListScreen = new ILib.PagedScreenController(interfaceOrderListScreenParameters);
    interfaceOrderListScreen: {
        this.$screens.main = interfaceOrderListScreen;
        this.$screens.interfaceOrderList = interfaceOrderListScreen;
        interfaceOrderListScreen.addToHeader(interfaceOrderListHeaderLine);
        interfaceOrderListScreen.addToHeader(delimiterLine);
        interfaceOrderListScreen.addToFooter(delimiterLine);
        interfaceOrderListScreen.addToFooter(new ILib.ButtonLine("Save changes")
            .onUpdate(function () {
                return {
                    color: "yellowColor", // selectable color
                    unselectable: !interfaceOrderWasChanged
                };
            })
            .onSelected(saveChangesAndResetSelected)
        );
        interfaceOrderListScreen.addToFooter(new ILib.ButtonLine("Add interface")
            .onSelected(function () {
                screens.addInterface.runScreen();
                return true;
            })
        );
        interfaceOrderListScreen.addToFooter(new ILib.ButtonLine("Add category")
            .onSelected(function () {
                screens.addCategory.runScreen();
                return true;
            })
        );
        interfaceOrderListScreen.addToFooter(new ILib.ButtonLine("Reset")
            .onSelected(resetOrderList)
        );
        interfaceOrderListScreen.addToFooter(spacerLine);
        const updateReorderingListScreen = function updateReorderingListScreen() {
            this.clearContent(); // this === interfaceOrderListScreen;
            const reorderListCount = interfaceOrder.length;
            for (let i = 0; i < reorderListCount; i++) {
                let line = new ILib.ButtonLine("Line")
                    .onUpdate(updateListItem)
                    .onSelected(selectListItem);
                line.data_index = i;
                this.addToContent(line);
            }
        }
        interfaceOrderListScreen.onRun(updateReorderingListScreen);
        interfaceOrderListScreen.onReturn(cancelChangesAndResetSelected);
    }
    const returnToOrderList = interfaceOrderListScreen.returnToScreen.bind(interfaceOrderListScreen);
    const moveItemScreen = new ILib.PagedScreenController(interfaceOrderListScreenParameters);
    moveItemScreen: {
        this.$screens.moveItem = moveItemScreen;
        moveItemScreen.addToHeader(interfaceOrderListHeaderLine);
        moveItemScreen.addToHeader(delimiterLine);
        moveItemScreen.addToFooter(delimiterLine);
        moveItemScreen.addToFooter(new ILib.ButtonLine("Deselect")
            .onSelected(function () {
                selectedOrderItemKey = null;
                selectedOrderItemIndex = null;
                interfaceOrderListScreen.screenParameters.initialChoicesKey = interfaceOrderListScreen.selectedChoicesKey;
                interfaceOrderListScreen.runPage(moveItemScreen.currentPage);
                return true;
            })
        );
        moveItemScreen.addToFooter(new ILib.ButtonLine("Move up")
            .onUpdate(function () {
                return {
                    unselectable: interfaceOrder.length <= 1
                };
            })
            .onSelected(function () {
                let minPosition = 0;
                let newPosition = selectedOrderItemIndex - 1;
                if (newPosition < minPosition)
                    newPosition = interfaceOrder.length - 1;
                let temp = interfaceOrder[selectedOrderItemIndex];
                interfaceOrder[selectedOrderItemIndex] = interfaceOrder[newPosition];
                interfaceOrder[newPosition] = temp;
                selectedOrderItemIndex = newPosition;
                interfaceOrderWasChanged = true;
                moveItemScreen.runPageWithLineIndex(selectedOrderItemIndex, true);
                return true;
            })
        );
        moveItemScreen.addToFooter(new ILib.ButtonLine("Move down")
            .onUpdate(function () {
                return {
                    unselectable: interfaceOrder.length <= 1
                };
            })
            .onSelected(function () {
                let maxPosition = interfaceOrder.length - 1;
                let newPosition = selectedOrderItemIndex + 1;
                if (newPosition > maxPosition)
                    newPosition = 0;
                let temp = interfaceOrder[selectedOrderItemIndex];
                interfaceOrder[selectedOrderItemIndex] = interfaceOrder[newPosition];
                interfaceOrder[newPosition] = temp;
                selectedOrderItemIndex = newPosition;
                interfaceOrderWasChanged = true;
                moveItemScreen.runPageWithLineIndex(selectedOrderItemIndex, true);
                return true;
            })
        );
        moveItemScreen.addToFooter(new ILib.ButtonLine("Remove")
            .onUpdate(function () {
                return {
                    unselectable: selectedOrderItemKey === "InterfaceReordering_OTHER"
                }
            })
            .onSelected(function () {
                if (selectedOrderItemKey !== "InterfaceReordering_OTHER") {
                    interfaceOrder.splice(selectedOrderItemIndex, 1);
                    let returnIndex = selectedOrderItemIndex === 1 ? 0 : selectedOrderItemIndex - 1;
                    selectedOrderItemKey = null;
                    selectedOrderItemIndex = null;
                    interfaceOrderWasChanged = true;
                    interfaceOrderListScreen.runPageWithLineIndex(returnIndex);
                    return true;
                }
            })
        );
        moveItemScreen.addToFooter(spacerLine);
        const updateMoveItemScreen = function updateMoveItemScreen() {
            this.clearContent(); // this === updateMoveItemScreen;
            const reorderListCount = interfaceOrder.length;
            for (let i = 0; i < reorderListCount; i++) {
                let line = new ILib.LabelLine("Line")
                    .onUpdate(updateListItem);
                line.data_index = i;
                this.addToContent(line);
            }
        }
        moveItemScreen.onRun(updateMoveItemScreen);
        moveItemScreen.onReturn(cancelChangesAndResetSelected);
    }
    /*** Adding order items to main list ***/
    const addItemAndReturnToList = function (pair, type) {
        const newItemIndex = interfaceOrder.length;
        interfaceOrder.push({ key: pair.key, name: pair.name, type: type });
        interfaceOrderWasChanged = true;
        screens.interfaceOrderList.screenParameters.initialChoicesKey = screens.interfaceOrderList.selectedChoicesKey;
        screens.interfaceOrderList.runPageWithLineIndex(newItemIndex);
        return true;
    }
    const addAllItemsAndReturnToList = function (items, type) {
        let lastItemIndex;
        for (let i = 0; i < items.length; i++) {
            let pair = items[i];
            lastItemIndex = interfaceOrder.length;
            interfaceOrder.push({ key: pair.key, name: pair.name, type: type });
        }
        interfaceOrderWasChanged = true;
        screens.interfaceOrderList.screenParameters.initialChoicesKey = screens.interfaceOrderList.selectedChoicesKey;
        screens.interfaceOrderList.runPageWithLineIndex(lastItemIndex);
        return true;
    }
    const updateItems = function updateItems(type, makePairFunc) {
        const itemsInUse = interfaceOrder.reduce(function (accum, orderItem) {
            if (orderItem.type === type)
                accum[orderItem.key] = 1;
            return accum;
        }, {});
        this.clearContent();
        let noItemsToAdd = true;
        const items = [];
        for (let key in interfacesOnStation) {
            let id = interfacesOnStation[key];
            if (id === null)
                continue;
            const pair = makePairFunc(key, id);
            if (itemsInUse[pair.key])
                continue;
            itemsInUse[pair.key] = 1;
            let line = new ILib.ButtonLine(pair.name).onSelected(function () {
                return addItemAndReturnToList(this.data_pair, this.data_type)
            });
            line.data_pair = pair;
            line.data_type = type;
            this.addToContent(line);
            noItemsToAdd = false;
            items.push(pair);
        }
        if (noItemsToAdd)
            this.addToContent(new ILib.LabelLine("No items to add", "CENTER"));
        this.data_items = items;
    }
    addInterfaceScreen: {
        const addInterfaceScreen = this.$screens.addInterface = new ILib.PagedScreenController({
            title: "Add interface",
            screenID: "InterfaceReordering_AddInterface",
            allowInterrupt: true,
            exitScreen: "GUI_SCREEN_INTERFACES"
        }, returnToOrderList)
            .onRun(function () {
                updateItems.call(this, "Interface", function (key, id) {
                    return { key: key, name: id.title };
                });
            });
        addInterfaceScreen.addToHeader(delimiterLine);
        addInterfaceScreen.addToFooter(delimiterLine);
        addInterfaceScreen.addToFooter(new ILib.ButtonLine("Add all")
            .onUpdate(function () {
                let isItemsToAdd = addInterfaceScreen.data_items &&
                    addInterfaceScreen.data_items.length > 0;
                return {
                    unselectable: !isItemsToAdd
                };
            })
            .onSelected(function () {
                const items = addInterfaceScreen.data_items;
                return addAllItemsAndReturnToList.call(this, items, "Interface")
            })
        );
    }
    addCategoryScreen: {
        const addCategoryScreen = this.$screens.addCategory = new ILib.PagedScreenController({
            title: "Add category",
            screenID: "InterfaceReordering_AddCategory",
            allowInterrupt: true,
            exitScreen: "GUI_SCREEN_INTERFACES"
        }, returnToOrderList)
            .onRun(function () {
                updateItems.call(this, "Category", function (key, id) {
                    return { key: id.category, name: id.category };
                });
            });
        addCategoryScreen.addToHeader(delimiterLine);
        addCategoryScreen.addToFooter(delimiterLine);
        addCategoryScreen.addToFooter(new ILib.ButtonLine("Add all")
            .onUpdate(function () {
                let isItemsToAdd = addCategoryScreen.data_items &&
                    addCategoryScreen.data_items.length > 0;
                return {
                    unselectable: !isItemsToAdd
                };
            })
            .onSelected(function () {
                const items = addCategoryScreen.data_items;
                return addAllItemsAndReturnToList.call(this, items, "Category")
            })
        );
    }
}
// Order updating //
this.$updateInterfaceOrder = function $updateInterfaceOrder(newReorderings) {
    const originalSetInterface = this.$originalSetInterface;
    for (let stationId = 0; stationId < this.$stationIdToInterfacesMap.length; stationId++) {
        let station = this.$idToStationMap[stationId];
        if (!station || !station.isValid) {
            this.$idToStationMap[stationId] = null;
            this.$stationIdToInterfacesMap[stationId] = null;
            continue;
        }
        let interfaces = this.$stationIdToInterfacesMap[stationId];
        for (let key in interfaces) {
            originalSetInterface.call(station, key, null); // hide all
        }
    }
    this.$interfacePrefixes = this.$makeInterfacePrefixes(newReorderings);
    this.$interfaceOrder = newReorderings.slice(); // NOT SPLICE
    this.$unsavedInterfaceOrder.length = 0;
    Array.prototype.push.apply(this.$unsavedInterfaceOrder, this.$interfaceOrder);
    log("InterfaceReordering_Message", "New prefixes set: " + JSON.stringify(this.$interfacePrefixes));
    for (let stationId = 0; stationId < this.$stationIdToInterfacesMap.length; stationId++) {
        let station = this.$idToStationMap[stationId];
        if (!station)
            continue;
        let interfaces = this.$stationIdToInterfacesMap[stationId];
        for (let key in interfaces) {
            let interfaceDefinition = interfaces[key];
            station.setInterface(key, interfaceDefinition); // show with new rules
        }
    }
}
this.$makeInterfacePrefixes = function $makeInterfacePrefixes(interfaceOrder) {
    return this.$makeInterfacePrefixes_V1(interfaceOrder);
}
/// Use zero width and hair spaces, on some computers they even don't display as "?"
this.$makeInterfacePrefixes_V0 = function $makeInterfacePrefixes_V0(interfaceOrder) {
    // 0x200A - hair space character
    // 0x200B - zero width character
    const markerBeforeOthers = String.fromCharCode(0x200A) + String.fromCharCode(0x200B) + String.fromCharCode(0x200B);
    const markerOthers = String.fromCharCode(0x200B) + String.fromCharCode(0x200A) + String.fromCharCode(0x200B);
    const markerAfterOthers = String.fromCharCode(0x200B) + String.fromCharCode(0x200B) + String.fromCharCode(0x200A);
    const markerDepth = String.fromCharCode(0x200B);
    const interfacePrefixes = {/* key: prefix */ };
    let beforeOthers = true;
    let depthLevel = 1;
    for (let i = 0; i < interfaceOrder.length; i++) {
        let orderItem = interfaceOrder[i];
        if (orderItem.type === "") {
            beforeOthers = false;
            depthLevel = 1;
        }
        else if (beforeOthers) {
            interfacePrefixes[orderItem.key] = markerBeforeOthers + new Array(depthLevel).join(markerDepth);
        }
        else {
            interfacePrefixes[orderItem.key] = markerAfterOthers + new Array(depthLevel).join(markerDepth);
        }
        depthLevel += 1;
    }
    interfacePrefixes["InterfaceReordering_OtherPrefix"] = markerOthers;
    return interfacePrefixes;
}
this.$makeInterfacePrefixes_V1 = function $makeInterfacePrefixes_V1(interfaceOrder) {
    const markerBeforeOthers = String.fromCharCode(0x1F, 0x1F);
    const markerOthers = String.fromCharCode(0x1F, 0x200A);
    const markerAfterOthers = String.fromCharCode(0x200A, 0x200A);
    const markerDepth = String.fromCharCode(0x200A);
    const interfacePrefixes = {/* key: prefix */ };
    let beforeOthers = true;
    let depthLevel = 1;
    for (let i = 0; i < interfaceOrder.length; i++) {
        let orderItem = interfaceOrder[i];
        if (orderItem.type === "") {
            beforeOthers = false;
            depthLevel = 1;
        }
        else if (beforeOthers) {
            interfacePrefixes[orderItem.key] = markerBeforeOthers + new Array(depthLevel).join(markerDepth);
        }
        else {
            interfacePrefixes[orderItem.key] = markerAfterOthers + new Array(depthLevel).join(markerDepth);
        }
        depthLevel += 1;
    }
    interfacePrefixes["InterfaceReordering_OtherPrefix"] = markerOthers;
    return interfacePrefixes;
}
 |