loading...

Creating a text editor in Electron: part 3 - Setting Preferences

aurelkurtula profile image aurel kurtula Updated on ・7 min read

Creating a text editor in Electron (3 Part Series)

1) Creating a text editor in Electron: part 1 - Reading files 2) Creating a text editor in Electron: part 2 - writing files 3) Creating a text editor in Electron: part 3 - Setting Preferences

Welcome to the last part of the series where we explore the basics of Electron by building a text editor. If you like to get a full picture of what we're doing here, be sure to read part one, and part two

A convention that we see in almost all the apps we use is the ability to allow users to make their editor their own. That's what we will do in this section. We'll let the users set the theme of the application. By the end we'll have introduced a new window which will look like this:

Custom header

Before we get into the meat of the tutorial lets, change the boring default header.

(It's all about making readers happy 🤪).

It's pretty simple. When we define the window (new BrowserWindow), we can give it options for the frame. If we set the frame to false, it would delete it- including the three buttons on the left-hand side. So we want to remove the default styling but not the buttons. At ./main.js change the BrowserWindow definition to include the relevant option:

window = new BrowserWindow({ 
    width:800, 
    height:600, 
    titleBarStyle: 'hidden' 
})

If we run the app we see the three buttons but no header, which means we would have to create our own. So in ./static/index.html we would do something like this:

<header id="customtitle_wrap">
    <h1 id="customtitle">Texty</h1>
</header>

In part two we added the ability for an asterisk to appear in the title tag to indicate when a file needs saving. Now, we need to add that functionality to #customtitle rather than the title tag.

The CSS can now be whatever you'd like, however -webkit-app-region: drag should be applied to #customtitle so that it would be a handle from which to drag the window around the screen. Read the documentation for frameless windows to see all the options.

I grabbed a font from google fonts; however, in a real app, we would at least download the font so that users don't need to be connected to the internet.

Initializing the preferences window

Just as we've done on the first tutorial we need to load an HTML page into a new window. Let's create the page at ./static/preferences.html:

<body>
    <p class="notification">Here you're able to personalise the interface by picking the colors you'd like to see. The changes will be saved automatically upon window being closed</p>
    <div class="container">
    <div id="content" >
        <form action="">
            <button id="defaultValues">Reset</button>
            <p><span>Background</span> <label for="background"></label> <span>
                <input type="text" name="background" value="#FFFFFF"></span> 
            </p>
            <p><span>Border Color</span> <label for="border-color"></label> <span>
                <input type="text" name="border-color" value="#50C68A"></span> 
            </p>
            <p><span>Text Color</span> <label for="text-color"></label> <span>
                <input type="text" name="text-color" value="#232323"></span> 
            </p>
            <p><span>Sidebar Background</span> <label for="sidebar-color"></label> <span>
                <input type="text" name="sidebar-color" value="#2F3235"></span> 
            </p>
            <p><span>Sidebar Text</span> <label for="sidebar-text"></label> <span>
                <input type="text" name="sidebar-text" value="#939395"></span> 
            </p>
        </form>
    </div>
    </div>
    <script src="scripts/preferences.js"></script>
</body>

This page has to launch when a menu button gets clicked. Let's add that button at ./components/Menu.js

{
    label: app.getName(),
    submenu: [
        {
            label: 'Preferences',
            accelerator: 'cmd+,', // shortcut
            click: _ => {
                const htmlPath = path.join('file://', __dirname, '../static/preferences.html')
                let prefWindow = new BrowserWindow({ width: 500, height: 300, resizable: false })
                prefWindow.loadURL(htmlPath)
                prefWindow.show()
                // on window closed
            },
        },
    ]
}

When Preferences is selected the ./static/preferences.html page loads in a new browser window. This time we are making sure users are unable to resize it.

With some CSS applied, we get this:

As specified in the HTML above, the default colours are hardcoded in the form. With Javascript, we want to apply those colour values as the background colour for the labels, and when users enter new colour values have them be reflected in the labels. We could have fun with colour pickers, but we'll keep it basic and assume users want to input their preferred colours. In which case we need to listen to input changes.

This functionality needs to go in ./static/scripts/preferences.js.

Let's remember the HTML:

<p>
    <span>Sidebar Text</span> 
    <label for="sidebar-text"></label> <span>
    <input type="text" name="sidebar-text" value="#939395"></span> 
</p>

Hence the javascript can be as simple as looping through the inputs and changing the labels:

var inputs = document.getElementsByTagName('input')
for(var i = 0 ; i < inputs.length; i++){
    document.querySelector(`label[for="${inputs[i].name}"]`).style.backgroundColor = inputs[i].value
    inputs[i].onkeyup = e => {
        document.querySelector(`label[for="${e.target.name}"]`).style.backgroundColor = e.target.value
    }
}

The code loops through each input element, apply their values as label background colours, then on input changes re-applies the colours.

Saving the colour preferences

The point of this window is that these colours persist when the application closes, so they have to be stored somewhere. Electron gives us a path to store user data. The documentation states that we access this through electron.app.getPath('userData')

The directory for storing your app's configuration files, which by default it is the appData directory appended with your app's name.

Within that folder, we want to store our colours as JSON. We do this using the same messaging from Render process to the Main process pattern as we've done in part two.

First, let's collect all the colours then send them to the Main process.

let preferences = {};
for(var i = 0 ; i < inputs.length; i++){
    ...
    preferences[inputs[i].name] = inputs[i].value
    inputs[i].onkeyup = e => {
        preferences[e.target.name] = e.target.value
        ...
        ipcRenderer.send(PREFERENCE_SAVE_DATA_NEEDED, preferences)
    }
}

The preferences object is populated with all the default colours. Then whenever one of the inputs changes, the corresponding object key is changed. Lastly, we send a PREFERENCE_SAVE_DATA_NEEDED message to the Main process with the preferences object as the message body.

At the top of ./components/Menu.js we can listen for the message and collect its data

let inputs;
ipcMain.on(PREFERENCE_SAVE_DATA_NEEDED, (event, preferences) => {
    inputs = preferences
})

Finally, for the menu, a pattern which I've seen in almost all the mac apps is that preferences are saved without needing a "save" button. We can do the same thing here by acting upon window getting closed.

In the Menu page, we can write the logic on window close.

{
    label: 'Preferences',
    accelerator: 'cmd+,', // shortcut
    click: _ => {
        ....
        prefWindow.on('close', function () {
            prefWindow = null 
            userDataPath = app.getPath('userData');
            filePath = path.join(userDataPath, 'preferences.json')
            inputs && fs.writeFileSync(filePath, JSON.stringify(inputs));
            window.webContents.send(PREFERENCE_SAVED, inputs); 
        })

    },
}

The userDataPath is located at /Users/YourUserName/Library/Application Support/Electron/ and in there you'll find our preferences.json which holds the colours.

When that's done, the PREFERENCE_SAVED message is sent to the Render process of our original window.

Now we need to read the colours from the preferences.json file and apply them in the UI.

First, let's do it in the ./static/scripts/preferences.js

const fs = require('fs')
let userDataPath = remote.app.getPath('userData');
let filePath = path.join(userDataPath, 'preferences.json')
let usersStyles =  JSON.parse( fs.readFileSync(filePath) )

for(let style in usersStyles) {
    document.querySelector(`input[name="${style}"]`).value = usersStyles[style]
    document.querySelector(`label[for="${style}"]`).style.backgroundColor = usersStyles[style]
}

The process is reversed there. We read the saved data from preferences.json, loop through the colours and apply them as the input values and label background colours.

Reseting colors.

The reason why we'd want to hardcode the colours in the HTML form is so that we'd access them at any time with defaultValue in javascript. We'll do so upon clicking the reset button:

<button id="defaultValues">Reset</button>

On click, loop through the input fields and apply the default values accordingly.

document.getElementById('defaultValues').addEventListener('click', function(e) { // reset
    e.preventDefault();
    preferences = {};
    for(var i = 0 ; i < inputs.length; i++){
        preferences[inputs[i].name] = inputs[i].defaultValue
        document.querySelector(`label[for="${inputs[i].name}"]`).style.backgroundColor = inputs[i].defaultValue
        inputs[i].value = inputs[i].defaultValue
    }
    ipcRenderer.send(PREFERENCE_SAVE_DATA_NEEDED, preferences)
} )

The above code does the following

  1. Resets the preferences object.
  2. Re-populates the preferences object with input defaultValue .
  3. Changes the background colour of labels with input defaultValue.
  4. Changes the input value with input defaultValue.
  5. Sends a message to the Main process.

Applying the saved colour to the main window

Upon closing the preferences window, a message gets transmitted.

window.webContents.send(PREFERENCE_SAVED, inputs);

We can listen to it in the main window and use the content sent with the message.

Before doing so let's talk CSS.

The most important bit of CSS are the variables:

:root {
    --background: #FFFFFF;
    --border-color: #50C68A;
    --text-color: #232323;
    --sidebar-color: #2F3235;
    --sidebar-text: #939395;
}

Whenever we change those variables with javascript, the appearance of every element where we applied those variables to would change.

We can do this at ./static/scripts/index.js

    let userDataPath = remote.app.getPath('userData');
    let filePath = path.join(userDataPath, 'preferences.json')

    let usersStyles  = JSON.parse( fs.readFileSync(filePath) )

    for(let style in usersStyles) {
        document.documentElement.style.setProperty(`--${style}`, usersStyles[style]);
    }
    ipcRenderer.on(PREFERENCE_SAVED, function (event, inputs) {
        for(let style in inputs) {
            document.documentElement.style.setProperty(`--${style}`, inputs[style]);
        }
    });

There you have it. Now every element that uses those variables will be changed automatically.

And the end result looks like this

You can clone the repository at GitHub

Conclusion

That's it for this series. As I tried to make it clear there are a lot of things missing. The code can definitely be refactored, there can be storage improvements, and error handling is none existent.

While I was working on this demo I thought about my current writing workflow which consists of node generated markdown pages hosted as GitLab wiki and I feel adding an Electron interface might make the process of note-taking slightly smoother. That might be my next personal project.

Hope you've been equally inspired 💡

Creating a text editor in Electron (3 Part Series)

1) Creating a text editor in Electron: part 1 - Reading files 2) Creating a text editor in Electron: part 2 - writing files 3) Creating a text editor in Electron: part 3 - Setting Preferences

Posted on by:

aurelkurtula profile

aurel kurtula

@aurelkurtula

I love JavaScript, reading books, drinking coffee and taking notes.

Discussion

markdown guide
 

Hi great tutorial. I cloned the repo and the first 2 parts of the tutorial works fine but with the last one I get the following window and quite don't understand why... I am completly new to npm (programming in c++ for several years) and I can't get any feedback on what is goind wrong.
Thanks for the help

 
 

Well, yes, of course.

At some point, I want to convert the way I currently take notes into a react app and something like that would be ideal. Though I'm sure there are plenty of options. After learning those basics, now adding things to it is just a matter of preference.