ui.js

/**
 * @file ui.js
 * @description This file contains every fonction called via an event firing. Some fonctions are also called via user inputs on sub-menus.
 * 
 * Functions included in this file : 
 *  - **changeChannelButtonStatus**
 *  - **toggleDisplayForVerticalCursorScrollers + sub-functions**
 *  - **toggleDisplayForHorizontalCursorScrollers + sub-functions**
 *  - **changeScreenSize + sub-functions**
 *  - **changeScreenLightMode**
 *  - **setScrollersEvents + sub-functions**
 *  - **setupTriggerCursor + sub-functions**
 * 
 * @version 1.0.0
 * @since 2024-05-31
 * @author Owen Pichot
 * @license Public Domain
 */

/**
 * @module UI
 */

/**
 * This function executes whenever a user clicks on one of the channel buttons :
 * The button will be grayed out if it is already in focus.
 * The button will be 'activated/colored' if it is grayed out.
 * The button will be focused if not already focused.
 * 
 * @function changeChannelButtonStatus
 * @memberof module:UI
 * @param {string} channelKey Object key corresponding to a certain signal saved within 'channelData'.
 * @throws Will throw an error if the button clicked on is not linked to an active channel. We then warn the user.
 * @returns {void}
 */
function changeChannelButtonStatus(channelKey) {
    let button = document.getElementById(channelKey);

    // Here we make sure only one button can be focused
    Object.keys(channelData).forEach(key => {
        let otherButton = document.getElementById(key);
        otherButton.classList.remove('button-focused');
        
        if (key !== channelKey) {
            channelData[key].focused = false;
        }
    });


    try {//in case the channel clicked is not active (= not in the channelData dictionnary)
        if (!channelData[channelKey].focused) {//if channel is not focused, then
            button.classList.add('button-focused');
            channelData[channelKey].focused = true;

            //Here we also set the correct values for the vertical scaling of this channel to the knob of the html page
            document.getElementById('vertical-scaling').value = channelData[channelKey].verticalScale;
            //here we update the z-index of that cursor to make it easy for the user to interact with.
            Object.keys(channelData).forEach(key => {
                if (key == channelKey){
                    document.getElementById('scroller-' + channelKey).style.zIndex = 50000;
                }else{
                    document.getElementById('scroller-' + key).style.zIndex = 10000;
                }
            })
            
            // If channel is not displayed, display it
            if (!channelData[channelKey].display) {
                button.classList.remove("channel-not-displayed");
                button.classList.add("channel-displayed");
                button.classList.add(channelData[channelKey].colorDark);
                channelData[channelKey].display = true;
                document.getElementById('scroller-' + channelKey).style.display = 'block';
                document.getElementById('scroller-' + channelKey).style.top = channelData[channelKey].verticalOffsetRelativeCursorPosition;
            }
        } else {
            if (channelData[channelKey].display) {//If button was focused, remove its styles.
                button.classList.remove("channel-displayed");
                button.classList.add("channel-not-displayed");
                button.classList.remove(channelData[channelKey].colorDark);
                channelData[channelKey].display = false;

                document.getElementById('scroller-' + channelKey).style.display = 'none';
            }
            // remove the focus since the button was clicked a second time
            button.classList.remove('button-focused');
            channelData[channelKey].focused = false;
        }
    } catch (error) {
        if (error instanceof TypeError) {//Button not linked to an active channel
            console.error(error)
            showToast("This channel is not active.", "toast-error");

        } else {
            alert("An unknown error occured, please look at the console.")
            console.error(error);
        }
    }
    

    // console.log(channelData);
};

/**
 * @type {boolean}
 * @description This variable is necessary for the function 'toggleDisplayForVerticalCursorScrollers' in order to prevent adding multiple listeners onto the same element.
 */
let isVerticalMouseDownListenerSet = false;
/**
 * @type {boolean}
 * @description This variable is necessary for the function 'toggleDisplayForHorizontalCursorScrollers' in order to prevent adding multiple listeners onto the same element.
 */
let isHorizontalMouseDownListenerSet = false;

/**
 * This function is fired when a user chooses to display the vertical cursors.
 * It will start by displaying the corresponding cursors and assign event listeners to them.
 * If the cursors have been previously hidden then showned again, the listeners won't be set again.
 * 
 * @function toggleDisplayForVerticalCursorScrollers
 * @memberof module:UI
 * @returns {void}
 */
function toggleDisplayForVerticalCursorScrollers(){
    const scrollerA = document.getElementById("vertical-scroller-A");
    const scrollerB = document.getElementById("vertical-scroller-B");
    const scrollBar = document.getElementById("scrollbar-horizontal");
    let isDragging = false;
    let currentMoveListener = null;
    let currentUpListener = null;

    /**
     * This fonction keeps track of a moving vertical cursor being dragged by a user.
     * We get the cursor's position anytime it is being moved and save these values to the config relative to the cursors.
     * 
     * @function onMouseMoveScrollerVertical
     * @memberof module:UI
     * @param {object} scroller Scroller element on the top of the screen.
     * @param {number} startX Position from where the cursor started moving (abs value in pixels). 
     * @param {String} whichCursor Which of the 2 scrollers are we moving.
     * @listens MouseMove For a user dragging the mouse.
     * @returns {function} Returns the entire fonction in order to assign it to a variable and easily keep track of the listeners.
     */
    function onMouseMoveScrollerVertical(scroller, startX, whichCursor){
        return function(event) {
            let newX = event.clientX - startX;
            newX = Math.max(newX, 0);
            newX = Math.min(newX, scrollBar.clientWidth - scroller.clientWidth);
            scroller.style.left = newX + 'px';
            if (whichCursor == "A"){
                cursorOptions.verticalAPosition = newX + ((parseInt(window.getComputedStyle(scrollerA).width)) / 2);
            }else{
                cursorOptions.verticalBPosition = newX + ((parseInt(window.getComputedStyle(scrollerB).width)) / 2);
            }
        };
    }

    /**
     * This fonction handles the final part of a moving vertical cursor which is when a user finally releases it.
     * We then remove all listeners assigned to this cursor and reset the variables responsible for keeping track of the events.
     * 
     * @function onMouseUpScrollerVertical
     * @memberof module:UI
     * @listens Mouseup For a user releasing the mouse.
     * @returns  {void}
     */
    function onMouseUpScrollerVertical(){
        if (!isDragging) return;
        isDragging = false;

        document.removeEventListener('mousemove', currentMoveListener);
        document.removeEventListener('mouseup', currentUpListener);
        currentMoveListener = null;
        currentUpListener = null;
    }

    /**
     * This functions sets up listeners to allow the user to move one of the two vertical cursors by dragging it along the scrollbar.
     * 
     * @function setupDragListeners
     * @memberof module:UI
     * @param {Object} scroller Scroller element on top of the screen.
     * @param {string} whichCursor Which of the two vertical cursors we are sending (A / B).
     * @returns {void}
     * 
     * @listens Mousedown For a user clicking on one of the vertical measure cursors. 
     * When a user clicks on one, we attach two new listeners to the cursor to track where it is dragged to and when the user relases the mouse.
     */
    function setupDragListeners(scroller, whichCursor) {
        scroller.addEventListener('mousedown', function(event) {
            if (isDragging) return; // Prevents adding multiple listeners during an active drag
            isDragging = true;
            let startX = event.clientX - scroller.getBoundingClientRect().left + scrollBar.getBoundingClientRect().left;

            currentMoveListener = onMouseMoveScrollerVertical(scroller, startX, whichCursor);
            currentUpListener = onMouseUpScrollerVertical;

            document.addEventListener('mousemove', currentMoveListener);
            document.addEventListener('mouseup', currentUpListener);
        });
    }

    if (cursorOptions.isVerticalCursorOn === "true"){
        scrollerA.style.display = "block";
        scrollerA.style.left = (cursorOptions.verticalAPosition - (parseInt(window.getComputedStyle(scrollerA).width)) / 2) + "px";
    
        scrollerB.style.display = "block";
        scrollerB.style.left = (cursorOptions.verticalBPosition - (parseInt(window.getComputedStyle(scrollerB).width)) / 2) + "px";

        if (!isVerticalMouseDownListenerSet){
            setupDragListeners(scrollerA, "A");
            setupDragListeners(scrollerB, "B");
        }

        isVerticalMouseDownListenerSet = true;
    } else {
        // Hide scrollers (A & B)
        scrollerA.style.display = "none";
        scrollerB.style.display = "none";
    }
};

/**
 * Similarly to 'toggleDisplayForVerticalCursorScrollers' this function is fired when a user chooses to display the horizontal cursors.
 * It will start by displaying the corresponding cursors and assign event listeners to them.
 * If the cursors have been previously hidden then showned again, the listeners won't be set again.
 * 
 * @function toggleDisplayForHorizontalCursorScrollers
 * @memberof module:UI
 * @returns {void}
 */
function toggleDisplayForHorizontalCursorScrollers(){
    const scrollerA = document.getElementById("scroller-horizontal-A");
    const scrollerB = document.getElementById("scroller-horizontal-B");
    const scrollBar = document.getElementById("scroll-bar-horizontal-cursors");
    let isDragging = false;
    let currentMoveListener = null;
    let currentUpListener = null;

    /**
     * This fonction keeps track of a moving horizontal cursor being dragged by a user.
     * We get the cursor's position anytime it is being moved and save these values to the config relative to the cursors.
     * 
     * @function onMouseMoveScrollerHorizontal
     * @memberof module:UI
     * @param {object} scroller Scroller element on the top of the screen.
     * @param {number} startY Position from where the cursor started moving (abs value in pixels).
     * @param {string} whichCursor Which of the 2 scrollers are we moving.
     * @listens MouseMove For a user dragging the mouse.
     * @returns {function} Returns the entire fonction in order to assign it to a variable and easily keep track of the listeners.
     */
    function onMouseMoveScrollerHorizontal(scroller, startY, whichCursor){
        return function(event) {
            let newY = event.clientY - startY;
            newY = Math.max(newY, 0);
            newY = Math.min(newY, scrollBar.clientHeight - scroller.clientHeight);
            scroller.style.top = newY + 'px';
            if (whichCursor == "A"){
                cursorOptions.horizontalAPosition = newY + ((parseInt(window.getComputedStyle(scrollerA).height)) / 2);
            }else{
                cursorOptions.horizontalBPosition = newY + ((parseInt(window.getComputedStyle(scrollerA).height)) / 2);
            }
        };
    };

    /**
     * This fonction handles the final part of a moving horizontal cursor which is when a user finally releases it.
     * We then remove all listeners assigned to this cursor and reset the variables responsible for keeping track of the events.
     * 
     * @function onMouseUpScrollerHorizontal
     * @memberof module:UI
     * @listens Mouseup For a user releasing the mouse.
     * @returns  {void}
     */
    function onMouseUpScrollerHorizontal(){
        if (!isDragging) return;
        isDragging = false;

        document.removeEventListener('mousemove', currentMoveListener);
        document.removeEventListener('mouseup', currentUpListener);
        currentMoveListener = null;
        currentUpListener = null;
    };

    /**
     * This functions sets up listeners to allow the user to move one of the two horizontal cursors by dragging it along the scrollbar.
     * 
     * @function setupDragListeners
     * @memberof module:UI
     * @param {Object} scroller Scroller element on top of the screen.
     * @param {string} whichCursor Which of the two horizontal cursors we are sending (A / B).
     * @returns {void}
     * 
     * @listens Mousedown For a user clicking on one of the horizontal measure cursors. 
     * When a user clicks on one, we attach two new listeners to the cursor to track where it is dragged to and when the user relases the mouse.
     */
    function setupDragListeners(scroller, whichCursor) {
        scroller.addEventListener('mousedown', function(event) {
            if (isDragging) return; // Prevents adding multiple listeners during an active drag
            isDragging = true;
            let startY = event.clientY - scroller.getBoundingClientRect().top + scrollBar.getBoundingClientRect().top;

            currentMoveListener = onMouseMoveScrollerHorizontal(scroller, startY, whichCursor);
            currentUpListener = onMouseUpScrollerHorizontal;

            document.addEventListener('mousemove', currentMoveListener);
            document.addEventListener('mouseup', currentUpListener);
        });
    }

    if (cursorOptions.isHorizontalCursorOn === "true"){
        scrollerA.style.display = "block";
        scrollerA.style.top = (cursorOptions.horizontalAPosition - (parseInt(window.getComputedStyle(scrollerA).height)) / 2) + "px";
    
        scrollerB.style.display = "block";
        scrollerB.style.top = (cursorOptions.horizontalBPosition - (parseInt(window.getComputedStyle(scrollerB).height)) / 2) + "px";

        if (!isHorizontalMouseDownListenerSet){
            setupDragListeners(scrollerA, "A");
            setupDragListeners(scrollerB, "B");
        }
        isHorizontalMouseDownListenerSet = true;
    } else {
        // Hide scrollers (A & B)
        scrollerA.style.display = "none";
        scrollerB.style.display = "none";
    }
};

/**
 * This function resizes the oscilloscope's screen.
 * It will also move around some elements and/or resize them to make them fit within the new layout of the page.
 * 
 * @function changeScreenSize
 * @memberof module:UI
 * @param {string} size Size, in pixels, to assign to the canvas (width|height).
 * @returns {void}
 * @listens Escape When the screen is maximized, part of this function will listen for the 'Escape' key being pressed and reset the view back to standard.
 * 
 * @example
 * changeScreenSize("1200|800");
 * changeScreenSize("400|267");
 */
function changeScreenSize(size){
    const width = parseInt(size.split("|")[0]);
    const height = parseInt(size.split("|")[1]);

    CANVAS.width = width;
    CANVAS.height = height;

    const horizontalScrollBar = document.getElementById("scrollbar-horizontal");
    const leftVerticalScrollBar = document.getElementById("scroll-bar");
    const rightVerticalScrollBar = document.getElementById("scroll-bar-horizontal-cursors");
    const scrollerHorizontal = document.getElementById("scroller-Horizontal");
    const scrollers = document.querySelectorAll(".scrollers")
    const channelButtons = document.querySelectorAll(".ch-button");
    const channelButtonsContainer = document.getElementById("channel-select-div");
    const functionButtons = document.querySelectorAll(".function-buttons");
    const measurementsHolder = document.getElementById("info-display-oscilloscope");

    horizontalScrollBar.style.width = width + "px";
    leftVerticalScrollBar.style.height = height + "px";
    rightVerticalScrollBar.style.height = height + "px";
    scrollerHorizontal.style.left = (width / 2) - 10 + "px";
    scrollers.forEach(scroller => {
        scroller.style.top = (height / 2) - 5 + "px";
    });

    if (cursorOptions.isHorizontalCursorOn && size != "TD"){
        cursorOptions.horizontalAPosition = CANVAS.height / 3;
        cursorOptions.horizontalBPosition = (CANVAS.height / 3) * 2;
        document.getElementById("scroller-horizontal-A").style.top = (cursorOptions.horizontalAPosition - 5) + 'px';
        document.getElementById("scroller-horizontal-B").style.top = (cursorOptions.horizontalBPosition - 5) + 'px';
    }

    if (cursorOptions.isVerticalCursorOn && size != "TD"){
        cursorOptions.verticalAPosition = CANVAS.width / 3;
        cursorOptions.verticalBPosition = (CANVAS.width / 3) * 2;
        document.getElementById("vertical-scroller-A").style.left = (cursorOptions.verticalAPosition - 5) + 'px';
        document.getElementById("vertical-scroller-B").style.left = (cursorOptions.verticalBPosition - 5) + 'px';
    }

    function setTinyScreenSize(){//Set specific styles for the tiny screen.
        channelButtons.forEach(button => {
            button.style.width = "20px";
            button.style.height = "10px";
            button.style.fontSize = "10px";
            button.style.lineHeight = "3px";
            button.firstElementChild.style.position = "absolute";
            button.firstElementChild.style.left = "8px";
        });

        functionButtons.forEach(button => {
            button.style.width = "105px";
            button.style.height = "20";
            button.style.fontSize = "13px";
            button.style.lineHeight = "5px";
        });

        channelButtonsContainer.style.margin = "1em auto";

        measurementsHolder.style.flexDirection = "column";

        document.getElementById("vpdiv-mesurements").style.width = "fit-content";
        document.getElementById("vpdiv-mesurements").style.margin = "0px";

        document.querySelectorAll(".chanmeasures").forEach(p => {
            p.style.width = "fit-content";
        });

        document.getElementById("mathres-measurements").style.display = "none";
    };

    function setSmallScreenSize(){//Set specific styles for the small screen.
        channelButtons.forEach(button => {
            button.style.width = "40px";
            button.style.height = "15px";
            button.style.fontSize = "15px";
            button.style.lineHeight = "3px";
            button.firstElementChild.style.position = "absolute";
            button.firstElementChild.style.left = "13px";
        });

        functionButtons.forEach(button => {
            button.style.width = "150px";
            button.style.height = "40px";
            button.style.fontSize = "18px";
            button.style.lineHeight = "5px";
        });

        channelButtonsContainer.style.margin = "1em auto";

        measurementsHolder.style.flexDirection = "column";

        document.getElementById("vpdiv-mesurements").style.width = "fit-content";
        document.getElementById("vpdiv-mesurements").style.margin = "0px";

        document.querySelectorAll(".chanmeasures").forEach(p => {
            p.style.width = "fit-content";
        });

        document.getElementById("mathres-measurements").style.display = "none";
        document.getElementById("tpdiv-measurement").width = "fit-content";
        document.getElementById("tpdiv-measurement").margin = "0px";

    };

    function setStandardScreenSize(){//Set specific styles for the standard screen.
        channelButtons.forEach(button => {
            button.style.width = "";
            button.style.height = "";
            button.style.fontSize = "";
            button.style.lineHeight = "";
            button.firstElementChild.style.position = "";
            button.firstElementChild.style.left = "";
        });

        functionButtons.forEach(button => {
            button.style.width = "";
            button.style.height = "";
            button.style.fontSize = "";
            button.style.lineHeight = "";
        });

        channelButtonsContainer.style.margin = "";

        measurementsHolder.style.flexDirection = "row";

        document.getElementById("vpdiv-mesurements").style.width = "";
        document.getElementById("vpdiv-mesurements").style.margin = "";

        document.querySelectorAll(".chanmeasures").forEach(p => {
            p.style.width = "";
        });

        document.getElementById("mathres-measurements").style.display = "flex";
        document.getElementById("tpdiv-measurement").width = "";
        document.getElementById("tpdiv-measurement").margin = "";
    };

    function setLargeScreenSize(){//Set specific styles for the large screen.
        document.querySelectorAll("#channel-select-div span").forEach(span => {
            span.style.flexWrap = "wrap";
            span.style.justifyContent = "space-evenly";
        });

        channelButtons.forEach(button => {
            button.style.width = "";
            button.style.height = "";
            button.style.fontSize = "";
            button.style.lineHeight = "";
            button.firstElementChild.style.position = "";
            button.firstElementChild.style.left = "";
            button.style.margin = "0px 0.2em 1em 0.2em"
        });

        functionButtons.forEach(button => {
            button.style.width = "";
            button.style.height = "";
            button.style.fontSize = "";
            button.style.lineHeight = "";
        });

        channelButtonsContainer.style.margin = "";

        measurementsHolder.style.flexDirection = "row";

        document.getElementById("vpdiv-mesurements").style.width = "";
        document.getElementById("vpdiv-mesurements").style.margin = "";

        document.querySelectorAll(".chanmeasures").forEach(p => {
            p.style.width = "";
        });

        document.getElementById("mathres-measurements").style.display = "flex";
        document.getElementById("tpdiv-measurement").width = "";
        document.getElementById("tpdiv-measurement").margin = "";
    };

    function exitFullScreen(event){//Function associated to a listener to allow the user to quit the fullscreen mode.
        if (event.key === "Escape"){
            CANVAS.width = 1200;
            CANVAS.height = 800;

            document.getElementById("aside").style.display = "flex";

            horizontalScrollBar.style.width = CANVAS.width + "px";
            leftVerticalScrollBar.style.height = CANVAS.height + "px";
            rightVerticalScrollBar.style.height = CANVAS.height + "px";
            scrollerHorizontal.style.left = (CANVAS.width / 2) - 10 + "px";
            scrollers.forEach(scroller => {
                scroller.style.top = (CANVAS.height / 2) - 5 + "px";
            })

            document.getElementById("info-display-oscilloscope").style.display = "flex";
            document.getElementById("auto-measures-display").style.display = "flex";

            setStandardScreenSize();

            document.removeEventListener("keydown", exitFullScreen);
            showToast("Back to standard screen size.", "toast-info");

            clearCanvas();
            drawGrid('rgba(128, 128, 128, 0.5)', 3);
        }
    }

    function setMaximizedScreenSize(){//Set specific styles for the maximized screen.
        const bodyWidth = document.body.clientWidth;
        const bodyHeight = document.body.clientHeight;

        CANVAS.width = bodyWidth - 40;
        CANVAS.height = bodyHeight - 20;

        document.getElementById("aside").style.display = "none";
        document.getElementById("middle-line").style.marginLeft = "0px";

        horizontalScrollBar.style.width = CANVAS.width + "px";
        leftVerticalScrollBar.style.height = CANVAS.height + "px";
        rightVerticalScrollBar.style.height = CANVAS.height + "px";
        scrollerHorizontal.style.left = (CANVAS.width / 2) - 10 + "px";
        scrollers.forEach(scroller => {
            scroller.style.top = (CANVAS.height / 2) - 5 + "px";
        });

        if (cursorOptions.isHorizontalCursorOn){
            cursorOptions.horizontalAPosition = CANVAS.height / 3;
            cursorOptions.horizontalBPosition = (CANVAS.height / 3) * 2;
            document.getElementById("scroller-horizontal-A").style.top = (cursorOptions.horizontalAPosition - 5) + 'px';
            document.getElementById("scroller-horizontal-B").style.top = (cursorOptions.horizontalBPosition - 5) + 'px';
        }
        if (cursorOptions.isVerticalCursorOn){
            cursorOptions.verticalAPosition = CANVAS.width / 3;
            cursorOptions.verticalBPosition = (CANVAS.width / 3) * 2;
            document.getElementById("vertical-scroller-A").style.left = (cursorOptions.verticalAPosition - 5) + 'px';
            document.getElementById("vertical-scroller-B").style.left = (cursorOptions.verticalBPosition - 5) + 'px';
        }

        document.getElementById("info-display-oscilloscope").style.display = "none";
        document.getElementById("auto-measures-display").style.display = "none";

        showToast("To escape full-screen, press 'ESC'", "toast-info");

        document.addEventListener("keydown", exitFullScreen);
    };

    if (size == "400|267"){
        setTinyScreenSize();
    }else if (size == "800|533"){
        setSmallScreenSize();
    }else if (size == "1200|800"){
        setStandardScreenSize();
    }else if (size == "1400|900"){
        setLargeScreenSize();
    }else if (size == "TD"){
        setMaximizedScreenSize();
    }

    clearCanvas();
    drawGrid('rgba(128, 128, 128, 0.5)', 3);
};

/**
 * This function works in two sections.
 * The first section is the immediate change of the UI depending on the theme the user changed.
 * The second section is there to save these changes in the UI to the database if the user is registered.
 * If the user is anonymous, a message will let him know that he needs an account to save his preferences.
 * 
 * @function changeScreenLightMode
 * @memberof module:UI
 * @returns {void}
 * @param {string} mode Which of the two modes to set (light / dark).
 * @example
 * changeScreenLightMode("light");
 * changeScreenLightMode("dark");
 */
function changeScreenLightMode(mode){
    if (mode == "light"){
        CANVAS.classList.remove("canvas-dark");
        CANVAS.classList.add("canvas-light");
        document.querySelectorAll(".canvas-scrollbars-dark").forEach(scrollbar => {
            scrollbar.classList.remove("canvas-scrollbars-dark");
            scrollbar.classList.add("canvas-scrollbars-light");
        });
    }else{
        CANVAS.classList.remove("canvas-light");
        CANVAS.classList.add("canvas-dark");
        document.querySelectorAll(".canvas-scrollbars-light").forEach(scrollbar => {
            scrollbar.classList.remove("canvas-scrollbars-light");
            scrollbar.classList.add("canvas-scrollbars-dark");
        });
    };

    Object.keys(channelData).forEach(key => {
        if (mode == "light"){
            document.getElementById(key).classList.remove(channelData[key].colorDark);
            document.getElementById(key).classList.add(channelData[key].colorLight);
            document.getElementById("scroller-"+key).style.backgroundColor = channelData[key].colorLight;
        }else{
            document.getElementById(key).classList.remove(channelData[key].colorLight);
            document.getElementById(key).classList.add(channelData[key].colorDark);
            document.getElementById("scroller-"+key).style.backgroundColor = channelData[key].colorDark;
        }
    });

    config.theme = mode;

    //Now we save the new theme to the db for later on.
    const UID = userId;
    const Http = new XMLHttpRequest();
    const url = `/oscillo/setThemePreference/${UID}/`;
    const csrfToken = document.querySelector('input[name="csrfmiddlewaretoken"]').value;

    Http.open("POST", url, true);
    Http.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
    Http.setRequestHeader("X-CSRFToken", csrfToken);

    const data = JSON.stringify({
        theme: mode,
    });

    Http.onreadystatechange = function() {
        if (Http.readyState === 4) {
            response = JSON.parse(Http.responseText);
            if (Http.status === 200) {
                console.log(response.message);
                showToast(response.message, "toast-success");
            } else {
                console.log("Error saving theme: ", response.message);
                showToast(response.message, "toast-error");
                return;
            }
        }
    };

    Http.send(data);
};

/**
 * This function is used to setup the interactions between the user and each of the vertical offset scrollers (left of the screen).
 * It needs to be called for each scroller.
 * 
 * @function setScrollersEvents
 * @memberof module:UI
 * @returns {void}
 * @param {number} Channel_ID Which channel are we setting an offset cursor for.
 * @example
 * setScrollersEvents(4);
 * @listens MouseMove - Whenever the scroller is moved, the new position of the scroller will be saved and its relative position is then mapped to an offset on the corresponding signal.
 */
function setScrollersEvents(Channel_ID){
    let isDragging = false;
    const scrollBar = document.getElementById("scroll-bar");
    const scroller = document.getElementById("scroller-CH"+Channel_ID);
    let startY;

    scroller.style.display = "block";
    scroller.style.backgroundColor = channelData["CH" + Channel_ID].colorDark;

    scroller.addEventListener('mousedown', function(event) {
        isDragging = true;
        startY = event.clientY - scroller.getBoundingClientRect().top + scrollBar.getBoundingClientRect().top;
        document.addEventListener('mousemove', onMouseMove);
        document.addEventListener('mouseup', onMouseUp);
    });

    function onMouseMove(event) {
        if (!isDragging) return;

        //this part handles the actual movement of the element on the screen
        let newY = event.clientY - startY;
        newY = Math.max(newY, 0);
        newY = Math.min(newY, scrollBar.clientHeight - scroller.clientHeight);

        scroller.style.top = newY + 'px';

        //this part maps the relative position of the scroller to the offset of the signal
        let percent = newY / (scrollBar.clientHeight - scroller.clientHeight);
        let verticalOffset = (percent - 0.5) * 1000;
        verticalOffset = Math.round(verticalOffset);

        //and now we actually update the vertical offset of the focused channel
        channelData["CH" + Channel_ID].verticalOffset = verticalOffset;
    };

    function onMouseUp(event) {
        isDragging = false;

        document.removeEventListener('mousemove', onMouseMove);
        document.removeEventListener('mouseup', onMouseUp);

        //Here below we save the cursor position for that channel to restore it when the user clicks on the channel again
        let newY = event.clientY - startY;
        newY = Math.max(newY, 0);
        newY = Math.min(newY, scrollBar.clientHeight - scroller.clientHeight);

        channelData["CH" + Channel_ID].verticalOffsetRelativeCursorPosition = newY;
    };
};

/**
 * This function is used to setup the interactions for the trigger cursor.
 * This cursor shows the user the current threshold set for the trigger and allows them to move it and therefore change the trigger threshold.
 * 
 * @function setupTriggerCursor
 * @memberof module:UI
 * @returns {void}
 * @listens MouseMove - Whenever the scroller is moved, the new position of the scroller will be saved and its relative position is then mapped to an offset on the corresponding signal.
 */
function setupTriggerCursor(){
    let  startY;
    let isDragging = false;

    const scrollBar = document.getElementById("scroll-bar-horizontal-cursors");
    const TriggerCursor = document.getElementById("trigger-cursor");

    TriggerCursor.addEventListener('mousedown', function(event) {
        isDragging = true;
        startY = event.clientY - TriggerCursor.getBoundingClientRect().top + scrollBar.getBoundingClientRect().top;
        document.addEventListener('mousemove', onMouseMove);
        document.addEventListener('mouseup', onMouseUp);
    });

    function onMouseMove(event){
        if (!isDragging) return;

        let newY = event.clientY - startY;
        newY = Math.max(newY, 0);
        newY = Math.min(newY, scrollBar.clientHeight - TriggerCursor.clientHeight);

        TriggerCursor.style.top = newY + 'px';

        let cursorPosToMilliVolts = parseFloat(getMilliVoltsRelativeToTriggerCursor(newY).value);
        triggerOptions.triggerLevel = cursorPosToMilliVolts;
    }

    function onMouseUp(event){
        isDragging = false;

        document.removeEventListener('mousemove', onMouseMove);
        document.removeEventListener('mouseup', onMouseUp);
    }
};