├── .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 | 40 | 41 | -------------------------------------------------------------------------------- /resources/ui/new_item_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 157 | 158 | -------------------------------------------------------------------------------- /resources/ui/todo_item_list_box_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 137 | 138 | --------------------------------------------------------------------------------