├── .gitignore
├── LICENSE
├── README.md
├── application
├── models
│ └── todo
│ │ └── item.rb
└── ui
│ └── todo
│ ├── application.rb
│ ├── application_window.rb
│ ├── item_list_box_row.rb
│ └── new_item_window.rb
├── gtk-todo
└── resources
├── gresources.xml
└── ui
├── application_window.ui
├── new_item_window.ui
└── todo_item_list_box_row.ui
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | /.config
4 | /coverage/
5 | /InstalledFiles
6 | /pkg/
7 | /spec/reports/
8 | /spec/examples.txt
9 | /test/tmp/
10 | /test/version_tmp/
11 | /tmp/
12 |
13 | # Used by dotenv library to load environment variables.
14 | # .env
15 |
16 | ## Specific to RubyMotion:
17 | .dat*
18 | .repl_history
19 | build/
20 | *.bridgesupport
21 | build-iPhoneOS/
22 | build-iPhoneSimulator/
23 |
24 | ## Specific to RubyMotion (use of CocoaPods):
25 | #
26 | # We recommend against adding the Pods directory to your .gitignore. However
27 | # you should judge for yourself, the pros and cons are mentioned at:
28 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
29 | #
30 | # vendor/Pods/
31 |
32 | ## Documentation cache and generated files:
33 | /.yardoc/
34 | /_yardoc/
35 | /doc/
36 | /rdoc/
37 |
38 | ## Environment normalization:
39 | /.bundle/
40 | /vendor/bundle
41 | /lib/bundler/man/
42 |
43 | # for a library or gem, you might want to ignore these files since the code is
44 | # intended to run in multiple environments; otherwise, check them in:
45 | # Gemfile.lock
46 | # .ruby-version
47 | # .ruby-gemset
48 |
49 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
50 | .rvmrc
51 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Lazarus Lazaridis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gtk-todo-tutorial
2 | The code of the **Creating a simple GTK+ ToDo application with Ruby**
3 |
4 | Tutorial's page [http://iridakos.com/tutorials/2018/01/25/creating-a-gtk-todo-application-with-ruby](http://iridakos.com/tutorials/2018/01/25/creating-a-gtk-todo-application-with-ruby)
5 |
--------------------------------------------------------------------------------
/application/models/todo/item.rb:
--------------------------------------------------------------------------------
1 | require 'securerandom'
2 | require 'json'
3 |
4 | module Todo
5 | class Item
6 | PROPERTIES = [:id, :title, :notes, :priority, :filename, :creation_datetime].freeze
7 |
8 | PRIORITIES = ['high', 'medium', 'normal', 'low'].freeze
9 |
10 | attr_accessor *PROPERTIES
11 |
12 | def initialize(options = {})
13 | if user_data_path = options[:user_data_path]
14 | # New item. When saved, it will be placed under the :user_data_path value
15 | @id = SecureRandom.uuid
16 | @creation_datetime = Time.now.to_s
17 | @filename = "#{user_data_path}/#{id}.json"
18 | elsif filename = options[:filename]
19 | # Load an existing item
20 | load_from_file filename
21 | else
22 | raise ArgumentError, 'Please specify the :user_data_path for new item or the :filename to load existing'
23 | end
24 | end
25 |
26 | # Loads an item from a file
27 | def load_from_file(filename)
28 | properties = JSON.parse(File.read(filename))
29 |
30 | # Assign the properties
31 | PROPERTIES.each do |property|
32 | self.send "#{property}=", properties[property.to_s]
33 | end
34 | rescue => e
35 | raise ArgumentError, "Failed to load existing item: #{e.message}"
36 | end
37 |
38 | # Resolves if an item is new
39 | def is_new?
40 | !File.exists? @filename
41 | end
42 |
43 | # Saves an item to its `filename` location
44 | def save!
45 | File.open(@filename, 'w') do |file|
46 | file.write self.to_json
47 | end
48 | end
49 |
50 | # Deletes an item
51 | def delete!
52 | raise 'Item is not saved!' if is_new?
53 |
54 | File.delete(@filename)
55 | end
56 |
57 | # Produces a json string for the item
58 | def to_json
59 | result = {}
60 | PROPERTIES.each do |prop|
61 | result[prop] = self.send prop
62 | end
63 |
64 | result.to_json
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/application/ui/todo/application.rb:
--------------------------------------------------------------------------------
1 | module Todo
2 | class Application < Gtk::Application
3 | attr_reader :user_data_path
4 |
5 | def initialize
6 | super 'com.iridakos.gtk-todo', Gio::ApplicationFlags::FLAGS_NONE
7 |
8 | @user_data_path = File.expand_path('~/.gtk-todo-tutorial')
9 | unless File.directory?(@user_data_path)
10 | puts "First run. Creating user's application path: #{@user_data_path}"
11 | FileUtils.mkdir_p(@user_data_path)
12 | end
13 |
14 | signal_connect :activate do |application|
15 | window = Todo::ApplicationWindow.new(application)
16 | window.present
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/application/ui/todo/application_window.rb:
--------------------------------------------------------------------------------
1 | module Todo
2 | class ApplicationWindow < Gtk::ApplicationWindow
3 | # Register the class in the GLib world
4 | type_register
5 |
6 | class << self
7 | def init
8 | # Set the template from the resources binary
9 | set_template resource: '/com/iridakos/gtk-todo/ui/application_window.ui'
10 |
11 | bind_template_child 'add_new_item_button'
12 | bind_template_child 'todo_items_list_box'
13 | end
14 | end
15 |
16 | def initialize(application)
17 | super application: application
18 |
19 | set_title 'GTK+ Simple ToDo'
20 |
21 | add_new_item_button.signal_connect 'clicked' do |button|
22 | new_item_window = NewItemWindow.new(application, Todo::Item.new(user_data_path: application.user_data_path))
23 | new_item_window.present
24 | end
25 |
26 | load_todo_items
27 | end
28 |
29 | def load_todo_items
30 | todo_items_list_box.children.each { |child| todo_items_list_box.remove child }
31 |
32 | json_files = Dir[File.join(File.expand_path(application.user_data_path), '*.json')]
33 | items = json_files.map{ |filename| Todo::Item.new(filename: filename) }
34 |
35 | items.each do |item|
36 | todo_items_list_box.add Todo::ItemListBoxRow.new(item)
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/application/ui/todo/item_list_box_row.rb:
--------------------------------------------------------------------------------
1 | module Todo
2 | class ItemListBoxRow < Gtk::ListBoxRow
3 | type_register
4 |
5 | class << self
6 | def init
7 | set_template resource: '/com/iridakos/gtk-todo/ui/todo_item_list_box_row.ui'
8 |
9 | bind_template_child 'details_button'
10 | bind_template_child 'todo_item_title_label'
11 | bind_template_child 'todo_item_details_revealer'
12 | bind_template_child 'todo_item_notes_text_view'
13 | bind_template_child 'delete_button'
14 | bind_template_child 'edit_button'
15 | end
16 | end
17 |
18 | def initialize(item)
19 | super()
20 |
21 | todo_item_title_label.text = item.title || ''
22 |
23 | todo_item_notes_text_view.buffer.text = item.notes
24 |
25 | details_button.signal_connect 'clicked' do
26 | todo_item_details_revealer.set_reveal_child !todo_item_details_revealer.reveal_child?
27 | end
28 |
29 | delete_button.signal_connect 'clicked' do
30 | item.delete!
31 |
32 | # Locate the application window
33 | application_window = application.windows.find { |w| w.is_a? Todo::ApplicationWindow }
34 | application_window.load_todo_items
35 | end
36 |
37 | edit_button.signal_connect 'clicked' do
38 | new_item_window = NewItemWindow.new(application, item)
39 | new_item_window.present
40 | end
41 | end
42 |
43 | def application
44 | parent = self.parent
45 | parent = parent.parent while !parent.is_a? Gtk::Window
46 | parent.application
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/application/ui/todo/new_item_window.rb:
--------------------------------------------------------------------------------
1 | module Todo
2 | class NewItemWindow < Gtk::Window
3 | # Register the class in the GLib world
4 | type_register
5 |
6 | class << self
7 | def init
8 | # Set the template from the resources binary
9 | set_template resource: '/com/iridakos/gtk-todo/ui/new_item_window.ui'
10 |
11 | # Bind the window's widgets
12 | bind_template_child 'id_value_label'
13 | bind_template_child 'title_text_entry'
14 | bind_template_child 'notes_text_view'
15 | bind_template_child 'priority_combo_box'
16 | bind_template_child 'cancel_button'
17 | bind_template_child 'save_button'
18 | end
19 | end
20 |
21 | def initialize(application, item)
22 | super application: application
23 | set_title "ToDo item #{item.id} - #{item.is_new? ? 'Create' : 'Edit' } Mode"
24 |
25 | id_value_label.text = item.id
26 | title_text_entry.text = item.title if item.title
27 | notes_text_view.buffer.text = item.notes if item.notes
28 |
29 | # Configure the combo box
30 | model = Gtk::ListStore.new(String)
31 | Todo::Item::PRIORITIES.each do |priority|
32 | iterator = model.append
33 | iterator[0] = priority
34 | end
35 |
36 | priority_combo_box.model = model
37 | renderer = Gtk::CellRendererText.new
38 | priority_combo_box.pack_start(renderer, true)
39 | priority_combo_box.set_attributes(renderer, "text" => 0)
40 |
41 | priority_combo_box.set_active(Todo::Item::PRIORITIES.index(item.priority)) if item.priority
42 |
43 | cancel_button.signal_connect 'clicked' do |button|
44 | close
45 | end
46 |
47 | save_button.signal_connect 'clicked' do |button|
48 | item.title = title_text_entry.text
49 | item.notes = notes_text_view.buffer.text
50 | item.priority = priority_combo_box.active_iter.get_value(0) if priority_combo_box.active_iter
51 | item.save!
52 |
53 | close
54 |
55 | # Locate the application window
56 | application_window = application.windows.find { |w| w.is_a? Todo::ApplicationWindow }
57 | application_window.load_todo_items
58 | end
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/gtk-todo:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'gtk3'
4 | require 'fileutils'
5 |
6 | # Require all ruby files in the application folder recursively
7 | application_root_path = File.expand_path(__dir__)
8 | Dir[File.join(application_root_path, '**', '*.rb')].each { |file| require file }
9 |
10 | # Define the input & output files of the command
11 | resource_xml = File.join(application_root_path, 'resources', 'gresources.xml')
12 | resource_bin = File.join(application_root_path, 'gresource.bin')
13 |
14 | # Build the binary
15 | system("glib-compile-resources",
16 | "--target", resource_bin,
17 | "--sourcedir", File.dirname(resource_xml),
18 | resource_xml)
19 |
20 | resource = Gio::Resource.load(resource_bin)
21 | Gio::Resources.register(resource)
22 |
23 | at_exit do
24 | # Before exiting, please remove the binary we produced, thanks.
25 | FileUtils.rm_f(resource_bin)
26 | end
27 |
28 | app = Todo::Application.new
29 | puts app.run
30 |
--------------------------------------------------------------------------------
/resources/gresources.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ui/application_window.ui
5 | ui/new_item_window.ui
6 | ui/todo_item_list_box_row.ui
7 |
8 |
9 |
--------------------------------------------------------------------------------
/resources/ui/application_window.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | False
7 |
8 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/resources/ui/new_item_window.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | False
7 |
8 |
9 | True
10 | False
11 | 10
12 | 10
13 | 10
14 | 10
15 | 10
16 | 10
17 |
18 |
19 | True
20 | False
21 | Id:
22 | 1
23 |
24 |
25 | 0
26 | 0
27 |
28 |
29 |
30 |
31 | True
32 | False
33 | Title:
34 | 1
35 |
36 |
37 | 0
38 | 1
39 |
40 |
41 |
42 |
43 | True
44 | False
45 | Notes:
46 | 1
47 |
48 |
49 | 0
50 | 2
51 |
52 |
53 |
54 |
55 | True
56 | False
57 | Priority:
58 | 1
59 |
60 |
61 | 0
62 | 3
63 |
64 |
65 |
66 |
67 | True
68 | False
69 | id-of-the-todo-item-here
70 | 0
71 |
72 |
73 | 1
74 | 0
75 |
76 |
77 |
78 |
79 | True
80 | True
81 | True
82 |
83 |
84 | 1
85 | 1
86 |
87 |
88 |
89 |
90 | True
91 | True
92 | True
93 | True
94 | word
95 |
96 |
97 | 1
98 | 2
99 |
100 |
101 |
102 |
103 | True
104 | False
105 | True
106 |
107 |
108 | 1
109 | 3
110 |
111 |
112 |
113 |
114 | True
115 | False
116 | center
117 | 10
118 | start
119 |
120 |
121 | Cancel
122 | True
123 | True
124 | True
125 |
126 |
127 | True
128 | True
129 | 0
130 |
131 |
132 |
133 |
134 | Save
135 | True
136 | True
137 | True
138 |
139 |
140 | True
141 | True
142 | 1
143 |
144 |
145 |
146 |
147 | 1
148 | 4
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
--------------------------------------------------------------------------------
/resources/ui/todo_item_list_box_row.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | True
7 | True
8 | start
9 |
10 |
11 | True
12 | False
13 | start
14 | vertical
15 | top
16 |
17 |
18 | True
19 | False
20 | start
21 | 10
22 |
23 |
24 | True
25 | False
26 | True
27 | This is a ToDo item title
28 | 0
29 |
30 |
31 | False
32 | True
33 | 0
34 |
35 |
36 |
37 |
38 | ...
39 | True
40 | True
41 | True
42 | start
43 | True
44 |
45 |
46 | False
47 | True
48 | 1
49 |
50 |
51 |
52 |
53 | False
54 | True
55 | 0
56 |
57 |
58 |
59 |
60 | True
61 | False
62 | start
63 |
64 |
65 | True
66 | False
67 | 10
68 | vertical
69 |
70 |
71 | True
72 | False
73 | expand
74 |
75 |
76 | Delete
77 | True
78 | True
79 | True
80 |
81 |
82 | True
83 | True
84 | 1
85 |
86 |
87 |
88 |
89 | Edit
90 | True
91 | True
92 | True
93 |
94 |
95 | True
96 | True
97 | 1
98 |
99 |
100 |
101 |
102 | False
103 | True
104 | 0
105 |
106 |
107 |
108 |
109 | True
110 | False
111 |
112 |
113 | True
114 | True
115 | False
116 |
117 |
118 |
119 |
120 | False
121 | True
122 | 1
123 |
124 |
125 |
126 |
127 |
128 |
129 | False
130 | False
131 | 1
132 |
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------