Recently, while experimenting with GTK and its Ruby bindings, I decided to write a tutorial introducing this functionality. In this post, we will create a simple ToDo application (something like what we created with Ruby on Rails) using the gtk3 gem (a.k.a. the GTK+ Ruby bindings).

You can find the tutorial's code on GitHub.

What is GTK+?

According to GTK+'s website:

GTK+, or the GIMP Toolkit, is a multi-platform toolkit for creating graphical user interfaces. Offering a complete set of widgets, GTK+ is suitable for projects ranging from small one-off tools to complete application suites.

The site also explains why GTK+ was created:

GTK+ was initially developed for and used by the GIMP, the GNU Image Manipulation Program. It is called the "The GIMP ToolKit" so that the origins of the project are remembered. Today it is more commonly known as GTK+ for short and is used by a large number of applications including the GNU project's GNOME desktop.

Prerequisites

GTK+:

Make sure you have GTK+ installed. I developed the tutorial's application in Ubuntu 16.04, which has GTK+ (version 3.18) installed by default.

You can check your version with the following command: dpkg -l libgtk-3-0 .

Ruby:

You should have Ruby installed on your system. I use RVM to manage multiple Ruby versions installed on my system. If you want to do that too, you can find RVM installation instructions on its homepage and instructions for installing Ruby versions (a.k.a., Rubies) on the related documentation page.

This tutorial uses Ruby 2.4.2. You can check your version using ruby --version or via RVM with rvm list .

Glade:

Per Glade's website, "Glade is a RAD tool to enable quick & easy development of user interfaces for the GTK+ toolkit and the GNOME desktop environment."

We will use Glade to design our application's user interface. If you are on Ubuntu, install glade with sudo apt install glade .

GTK3 gem:

This gem provides the Ruby bindings for the GTK+ toolkit. In other words, it allows us to talk to the GTK+ API using the Ruby language.

Install the gem with gem install gtk3 .

Defining application specs

The application we will build in this tutorial will:

Have a user interface (i.e., a desktop application)

Allow users to set miscellaneous properties for each item (e.g., priority)

Allow users to create and edit ToDo items All items will be saved as files in the user's home directory in a folder named .gtk-todo-tutorial

Allow users to archive ToDo items Archived items should be put in their own folder called archived



Application structure

gtk-todo-tutorial # root directory

|-- application

|-- ui # everything related to the ui of the application

|-- models # our models

|-- lib # the directory to host any utilities we might need

|-- resources # directory to host the resources of our application

gtk-todo # the executable that will start our application

Building the ToDo application

Initializing the application

Create a directory to save all the files the application will need. As you can see in the structure above, I named mine gtk-todo-tutorial .

Create a file named gtk-todo (that's right, no extension) and add the following:

#!/usr/bin/env ruby



require 'gtk3'



app = Gtk::Application . new 'com.iridakos.gtk-todo' , :flags_none



app. signal_connect :activate do | application |

window = Gtk::ApplicationWindow . new ( application )

window. set_title 'Hello GTK+Ruby!'

window. present

end



puts app. run

This will be the script that starts the application.

Note the shebang ( #! ) in the first line. This is how we define which interpreter will execute the script under Unix/Linux operating systems. This way, we don't have to use ruby gtk-todo ; we can just use the script's name: gtk-todo .

Don't try it yet though, because we haven't changed the file's mode to be executable. To do so, type the following command in a terminal after navigating to the application's root directory:

chmod + x . / gtk - todo # make the script executable

From the console, execute:

./gtk-todo # execute the script

Notes:

The application object we defined above (and all of the GTK+ widgets in general) emit signals to trigger events. Once an application starts running, for example, it emits a signal to trigger the activate event. All we have to do is to define what we want to happen when this signal is emitted. We accomplished this by using the signal_connect instance method and passing it a block whose code will be executed upon the given event. We will be doing this a lot throughout the tutorial.

event. All we have to do is to define what we want to happen when this signal is emitted. We accomplished this by using the instance method and passing it a block whose code will be executed upon the given event. We will be doing this a lot throughout the tutorial. When we initialized the Gtk::Application object, we passed two parameters: com.iridakos.gtk-todo : This is our application's ID and, in general, it should be a reverse DNS style identifier. You can learn more about its usage and best practices on GNOME's wiki. :flags_none : This flag defines the behavior of the application. We used the default behavior. Check out all the flags and the types of applications they define. We can use the Ruby-equivalent flags, as defined in Gio::ApplicationFlags.constants . For example, instead of using :flags_none , we could use Gio::ApplicationFlags::FLAGS_NONE .

object, we passed two parameters:

Suppose the application object we previously created ( Gtk::Application ) had a lot of things to do when the activate signal was emitted or that we wanted to connect to more signals. We would end up creating a huge gtk-todo script file, making it hard to read/maintain. It's time to refactor.

As described in the application structure above, we'll create a folder named application and sub-folders ui , models , and lib .

In the ui folder, we'll place all files related to our user interface.

folder, we'll place all files related to our user interface. In the models folder, we'll place all files related to our models.

folder, we'll place all files related to our models. In the lib folder, we'll place all files that don't belong to either of those categories.

We will define a new subclass of the Gtk::Application class for our application. We'll create a file named application.rb under application/ui/todo with the following contents:

module ToDo

class Application < Gtk::Application

def initialize

super 'com.iridakos.gtk-todo' , Gio::ApplicationFlags::FLAGS_NONE



signal_connect :activate do | application |

window = Gtk::ApplicationWindow . new ( application )

window. set_title 'Hello GTK+Ruby!'

window. present

end

end

end

end

We'll change the gtk-todo script accordingly:

#!/usr/bin/env ruby



require 'gtk3'



app = ToDo::Application . new



puts app. run

Much cleaner, isn't it? Yeah, but it doesn't work. We get something like:

./gtk-todo:5:in `<main>': uninitialized constant ToDo (NameError)

The problem is that we haven't required any of the Ruby files placed in the application folder. We need to change the script file as follows and execute it again.

#!/usr/bin/env ruby



require 'gtk3'



# Require all ruby files in the application folder recursively

application_root_path = File . expand_path ( __dir__ )

Dir [ File . join ( application_root_path, '**' , '*.rb' ) ] . each { | file | require file }



app = ToDo::Application . new



puts app. run

Now it should be fine.

Resources

At the beginning of this tutorial, we said we would use Glade to design the application's user interface. Glade produces xml files with the appropriate elements and attributes that reflect what we designed via its user interface. We need to use those files for our application to get the UI we designed.

These files are resources for the application, and the GResource API provides a way for packing them all together in a binary file that later can be accessed from inside the application with advantages—as opposed to manually having to deal with already loaded resources, their location on the file system, etc. Read more about the GResource API.

Describing the resources

First, we need to create a file describing the resources of the application. Create a file named gresources.xml and place it directly under the resources folder.

<?xml version = "1.0" encoding = "UTF-8" ?>

<gresources >

<gresource prefix = "/com/iridakos/gtk-todo" >

<file preprocess = "xml-stripblanks" > ui/application_window.ui </file >

</gresource >

</gresources >

This description basically says: "We have a resource that is located under the ui directory (relative to this xml file) with the name application_window.ui . Before loading this resource, please remove the blanks." Of course, this won't work yet, since we haven't created the resource via Glade. Don't worry though, one thing at a time.

Note: The xml-stripblanks directive will use the xmllint command to remove the blanks. In Ubuntu, you have to install the package libxml2-utils .

Building the resources binary file

To produce the binary resources file, we will use another GLib library utility called glib-compile-resources . Check if you have it installed with dpkg -l libglib2.0-bin . You should see something like this:

ii libglib2.0-bin 2.48.2-0ubuntu amd64 Programs for the GLib library

If not, install the package ( sudo apt install libglib2.0-bin in Ubuntu).

Let's build the file. We will add code to our script so the resources will be built every time we execute it. Change the gtk-todo script as follows:

#!/usr/bin/env ruby



require 'gtk3'

require 'fileutils'



# Require all ruby files in the application folder recursively

application_root_path = File . expand_path ( __dir__ )

Dir [ File . join ( application_root_path, '**' , '*.rb' ) ] . each { | file | require file }



# Define the source & target files of the glib-compile-resources command

resource_xml = File . join ( application_root_path, 'resources' , 'gresources.xml' )

resource_bin = File . join ( application_root_path, 'gresource.bin' )



# Build the binary

system ( "glib-compile-resources" ,

"--target" , resource_bin,

"--sourcedir" , File . dirname ( resource_xml ) ,

resource_xml )



at_exit do

# Before existing, please remove the binary we produced, thanks.

FileUtils . rm_f ( resource_bin )

end



app = ToDo::Application . new

puts app. run

When we execute it, the following happens in the console; we'll fix it later:

/.../gtk-todo-tutorial/resources/gresources.xml: Failed to locate 'ui/application_window.ui' in any source directory.

Here's what we did:

Added a require statement for the fileutils library so we can use it in the at_exit call

statement for the library so we can use it in the call Defined the source and target files of the glib-compile-resources command

command Executed the glib-compile-resources command

command Set a hook so the binary file will be deleted before exiting the script (i.e., before the application exits) so the next time it will be built again

Loading the resources binary file

We've described the resources and packed them in a binary file. Now we have to load and register them in the application so we can use them. This is as easy as adding the following two lines before the at_exit hook:

resource = Gio::Resource . load ( resource_bin )

Gio::Resources . register ( resource )

That's it. From now on, we can use the resources from anywhere inside the application. (We'll see how later.) For now, the script fails since it can't load a binary that isn't produced. Be patient; we'll get to the interesting part soon. Actually now.

Designing the main application window

Introducing Glade

To begin, open Glade.

Here's what we see:

On the left, there is a list of widgets that can be dragged and dropped in the middle section. (You can't add a top-level window inside a label widget.) I'll call this the Widget section .

. The middle section contains our widgets as they will appear (most of the time) in the application. I'll call this the Design section .

. On the right are two subsections: The top section contains the hierarchy of the widgets as they're added to the resource. I'll call this the Hierarchy section . The bottom section contains all the properties that can be configured via Glade for a widget selected above. I'll call this the Properties section .



I will describe the steps for building this tutorial's UI using Glade, but if you are interested in building GTK+ applications, you should look at the tool's official resources & tutorials.

Create the application window design

Let's create the application window by simply dragging the Application Window widget from the Widget section to the Design section.

Gtk::Builder is an object used in GTK+ applications to read textual descriptions of a user interface (like the one we will build via Glade) and build the described object widgets.

The first thing in the Properties section is the ID , and it has a default value applicationWindow1 . If we leave this property as-is, we would later create a Gtk::Builder through our code that would load the file produced by Glade. To obtain the application window, we would have to use something like:

application_window = builder. get_object ( 'applicationWindow1' )



application_window. signal_connect 'whatever' do | a,b |

...

The application_window object would be of class Gtk::ApplicationWindow ; thus whatever we had to add to its behavior (like setting its title) would take place outside the original class. Also, as shown in the snippet above, the code to connect to a window's signal would be placed inside the file that instantiated it.

The good news is that GTK+ introduced a feature in 2013 that allows creation of composite widget templates, which (among other advantages) allow us to define the custom class for the widget (which eventually derives from an existing GTK::Widget class in general). Don't worry if you are confused. You will understand what is going on after we write some code and view the results.

To define our design as a template, check the Composite checkbox in the property widget. Note that the ID property changed to Class Name . Fill in TodoApplicationWindow . This is the class we will create in our code to represent this widget.

Save the file with the name application_window.ui in a new folder named ui inside the resources . Here's what we see if we open the file from an editor:

<?xml version = "1.0" encoding = "UTF-8" ?>

<!-- Generated with glade 3.18.3 -->

<interface >

<requires lib = "gtk+" version = "3.12" />

<template class = "TodoApplicationWindow" parent = "GtkApplicationWindow" >

<property name = "can_focus" > False </property >

<child >

<placeholder />

</child >

</template >

</interface >

Our widget has a class and a parent attribute. Following the parent class attribute convention, our class must be defined inside a module named Todo . Before getting there, let's try to start the application by executing the script ( ./gtk-todo ).

Yeah! It starts!

Create the application window class

If we check the contents of the application's root directory while running the application, we can see the gresource.bin file there. Even though the application starts successfully because the resource bin is present and can be registered, we won't use it yet. We'll still initiate an ordinary Gtk::ApplicationWindow in our application.rb file. Now it's time to create our custom application window class.

Create a file named application_window.rb in the application/ui/todo folder and add the following content:

module Todo

class ApplicationWindow < Gtk::ApplicationWindow

# Register the class in the GLib world

type_register



class << self

def init

# Set the template from the resources binary

set_template resource: '/com/iridakos/gtk-todo/ui/application_window.ui'

end

end



def initialize ( application )

super application: application



set_title 'GTK+ Simple ToDo'

end

end

end

We defined the init method as a singleton method on the class after opening the eigenclass in order to bind the template of this widget to the previously registered resource file.

Before that, we called the type_register class method, which registers and makes available our custom widget class to the GLib world.

Finally, each time we create an instance of this window, we set its title to GTK+ Simple ToDo .

Now, let's go back to the application.rb file and use what we just implemented:

module ToDo

class Application < Gtk::Application

def initialize

super 'com.iridakos.gtk-todo' , Gio::ApplicationFlags::FLAGS_NONE



signal_connect :activate do | application |

window = Todo::ApplicationWindow . new ( application )

window. present

end

end

end

end

Execute the script.

Define the model

For simplicity, we'll save the ToDo items in files in JSON format under a dedicated hidden folder in our user's home directory. In a real application, we would use a database, but that is outside the scope of this tutorial.

Our Todo::Item model will have the following properties:

id : The item's id

: The item's id title : The title

: The title notes : Any notes

: Any notes priority : Its priority

: Its priority creation_datetime : The date and time the item was created

: The date and time the item was created filename: The name of the file an item is saved to

We'll create a file named item.rb under the application/models directory with the following contents:

require 'securerandom'

require 'json'



module Todo

class Item

PROPERTIES = [ :id , :title , :notes , :priority , :filename , :creation_datetime ] . freeze



PRIORITIES = [ 'high' , 'medium' , 'normal' , 'low' ] . freeze



attr_accessor * PROPERTIES



def initialize ( options = { } )

if user_data_path = options [ :user_data_path ]

# New item. When saved, it will be placed under the :user_data_path value

@id = SecureRandom. uuid

@creation_datetime = Time . now . to_s

@filename = "#{user_data_path}/#{id}.json"

elsif filename = options [ :filename ]

# Load an existing item

load_from_file filename

else

raise ArgumentError , 'Please specify the :user_data_path for new item or the :filename to load existing'

end

end



# Loads an item from a file

def load_from_file ( filename )

properties = JSON. parse ( File . read ( filename ) )



# Assign the properties

PROPERTIES. each do | property |

self . send "#{property}=" , properties [ property. to_s ]

end

rescue => e

raise ArgumentError , "Failed to load existing item: #{e.message}"

end



# Resolves if an item is new

def is_new?

! File . exists ? @filename

end



# Saves an item to its `filename` location

def save!

File . open ( @filename, 'w' ) do | file |

file. write self . to_json

end

end



# Deletes an item

def delete!

raise 'Item is not saved!' if is_new?



File . delete ( @filename )

end



# Produces a json string for the item

def to_json

result = { }

PROPERTIES. each do | prop |

result [ prop ] = self . send prop

end



result. to_json

end

end

end

Here we defined methods to:

Initialize an item: As "new" by defining the :user_data_path in which it will be saved later As "existing" by defining the :filename to be loaded from. The filename must be a JSON file previously generated by an item

Load an item from a file

Resolve whether an item is new or not (i.e., saved at least once in the :user_data_path or not)

or not) Save an item by writing its JSON string to a file

Delete an item

Produce the JSON string of an item as a hash of its properties

Add a new item

Create the button

Let's add a button to our application window for adding a new item. Open the resources/ui/application_window.ui file in Glade.

Drag a Button from the Widget section to the Design section.

from the Widget section to the Design section. In the Properties section, set its ID value to add_new_item_button .

. Near the bottom of the General tab in the Properties section, there's a text area just below the Label with optional image option. Change its value from Button to Add new item.

Save the file and execute the script.

Don't worry; we will improve the design later. Now, let's see how to connect functionality to our button's clicked event.

First, we must update our application window class so it learns about its new child, the button with id add_new_item_button . Then we can access the child to alter its behavior.

Change the init method as follows:

def init

# Set the template from the resources binary

set_template resource: '/com/iridakos/gtk-todo/ui/application_window.ui'



bind_template_child 'add_new_item_button'

end

Pretty simple, right? The bind_template_child method does exactly what it says, and from now on every instance of our Todo::ApplicationWindow class will have an add_new_item_button method to access the related button. So, let's alter the initialize method as follows:

def initialize ( application )

super application: application



set_title 'GTK+ Simple ToDo'



add_new_item_button. signal_connect 'clicked' do | button, application |

puts "OMG! I AM CLICKED"

end

end

As you can see, we'll access the button by the add_new_item_button method, and we define what we want to take place when it's clicked. Restart the application and try clicking the button. In the console, you should see the message OMG! I AM CLICKED when you click the button.

However, what we want to happen when we click this button is to show a new window to save a ToDo item. You guessed right: It's Glade o'clock.

Create the new item window

Create a new project in Glade by pressing the left-most icon in the top bar or by selecting File > New from the application menu.

Drag a Window from the Widget section to the Design area.

from the Widget section to the Design area. Check its Composite property and name the class TodoNewItemWindow .

Drag a Grid from the Widget section and place it in the window we added previously.

from the Widget section and place it in the window we added previously. Set 5 rows and 2 columns in the window that pops up.

rows and columns in the window that pops up. In the General tab of the Properties section, set the rows and columns spacing to 10 (pixels).

(pixels). In the Common tab of the Properties section, set the Widget Spacing > Margins > Top, Bottom, Left, Right all to 10 so that the contents aren't stuck to the grid's borders.

Drag four Label widgets from the Widget section and place one in each row of the grid.

widgets from the Widget section and place one in each row of the grid. Change their Label properties, from top to bottom, as follows: Id: Title: Notes: Priority:

properties, from top to bottom, as follows: In the General tab of the Properties section, change the Alignment and Padding > Alignment > Horizontal property from 0.50 to 1 for each property in order to right-align the label text.

for each property in order to right-align the label text. This step is optional but recommended. We will not bind those labels in our window since we don't need to alter their state or behavior. In this context, we don't need to set a descriptive ID for them as we did for the add_new_item_button button in the application window. BUT we will add more elements to our design, and the hierarchy of the widgets in Glade will be hard to read if they say label1 , label2 , etc. Setting descriptive IDs (like id_label , title_label , notes_label , priority_label ) will make our lives easier. I even set the grid's ID to main_grid because I don't like seeing numbers or variable names in IDs.

Drag a Label from the Widget section to the second column of the grid's first row. The ID will automatically be generated by our model; we won't allow editing, so a label to display it is more than enough.

from the Widget section to the second column of the grid's first row. The ID will automatically be generated by our model; we won't allow editing, so a label to display it is more than enough. Set the ID property to id_value_label .

property to . Set the Alignment and Padding > Alignment > Horizontal property to 0 so the text aligns on the left.

so the text aligns on the left. We will bind this widget to our Window class so we can change its text each time we load the window. Therefore, setting a label through Glade is not required, but it makes the design closer to what it will look like when rendered with actual data. You can set a label to whatever suits you best; I set mine to id-of-the-todo-item-here .

Drag a Text Entry from the Widget section to the second column of the second row of the grid.

from the Widget section to the second column of the second row of the grid. Set its ID property to title_text_entry . As you may have noticed, I prefer obtaining the widget type in the ID to make the code in the class more readable.

. As you may have noticed, I prefer obtaining the widget type in the ID to make the code in the class more readable. In the Common tab of the Properties section, check the Widget Spacing > Expand > Horizontal checkbox and turn on the switch next to it. This way, the widget will expand horizontally every time its parent (a.k.a., the grid) is resized.

Drag a Text View from the Widget section to the second column of the third row of the grid.

from the Widget section to the second column of the third row of the grid. Set its ID to notes . Nope, just testing you. Set its ID property to notes_text_view .

to . Nope, just testing you. Set its property to . In the Common tab of the Properties section, check the Widget Spacing > Expand > Horizontal, Vertical checkboxes and turn on the switches next to them. This way, the widget will expand horizontally and vertically every time its parent (the grid) is resized.

Drag a Combo Box from the Widget section to the second column of the fourth row of the grid.

from the Widget section to the second column of the fourth row of the grid. Set its ID to priority_combo_box .

to . In the Common tab of the Properties section, check the Widget Spacing > Expand > Horizontal checkbox and turn on the switch to its right. This allows the widget to expand horizontally every time its parent (the grid) is resized.

checkbox and turn on the switch to its right. This allows the widget to expand horizontally every time its parent (the grid) is resized. This widget is a drop-down element. We will populate its values that can be selected by the user when it shows up inside our window class.

Drag a Button Box from the Widget section to the second column of the last row of the grid.

from the Widget section to the second column of the last row of the grid. In the pop-up window, select 2 items.

items. In the General tab of the Properties section, set the Box Attributes > Orientation property to Horizontal .

. In the General tab of the Properties section, set the Box Attributes > Spacing property to 10 .

. In the Common tab of the Properties section, set the Widget Spacing > Alignment > Horizontal to Center .

. Again, our code won't alter this widget, but you can give it a descriptive ID for readability. I named mine actions_box .

Drag two Button widgets and place one in each box of the button box widget we added in the previous step.

widgets and place one in each box of the button box widget we added in the previous step. Set their ID properties to cancel_button and save_button , respectively.

properties to and , respectively. In the General tab of the Properties window, set their Button Content > Label with option image property to Cancel and Save, respectively.

The window is ready. Save the file under resources/ui/new_item_window.ui .

It's time to port it into our application.

Implement the new item window class

Before implementing the new class, we must update our GResource description file ( resources/gresources.xml ) to obtain the new resource:

<?xml version = "1.0" encoding = "UTF-8" ?>

<gresources >

<gresource prefix = "/com/iridakos/gtk-todo" >

<file preprocess = "xml-stripblanks" > ui/application_window.ui </file >

<file preprocess = "xml-stripblanks" > ui/new_item_window.ui </file >

</gresource >

</gresources >

Now we can create the new window class. Create a file under application/ui/todo named new_item_window.rb and set its contents as follows:

module Todo

class NewItemWindow < Gtk::Window

# Register the class in the GLib world

type_register



class << self

def init

# Set the template from the resources binary

set_template resource: '/com/iridakos/gtk-todo/ui/new_item_window.ui'

end

end



def initialize ( application )

super application: application

end

end

end

There's nothing special here. We just changed the template resource to point to the correct file of our resources.

We have to change the add_new_item_button code that executes on the clicked signal to show the new item window. We'll go ahead and change that code in application_window.rb to this:

add_new_item_button. signal_connect 'clicked' do | button |

new_item_window = NewItemWindow. new ( application )

new_item_window. present

end

Let's see what we have done. Start the application and click on the Add new item button. Tadaa!

But nothing happens when we press the buttons. Let's fix that.

First, we'll bind the UI widgets in the Todo::NewItemWindow class.

Change the init method to this:

def init

# Set the template from the resources binary

set_template resource: '/com/iridakos/gtk-todo/ui/new_item_window.ui'



# Bind the window's widgets

bind_template_child 'id_value_label'

bind_template_child 'title_text_entry'

bind_template_child 'notes_text_view'

bind_template_child 'priority_combo_box'

bind_template_child 'cancel_button'

bind_template_child 'save_button'

end

This window will be shown when either creating or editing a ToDo item, so the new_item_window naming is not very valid. We'll refactor that later.

For now, we will update the window's initialize method to require one extra parameter for the Todo::Item to be created or edited. We can then set a more meaningful window title and change the child widgets to reflect the current item.

We'll change the initialize method to this:

def initialize ( application, item )

super application: application

set_title "ToDo item #{item.id} - #{item.is_new? ? 'Create' : 'Edit' } Mode"



id_value_label. text = item. id

title_text_entry. text = item. title if item. title

notes_text_view. buffer . text = item. notes if item. notes



# Configure the combo box

model = Gtk::ListStore . new ( String )

Todo::Item::PRIORITIES . each do | priority |

iterator = model. append

iterator [ 0 ] = priority

end



priority_combo_box. model = model

renderer = Gtk::CellRendererText . new

priority_combo_box. pack_start ( renderer, true )

priority_combo_box. set_attributes ( renderer, "text" => 0 )



priority_combo_box. set_active ( Todo::Item::PRIORITIES . index ( item. priority ) ) if item. priority

end

Then we'll add the constant PRIORITIES in the application/models/item.rb file just below the PROPERTIES constant:

PRIORITIES = [ 'high' , 'medium' , 'normal' , 'low' ] . freeze

What did we do here?

We set the window's title to a string containing the current item's ID and the mode (depending on whether the item is being created or edited).

We set the id_value_label text to display the current item's ID.

text to display the current item's ID. We set the title_text_entry text to display the current item's title.

text to display the current item's title. We set the notes_text_view text to display the current item's notes.

text to display the current item's notes. We created a model for the priority_combo_box whose entries are going to have only one String value. At first sight, a Gtk::ListStore model might look a little confusing. Here's how it works. Suppose we want to display in a combo box a list of country codes and their respective country names. We would create a Gtk::ListStore defining that its entries would consist of two string values: one for the country code and one for the country name. Thus we would initialize the ListStore as: model = Gtk::ListStore . new ( String , String ) To fill the model with data, we would do something like the following (make sure you don't miss the comments in the snippet): [ [ 'gr' , 'Greece' ] , [ 'jp' , 'Japan' ] , [ 'nl' , 'Netherlands' ] ] . each do | country_pair |

entry = model. append

# Each entry has two string positions since that's how we initialized the Gtk::ListStore

# Store the country code in position 0

entry [ 0 ] = country_pair [ 0 ]

# Store the country name in position 1

entry [ 1 ] = country_pair [ 1 ]

end We also configured the combo box to render two text columns/cells (again, make sure you don't miss the comments in the snippet): country_code_renderer = Gtk::CellRendererText . new

# Add the first renderer

combo. pack_start ( country_code_renderer, true )

# Use the value in index 0 of each model entry a.k.a. the country code

combo. set_attributes ( country_code_renderer, 'text' => 0 )



country_name_renderer = Gtk::CellRendererText . new

# Add the second renderer

combo. pack_start ( country_name_renderer, true )

# Use the value in index 1 of each model entry a.k.a. the country name

combo. set_attributes ( country_name_renderer, 'text' => 1 ) I hope that made it a little clearer.

whose entries are going to have only one value. At first sight, a model might look a little confusing. Here's how it works. We added a simple text renderer in the combo box and instructed it to display the only value of each model's entry (a.k.a., position 0 ). Imagine that our model is something like [['high'],['medium'],['normal'],['low']] and 0 is the first element of each sub-array. I will stop with the model-combo-text-renderer explanations now…

Configure the user data path

Remember that when initializing a new Todo::Item (not an existing one), we had to define a :user_data_path in which it would be saved. We are going to resolve this path when the application starts and make it accessible from all the widgets.

All we have to do is check if the .gtk-todo-tutorial path exists inside the user's home ~ directory. If not, we will create it. Then we'll set this as an instance variable of the application. All widgets have access to the application instance. So, all widgets have access to this user path variable.

Change the application/application.rb file to this:

module ToDo

class Application < Gtk::Application

attr_reader :user_data_path



def initialize

super 'com.iridakos.gtk-todo' , Gio::ApplicationFlags::FLAGS_NONE



@user_data_path = File . expand_path ( '~/.gtk-todo-tutorial' )

unless File . directory ? ( @user_data_path )

puts "First run. Creating user's application path: #{@user_data_path}"

FileUtils . mkdir_p ( @user_data_path )

end



signal_connect :activate do | application |

window = Todo::ApplicationWindow . new ( application )

window. present

end

end

end

end

One last thing we need to do before testing what we have done so far is to instantiate the Todo::NewItemWindow when the add_new_item_button is clicked complying with the changes we made. In other words, change the code in application_window.rb to this:

add_new_item_button. signal_connect 'clicked' do | button |

new_item_window = NewItemWindow. new ( application, Todo::Item . new ( user_data_path: application. user_data_path ) )

new_item_window. present

end

Start the application and click on the Add new item button. Tadaa! (Note the - Create mode part in the title).

To close the Todo::NewItemWindow window when a user clicks the cancel_button , we only have to add this to the window's initialize method:

cancel_button. signal_connect 'clicked' do | button |

close

end

close is an instance method of the Gtk::Window class that closes the window.

Save the item

Saving an item involves two steps:

Update the item's properties based on the widgets' values.

Call the save! method on the Todo::Item instance.

Again, our code will be placed in the initialize method of the Todo::NewItemWindow :

save_button. signal_connect 'clicked' do | button |

item. title = title_text_entry. text

item. notes = notes_text_view. buffer . text

item. priority = priority_combo_box. active_iter . get_value ( 0 ) if priority_combo_box. active_iter

item. save !

close

end

Once again, the window closes after saving the item.

Let's try that out.

Now, by pressing Save and navigating to our ~/.gtk-todo-tutorial folder, we should see a file. Mine had the following contents:

{

"id" : "3d635839-66d0-4ce6-af31-e81b47b3e585" ,

"title" : "Optimize the priorities model creation" ,

"notes" : "It doesn't have to be initialized upon each window creation." ,

"priority" : "high" ,

"filename" : "/home/iridakos/.gtk-todo-tutorial/3d635839-66d0-4ce6-af31-e81b47b3e585.json" ,

"creation_datetime" : "2018-01-25 18:09:51 +0200"

}

Don't forget to try out the Cancel button as well.

View ToDo items

The Todo::ApplicationWindow contains only one button. It's time to change that.

We want the window to have Add new item on the top and a list below with all of our ToDo items. We'll add a Gtk::ListBox to our design that can contain any number of rows.

Open the resources/ui/application_window.ui file in Glade.

file in Glade. Nothing happens if we drag a List Box widget from the Widget section directly on the window. That is normal. First, we have to split the window into two parts: one for the button and one for the list box. Bear with me.

widget from the Widget section directly on the window. That is normal. First, we have to split the window into two parts: one for the button and one for the list box. Bear with me. Right-click on the new_item_window in the Hierarchy section and select Add parent > Box.

in the Hierarchy section and select Add parent > Box. In the pop-up window, set 2 for the number of items.

for the number of items. The orientation of the box is already vertical, so we are fine.

Now, drag a List Box and place it on the free area of the previously added box.

and place it on the free area of the previously added box. Set its ID property to todo_items_list_box .

property to . Set its Selection mode to None since we won't provide that functionality.

Design the ToDo item list box row

Each row of the list box we created in the previous step will be more complex than a row of text. Each will contain widgets that allow the user to expand an item's notes and to delete or edit the item.

Create a new project in Glade, as we did for the new_item_window.ui . Save it under resources/ui/todo_item_list_box_row.ui .

. Save it under . Unfortunately (at least in my version of Glade), there is no List Box Row widget in the Widget section. So, we'll add one as the top-level widget of our project in a kinda hackish way.

Drag a List Box from the Widget section to the Design area.

from the Widget section to the Design area. Inside the Hierarchy section, right-click on the List Box and select Add Row

In the Hierarchy section, right-click on the newly added List Box Row nested under the List Box and select Remove parent . There it is! The List Box Row is the top-level widget of the project now.

Check the widget's Composite property and set its name to TodoItemListBoxRow .

property and set its name to . Drag a Box from the Widget section to the Design area inside our List Box Row .

from the Widget section to the Design area inside our . Set 2 items in the pop-up window.

items in the pop-up window. Set its ID property to main_box .

Drag another Box from the Widget section to the first row of the previously added box.

from the Widget section to the first row of the previously added box. Set 2 items in the pop-up window.

items in the pop-up window. Set its ID property to todo_item_top_box .

property to . Set its Orientation property to Horizontal .

. Set its Spacing (General tab) property to 10.

Drag a Label from the Widget section to the first column of the todo_item_top_box .

from the Widget section to the first column of the . Set its ID property to todo_item_title_label .

property to . Set its Alignment and Padding > Alignment > Horizontal property to 0.00 .

. In the Common tab of the Properties section, check the Widget Spacing > Expand > Horizontal checkbox and turn on the switch next to it so the label will expand to the available space.

Drag a Button from the Widget section to the second column of the todo_item_top_box .

from the Widget section to the second column of the . Set its ID property to details_button .

property to . Check the Button Content > Label with optional image radio and type ... (three dots).

Drag a Revealer widget from the Widget section to the second row of the main_box .

widget from the Widget section to the second row of the . Turn off the Reveal Child switch in the General tab.

switch in the General tab. Set its ID property to todo_item_details_revealer .

property to . Set its Transition type property to Slide Down .

Drag a Box from the Widget section to the reveal space.

from the Widget section to the reveal space. Set its items to 2 in the pop-up window.

in the pop-up window. Set its ID property to details_box .

property to . In the Common tab, set its Widget Spacing > Margins > Top property to 10.

Drag a Button Box from the Widget section to the first row of the details_box .

from the Widget section to the first row of the . Set its ID property to todo_item_action_box .

property to . Set its Layout style property to expand .

Drag Button widgets to the first and second columns of the todo_item_action_box .

widgets to the first and second columns of the . Set their ID properties to delete_button and edit_button , respectively.

properties to and , respectively. Set their Button Content > Label with optional image properties to Delete and Edit, respectively.

Drag a Viewport widget from the Widget section to the second row of the details_box .

widget from the Widget section to the second row of the . Set its ID property to todo_action_notes_viewport .

property to . Drag a Text View widget from the Widget section to the todo_action_notes_viewport that we just added.

widget from the Widget section to the that we just added. Set its ID to todo_item_notes_text_view .

to . Uncheck its Editable property in the General tab of the Properties section.

Create the ToDo item list-box row class

Now we will create the class reflecting the UI of the list-box row we just created.

First we have to update our GResource description file to include the newly created design. Change the resources/gresources.xml file as follows:

<?xml version = "1.0" encoding = "UTF-8" ?>

<gresources >

<gresource prefix = "/com/iridakos/gtk-todo" >

<file preprocess = "xml-stripblanks" > ui/application_window.ui </file >

<file preprocess = "xml-stripblanks" > ui/new_item_window.ui </file >

<file preprocess = "xml-stripblanks" > ui/todo_item_list_box_row.ui </file >

</gresource >

</gresources >

Create a file named item_list_box_row.rb inside the application/ui folder and add the following:

module Todo

class ItemListBoxRow < Gtk::ListBoxRow

type_register



class << self

def init

set_template resource: '/com/iridakos/gtk-todo/ui/todo_item_list_box_row.ui'

end

end



def initialize ( item )

super ( )

end

end

end

We will not bind any children at the moment.

When starting the application, we have to search for files in the :user_data_path , and we must create a Todo::Item instance for each file. For each instance, we must also add a new Todo::ItemListBoxRow to the Todo::ApplicationWindow 's todo_items_list_box list box. One thing at a time.

First, let's bind the todo_items_list_box in the Todo::ApplicationWindow class. Change the init method as follows:

def init

# Set the template from the resources binary

set_template resource: '/com/iridakos/gtk-todo/ui/application_window.ui'



bind_template_child 'add_new_item_button'

bind_template_child 'todo_items_list_box'

end

Next, we'll add an instance method in the same class that will be responsible to load the ToDo list items in the related list box. Add this code in Todo::ApplicationWindow :

def load_todo_items

todo_items_list_box. children . each { | child | todo_items_list_box. remove child }



json_files = Dir [ File . join ( File . expand_path ( application. user_data_path ) , '*.json' ) ]

items = json_files. map { | filename | Todo::Item . new ( filename: filename ) }



items. each do | item |

todo_items_list_box. add Todo::ItemListBoxRow . new ( item )

end

end

Then we'll call this method at the end of the initialize method:

def initialize ( application )

super application: application



set_title 'GTK+ Simple ToDo'



add_new_item_button. signal_connect 'clicked' do | button |

new_item_window = NewItemWindow. new ( application, Todo::Item . new ( user_data_path: application. user_data_path ) )

new_item_window. present

end



load_todo_items

end

Note: We must first empty the list box of its current children rows then refill it. This way, we will call this method after saving a Todo::Item via the signal_connect of the save_button of the Todo::NewItemWindow , and the parent application window will be reloaded! Here's the updated code (in application/ui/new_item_window.rb ):

save_button. signal_connect 'clicked' do | button |

item. title = title_text_entry. text

item. notes = notes_text_view. buffer . text

item. priority = priority_combo_box. active_iter . get_value ( 0 ) if priority_combo_box. active_iter

item. save !



close



# Locate the application window

application_window = application. windows . find { | w | w. is_a ? Todo::ApplicationWindow }

application_window. load_todo_items

end

Previously, we used this code:

json_files = Dir [ File . join ( File . expand_path ( application. user_data_path ) , '*.json' ) ]

to find the names of all the files in the application-user data path with a JSON extension.

Let's see what we've created. Start the application and try adding a new ToDo item. After pressing the Save button, you should see the parent Todo::ApplicationWindow automatically updated with the new item!

What's left is to complete the functionality of the Todo::ItemListBoxRow .

First, we will bind the widgets. Change the init method of the Todo::ItemListBoxRow class as follows:

def init

set_template resource: '/com/iridakos/gtk-todo/ui/todo_item_list_box_row.ui'



bind_template_child 'details_button'

bind_template_child 'todo_item_title_label'

bind_template_child 'todo_item_details_revealer'

bind_template_child 'todo_item_notes_text_view'

bind_template_child 'delete_button'

bind_template_child 'edit_button'

end

Then, we'll set up the widgets based on the item of each row.

def initialize ( item )

super ( )



todo_item_title_label. text = item. title || ''



todo_item_notes_text_view. buffer . text = item. notes



details_button. signal_connect 'clicked' do

todo_item_details_revealer. set_reveal_child !todo_item_details_revealer. reveal_child ?

end



delete_button. signal_connect 'clicked' do

item. delete !



# Locate the application window

application_window = application. windows . find { | w | w. is_a ? Todo::ApplicationWindow }

application_window. load_todo_items

end



edit_button. signal_connect 'clicked' do

new_item_window = NewItemWindow. new ( application, item )

new_item_window. present

end

end



def application

parent = self . parent

parent = parent. parent while !parent. is_a ? Gtk::Window

parent. application

end

As you can see, when the details_button is clicked, we instruct the todo_item_details_revealer to swap the visibility of its contents.

is clicked, we instruct the to swap the visibility of its contents. After deleting an item, we find the application's Todo::ApplicationWindow to call its load_todo_items , as we did after saving an item.

to call its , as we did after saving an item. When clicking to edit a button, we create a new instance of the Todo::NewItemWindow passing an item as the current item. Works like a charm!

passing an item as the current item. Works like a charm! Finally, to reach the application parent of a list-box row, we defined a simple instance method application that navigates through the widget's parents until it reaches a window from which it can obtain the application object.

Save and run the application. There it is!

This has been a really long tutorial and, even though there are so many items that we haven't covered, I think we better end it here.

Long post, cat photo.

This was originally published on Lazarus Lazaridis's blog, iridakos.com, and is republished with permission.