Create  Edit  FrontPage  Index  Search  Changes  History  RSS  Login

tut-gtk2-treev-addrnhs

Adding Rows and Handling Selections

All of the examples that we have encountered so far in the "Tree View Widget" tutorial session, load the data into the tree model during the start-up. However, it is also possible to load data interactively. Here we will first expand theGrocery Listapplication to allow users to add and remove products, and then investigate a more elaborate tree view application of products, we also have already seen in the 'Using Gtk TreeStore' chapter where we were looking at different ways to load ascii data (arrays) into tree store in the segment entitled 'Tedious Job of Loading Multidimensional Tree Store'. In this later expanded (treev-MultiDim-loadASCI-add-n-rm-rows.rb) example we will deal particularly with the issue of child hierarchies that rows in a tree view may have and which too, are affected when adding new or removing existing rows. However, before we dive into theadding-n-removing-SELECTED-rows.rbexample, we will learn how to handle single and multiple selections.

Single Selections

Selection information is held for each tree view by a Gtk::TreeSelection object. Every tree view automatically has a Gtk::TreeSelection associated with it, and you can get it using Gtk::TreeView#selection. Selections are handled completely on the tree view side, which means that the model knows nothing about which rows are selected.

A Gtk::TreeSelection object will automatically be created for you for every Gtk::TreeView, so you will never create one by yourself, therefore it cannot exist independently of this widget. The primary reason the Gtk::TreeSelection objects exists is for cleanliness of code and API. That is, there is no conceptual reason why all these methods could not have been implemented as methods on the Gtk::TreeView widget rather than methods of a separate object.

One of the important things to remember when monitoring the selection of a view is that the 'Gtk::TreeSelection#changed' signal is mostly a hint, and is unreliable. That is, it may only emit one signal when a range of rows is selected. Additionally, it may on occasion emit a "changed" signal when nothing has happened (mostly as a result of programmers calling select_row on an already selected row). Therefore, it is best to use the signals provided by Gtk::TreeView for selection handling; e.g. Gtk::TreeView signals: 'columns-changed', 'cursor-changed', 'expand-collapse-cursor-row', 'row-activated', 'row-collapsed', 'row-expanded', 'select-all', ...

Tree views support multiple types of selections. You can change the selection type with Gtk::TreeSelection#mode=, where the mode value (GtkSelectionMode) is used to control what selections users are allowed to make, and can be one of the following:

Gtk Selection Mode

The Gtk::TreeSelection object can be obtained from a Gtk::TreeView by calling Gtk::TreeView#selection. This method will not work with the selection mode Gtk::SELECTION_MULTIPLE. The selection can be manipulated to check the selection status of the tree, as well as select and deselect individual rows. Selection is done completely on the view side. As a result, multiple views of the same model can have completely different selections. Additionally, you cannot change the selection of a row on the model that is not currently displayed by the view without expanding its parents first.

Multiple Selections

If your tree selection allows multiple rows to be selected (Gtk::SELECTION_MULTIPLE), you have two options to handle selections: (1) calling a method for every row or retrieving all of the selected rows. Following API segment will tell the whole story:

selected
Gets iter to the currently selected node if selection is set to Gtk::SELECTION_SINGLE or Gtk::SELECTION_BROWSE. iter may be nil if you just want to test if selection has any selected nodes. This method will not work if you use selection is Gtk::SELECTION_MULTIPLE.
selected_each {|model, path, iter| ... }
Calls a block for each selected node.
  • {|model, path, iter| ... }: The block to call for each selected node. (data : user data to pass to the function.)
  • Returns: self
select_path(path)
Select the row at path.

There are more ways to deal with tree view selections: either you get a list of the currently selected rows whenever you need it by obtaining Gtk::TreeSelection object via: Gtk::TreeView#selection and then traversing it by running on the obtained object the Gtk::TreeSelection#selected_each method, or you keep track of all selected and unselected actions and keep a list of the currently selected rows around for whenever you need them; as a last resort, you can also traverse your list or tree and check each single row for whether it is selected or not (which you need to do if you want all rows that are not selected for example).

Obtaining Gtk::TreeSelection object:

Depending on the Gtk::TreeSelection#mode you gain access either to a single or multiple selections

Checking a single selection:

selection = treeview.selection

 if iter = selection.selected        # will not work if selection mode is Gtk::SELECTION_MULTIPLE
   puts "selected row is #{iter[0]}"
 else
   puts "no row selected"
 end

Multiple selections:

When Gtk::TreeSelection#mode is set to Gtk::SELECTION_MULTIPLE you can traverse all the selected rows in the Gtk::TreeSelection object by running its Gtk::TreeSelection#selected_each method:

treeview.selection.selected_each do |model, path, iter|
  puts "#{iter[0]} is selected"
end

Example traversing entire TreeView structure:

def traverse_list_to_print_all_rows(store)
  # get first row in list store
  return unless iter = store.iter_first    #<-- same as: iter = store.get_iter("0")

  begin
    puts "Product: #{iter[2]} Buy:#{iter[0]}"
  end while iter.next!
end

Example obtaining only NON-selected rows:

def print_only_unselected_rows(store, treeview)
  # get first row in list store
  return unless iter = store.iter_first    #<-- same as: iter = store.get_iter("0")

  all_selections = treeview.selection   # <--- obtain all selections from TreeView
  begin
    puts "Product: #{iter[2]} Buy:#{iter[0]}"  if ! all_selections.iter_is_selected?(iter)
  end while iter.next!                         #  =-==============-=======================
end

Following is the program demonstrating some of the selection issues:

tview-selections.rb

#!/usr/bin/env ruby
=begin
SEARCHING TREE VIEW FOR SELECTIONS
==================================
Into this program I packed things we've learned so far about
Gtk::TreeView, and the pertinent data model.

Included items are:

  1) The connection strategy of model an view:
      Gtk::TreeViewColumn.new("Buy", renderer, :text => BUY_IT [, ...] )
      where  [, ...] are in the form of :property => COLUMN_NUM, ...

  2) The use of {{ set_cell_data_func(renderer) {|col, renderer, model, iter| ...} }}

  3) Catching double-clicks on a row. It's quite easy and is done by 
     connecting to a tree view's "row-activated" signal

  4) Traversing through selected rows via 
      {{ selection.selected_each do |model, path, iter| ... end }}

  5) Test {{ set_select_function }} 
=end

require 'gtk2'

def setup_tree_view(treeview)
  renderer = Gtk::CellRendererText.new
  column   = Gtk::TreeViewColumn.new("Buy", renderer,  :text => BUY_IT)
  column.set_cell_data_func(renderer) do |col, renderer, model, iter|
    renderer.background = iter[BUY_IT] ? "red" : nil
  end
  treeview.append_column(column)
  renderer = Gtk::CellRendererText.new
  column   = Gtk::TreeViewColumn.new("Count", renderer, :text => QUANTITY)
  treeview.append_column(column)
  renderer = Gtk::CellRendererText.new
  column   = Gtk::TreeViewColumn.new("Product", renderer, :text => PRODUCT)
  treeview.append_column(column)
end

def traverse_list_to_print_all_rows(store)
  # get first row in list store
  return unless iter = store.iter_first

  begin
    puts "Product: #{iter[2]} Buy:#{iter[0]}"
  end while iter.next!
end

def print_only_unselected_rows(store, treev)
  # get first row in list store
  return unless iter = store.iter_first

  # - You can check whether a given row is selected or not using the 
  # -
  # -   Gtk::TreeSelection#iter_is_selected?
  # - or 
  # -   Gtk::TreeSelection#path_is_selected? 
  # -
  # - methods. If you want to know all rows that are not selected, for 
  # - example, you could just traverse the whole list or tree, and use 
  # - the above methods to check for each row whether it is selected or not.

  all_selections = treev.selection   # <--- obtain all selections from TreeView
  begin
    puts "Product: #{iter[2]} Buy:#{iter[0]}"  if ! all_selections.iter_is_selected?(iter)
  end while iter.next!                         #  =-==============-=======================
end

# Print all selected rows (2nd way /using VIEW/
def print_all_selected_rows(treev)

  treev.selection.selected_each do |model, path, iter|
    puts "Product: #{iter[2]} Buy:#{iter[0]}"
  end
end

class GroceryItem
  attr_accessor :buy, :quantity, :product
  def initialize(b, q, p); @buy, @quantity, @product = b, q, p; end
end
BUY_IT = 0; QUANTITY = 1; PRODUCT  = 2

list = Array.new
list[0] = GroceryItem.new(true,  1, "Paper Towels") 
list[1] = GroceryItem.new(true,  2, "Bread")
list[2] = GroceryItem.new(false, 1, "Butter")
list[3] = GroceryItem.new(true,  1, "Milk")
list[4] = GroceryItem.new(false, 3, "Chips")
list[5] = GroceryItem.new(true,  4, "Soda") 

store = Gtk::ListStore.new(TrueClass, Integer, String)
treeview = Gtk::TreeView.new(store) # Add the tree model (store) to the tree view
treeview.selection.mode = Gtk::SELECTION_MULTIPLE

# Test {{ Gtk::TreeSelection#set_select_function }}
treeview.selection.set_select_function do |selection, model, path, currently_selected|
  if iter = model.get_iter(path)
    if ! currently_selected
      puts "At path:#{path} #{iter[2]} is about to be selected"
    else
      puts "At path:#{path} #{iter[2]} is about to be --unselected--"
    end
  end

  # allow selection state to change
  true
end

treeview.signal_connect("row-activated") do |view, path, column|
  iter = view.model.get_iter(path)
  puts "Double-clicked row contains product: #{iter[2]}!"
end

setup_tree_view(treeview)

list.each_with_index do |e, i|
    iter = store.append
    store.set_value(iter, BUY_IT,   list[i].buy)	# iter[BUY_IT]   = list[i].buy
    store.set_value(iter, QUANTITY, list[i].quantity)	# iter[QUANTITY] = list[i].quantity
    store.set_value(iter, PRODUCT,  list[i].product)	# iter[PRODUCT]  = list[i].product
end

vbox = Gtk::VBox.new(homogeneous = false, spacing = nil)
button = Gtk::Button.new("Collect and analize selections")
button.signal_connect("clicked") do |w|

  puts "==== All rows regardless of selection (using MODEL)"
  traverse_list_to_print_all_rows(store)

  puts "==== All NON-selected rows (using both, MODEL and VIEW)"
  print_only_unselected_rows(store, treeview)

  puts "==== All selected rows (using VIEW)"
  print_all_selected_rows(treeview)
end

scrolled_win = Gtk::ScrolledWindow.new
scrolled_win.add(treeview)
scrolled_win.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)

vbox.pack_start_defaults(scrolled_win)
vbox.pack_start_defaults(button)
window = Gtk::Window.new("Grocery List")
window.resizable = true
window.border_width = 10
window.signal_connect('destroy') { Gtk.main_quit }
window.set_size_request(250, 165)
window.add(vbox)
window.show_all
Gtk.main

The above program demonstrated how user selections are handled in tree view. If you look closely you will find Gtk::TreeSelection#set_select_function method with which we set the selection block. This code block is called before any node is selected or unselected, giving the programmer additional control over which nodes are about to get selected and unselected. The select block should returntrueif the state of the node may be toggled, and false if the state of the node should be left unchanged. The toggled value is passed to us in the block parameter in our example program and in API calledcurrently_selected.The best way to understand this feature is to run the program once with set_select_function block returningtrueand subsequently with the return valuefalseand comparing the console output of the two runs.

Note
Do not confuse Gtk::TreeSelection#set_select_function with Gtk::TreeViewColumn#set_cell_data_func.


Adding New Rows

treev-addrnhs-01.png

Now it is time to act on our first promise, namely to augment our Grocery List application, to allow adding and removing rows. The only difference in main body of the this application in comparison to the program from the earlier session is the addition of 'Add' and 'Remove' buttons. Also you should notice that the selection mode is set to Gtk::SELECTION_MULTIPLE to allow users to select multiple rows at the time. But the main additions to the application are in the form of the methods called add_product and remove_products. If user clicks the '+Add' button the application presents a Gtk::Dialog that asks the user to choose a category, enter a product name and quantity, or rather a number, of products to buy, and to set the check-box indicating whether or not to purchase the product.

If all of the entries are valid, the row is added under the chosen category. Also if the user specifies that the product should be purchased, the "Count" value is added to the total Count value for the appropriate category. On the other hand if the user clicks on the "-Remove" button, the selected items, provided they are not top level (parent) rows, should be removed and the "Count" values updated accordingly.

tooltips.png

Tooltips:

There is a hidden and with regards to selections irrelevant added feature here, namely thetooltip.If user hovers the cursor over the tree view display or over the remove button two different tool-tip messages are displayed to the user.

remove.tooltip_text = "You can select multiple\nlines and delete them"
treeview.tooltip_text = "You can select multiple lines"


Let us now look at the example:

adding-n-removing-SELECTED-rows.rb

#!/usr/bin/env ruby
require 'gtk2'

def add_product(treeview, list)
  # Create a dialog that will be used to create a new product.

  dialog = Gtk::Dialog.new(
      "Add a Product",
      nil,
      Gtk::Dialog::MODAL,
      [ Gtk::Stock::ADD,    Gtk::Dialog::RESPONSE_OK ],
      [ Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL ]
  )
  # Create widgets that will be packed into the dialog.
  combobox = Gtk::ComboBox.new
  entry = Gtk::Entry.new

  #                         min, max, step
  spin  = Gtk::SpinButton.new(0, 100, 1)
  # Set the precision to be displayed by spin button.
  spin.digits = 0
  check = Gtk::CheckButton.new("_Buy the Product")

  # Add all of the categories to the combo box.
  list.each_with_index do |e, i|
    combobox.append_text(list[i].product) if (e.product_type == P_CATEGORY)
  end

  ### Usually, after initialiying combobox you would set the default 
  ### combobox.active = 0    # set active index (1st row)

  table = Gtk::Table.new(4, 2, false)
  table.row_spacings = 5
  table.column_spacings = 5
  table.border_width = 5

  # Pack the table that will hold the dialog widgets.
  fll_shr = Gtk::SHRINK | Gtk::FILL
  fll_exp = Gtk::EXPAND | Gtk::FILL

  table.attach(Gtk::Label.new("Category:"), 0, 1, 0, 1, fll_shr, fll_shr,  0, 0)
  table.attach(combobox,                    1, 2, 0, 1, fll_exp, fll_shr,  0, 0)
  table.attach(Gtk::Label.new("Product:"),  0, 1, 1, 2, fll_shr, fll_shr,  0, 0)
  table.attach(entry,                       1, 2, 1, 2, fll_exp, fll_shr,  0, 0)
  table.attach(Gtk::Label.new("Quantity:"), 0, 1, 2, 3, fll_shr, fll_shr,  0, 0)
  table.attach(spin,                        1, 2, 2, 3, fll_exp, fll_shr,  0, 0)
  table.attach(check,                       1, 2, 3, 4, fll_exp, fll_shr,  0, 0)

  dialog.vbox.pack_start_defaults(table)
  dialog.show_all

  dialog.run do |response|
    # If the user presses OK, verify the entries and add the product.
    if response == Gtk::Dialog::RESPONSE_OK
      quantity = spin.value
      product = entry.text
      category = combobox.active_text
      buy = check.active?

      if product == "" || category == nil
        puts "All of the fields were not correctly filled out!"
        puts "DEBUG:  prod=(#{product}), ctg=(#{category})"
        dialog.destroy
        return
      end

      model = treeview.model
      iter = model.iter_first    #<-- same as: iter = model.get_iter("0")

      # Retrieve an iterator pointing to the selected category.
      begin
        name = iter[PROD_INDEX]  #<-- same as: name=iter.get_value(PROD_INDEX)
        break if name == category
      end while iter.next!

      child = model.append(iter)

      # child[BUY_INDEX]=buy # same as: model.set_value(child, BUY_INDEX, buy)
      child[BUY_INDEX]   = buy
      child[QTY_INDEX]   = quantity
      child[PROD_INDEX]  = product

      # Add the quantity to the running total if it is to be purchased.
      if buy
        qty_value = iter[QTY_INDEX]
        qty_value += quantity
        iter[QTY_INDEX] = qty_value
      end
    end
    dialog.destroy
  end
end

def remove_row(ref, model)
  path = ref.path
  iter = model.get_iter(path)

  # Only remove the row if it is not a root row.
  parent = iter.parent
  if parent
    buy       = iter[BUY_INDEX]
    quantity  = iter[QTY_INDEX]
    pqty      = parent[QTY_INDEX]
    if buy
      pqty -= quantity
      parent[QTY_INDEX] = pqty
    end
    iter = model.get_iter(path)
    model.remove(iter)
  end
end

def remove_products(treeview)
  # Gtk::TreeRowReference.new(model, path)
  selection = treeview.selection

  paths2rm = Array.new
  selection.selected_each do |mod, path, iter|
    ref = Gtk::TreeRowReference.new(mod, path)
    paths2rm << [ref, mod]
  end
  paths2rm.each { |ref, mod| remove_row(ref, mod) }
end

# Add three columns to the GtkTreeView. All three of the
# columns will be displayed as text, although one is a boolean
# value and another is an integer.
def setup_tree_view(treeview)
  # Create a new GtkCellRendererText, add it to the tree
  # view column and append the column to the tree view.
  renderer = Gtk::CellRendererText.new
  column = Gtk::TreeViewColumn.new("Buy", renderer, "text" => BUY_INDEX)
  treeview.append_column(column)
  renderer = Gtk::CellRendererText.new
  column = Gtk::TreeViewColumn.new("Count", renderer, "text" => QTY_INDEX)
  treeview.append_column(column) 
  renderer = Gtk::CellRendererText.new
  column = Gtk::TreeViewColumn.new("Product", renderer, "text" => PROD_INDEX)
  treeview.append_column(column)
end

window = Gtk::Window.new(Gtk::Window::TOPLEVEL)
window.resizable = true
window.title = "Grocery List"
window.border_width = 10
window.signal_connect('delete_event') { Gtk.main_quit }
window.set_size_request(275, 300)

class GroceryItem
  attr_accessor :product_type, :buy, :quantity, :product
  def initialize(t,b,q,p)
    @product_type, @buy, @quantity, @product = t, b, q, p
  end
end
BUY_INDEX = 0; QTY_INDEX = 1; PROD_INDEX = 2
P_CATEGORY = 0; P_CHILD = 1

list = Array.new
list[0] = GroceryItem.new(P_CATEGORY, true,  0, "Cleaning Supplies")
list[1] = GroceryItem.new(P_CHILD,    true,  1, "Paper Towels")
list[2] = GroceryItem.new(P_CHILD,    true,  3, "Toilet Paper")
list[3] = GroceryItem.new(P_CATEGORY, true,  0, "Food")
list[4] = GroceryItem.new(P_CHILD,    true,  2, "Bread")
list[5] = GroceryItem.new(P_CHILD,    false, 1, "Butter")
list[6] = GroceryItem.new(P_CHILD,    true,  1, "Milk")
list[7] = GroceryItem.new(P_CHILD,    false, 3, "Chips")
list[8] = GroceryItem.new(P_CHILD,    true,  4, "Soda")

treeview = Gtk::TreeView.new
treeview.tooltip_text = "You can select multiple lines"

setup_tree_view(treeview)

# Create a new tree model with three columns, as Boolean, 
# integer and string.
store = Gtk::TreeStore.new(TrueClass, Integer, String)

# Avoid creation of iterators on every iteration, since they
# need to provide state information for all iterations. Hence:
# establish closure variables for iterators parent and child.
parent = child = nil

# Add all of the products to the GtkListStore.
list.each_with_index do |e, i|

  # If the product type is a category, count the quantity
  # of all of the products in the category that are going
  # to be bought.
  if (e.product_type == P_CATEGORY)
    j = i + 1

    # Calculate how many products will be bought in
    # the category.
    while j < list.size && list[j].product_type != P_CATEGORY
      list[i].quantity += list[j].quantity if list[j].buy
      j += 1
    end

    # Add the category as a new root (parent) row (element).
    parent = store.append(nil)
    # store.set_value(parent, BUY_INDEX, list[i].buy # <= same as below
    parent[BUY_INDEX]   = list[i].buy
    parent[QTY_INDEX]   = list[i].quantity
    parent[PROD_INDEX]  = list[i].product

  # Otherwise, add the product as a child row of the category.
  else
    child = store.append(parent)
    # store.set_value(child, BUY_INDEX, list[i].buy # <= same as below
    child[BUY_INDEX]   = list[i].buy
    child[QTY_INDEX]   = list[i].quantity
    child[PROD_INDEX]  = list[i].product
  end
end

# Add the tree model to the tree view
treeview.model = store
treeview.expand_all

# Allow multiple rows to be selected at the same time.
treeview.selection.mode = Gtk::SELECTION_MULTIPLE

scrolled_win = Gtk::ScrolledWindow.new
scrolled_win.add(treeview)
scrolled_win.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)

add    = Gtk::Button.new(Gtk::Stock::ADD)
remove = Gtk::Button.new(Gtk::Stock::REMOVE)
remove.tooltip_text = "You can select multiple\nlines and delete them"

add.signal_connect('clicked')    { add_product(treeview, list) }
remove.signal_connect('clicked') { remove_products(treeview) }

hbox = Gtk::HBox.new(true, 5)
hbox.pack_start(add,    false, true, 0)
hbox.pack_start(remove, false, true, 0)

vbox = Gtk::VBox.new(false, 5)
vbox.pack_start(scrolled_win, true,  true, 0)
vbox.pack_start(hbox,         false, true, 0)

window.add(vbox)
window.show_all
Gtk.main

This program is rather involved, which is true for both added facilities that user can access by clicking either the "+Add" or "-Remove" buttons. For this program we will talk about removing rows shortly, under the title 'Removing Multiple Rows', but let us first look at the functionality behind the "+Add" button.

When "+Add" button is clicked add_product(treeview, list) method is called. In the first part of this method we build the a data entry dialogue, which allows a user to add new products under the existing product categories to the Grocery List. (Note that application as is would not allow to create a new product category.) As you probably noticed, we set up two product categories in our data array of records of type (class) "GroceyItem" called "list". We first build up the list of possible categories, which user will have to select in the Gtk::ComboBox in order to add a new item to the selected category. Then we build the data entry elements to provide the means to enter the products into the list. You shouldn't have too much trouble understanding this first part of this "add_product" method. But the second part (the dialog.run {...}) is where all the fun starts.

In the dialogue itself the button Gtk::Stock::ADD is associated with the Gtk::Dialog::RESPONSE_OK response signal, so we check if user has confirmed their entry with the "Add" button. Next, we have to make sure that all the data requirements are met. Most importantly that the items belong to an available product category. Only then we can proceed further. Otherwise we destroy the data entry dialogue and return to the main loop (the Grocery List window with the tree view scrollbars and the two "+Add" and "-Remove" buttons).

After we know we have legitimate data from the data entry dialogue, we need to prepare to traverse through our tree store searching for the matching product category. We start searching at the top of the tree store:

model = treeview.model
iter = model.iter_first    #<-- same as: iter = model.get_iter("0")

In the first line above we conveniently tuck away the model separating data store from the view (MVC). We use model to get to the starting iterator, either by Gtk::TreeStore#iter_first, or by Gtk::TreeStore#get_iter by supplying the top-level root path "0". Then comes a tricky while loop, where we need to check if the first iteration matches the selected category, and break out of the loop if it does, while on the other hand apply Gtk::TreeIter#next! at the end of the while loop. The API for Gtk::TreeIter#next! tells us that this command returns true if the iterator was successfully advanced to the next rowon the same level,and false otherwise. In this case (if false is returned) the iterator will point again to the first item in the tree store. The important thing to realize about Gtk::TreeIter#next! is that it does not traverse the entire tree. Namely, it does not descend into any children rows!

At this point we should have the iterator that points to our product category (the iter variable points to it), hence, under this product category we now create a new row for our new product. Adding the data to the new empty row is accomplished by:

# child[BUY_INDEX]=buy # same as: model.set_value(child, BUY_INDEX, buy)
child[BUY_INDEX]   = buy
child[QTY_INDEX]   = quantity
child[PROD_INDEX]  = product

It is worth noticing the comment here which tells us that the Gtk::TreeIter#[column] and Gtk::TreeModel#set_value instance methods can be used interchangeably.

Lastly if the user checked the "Buy the product" check button, we need to recalculate the total number of items for the parent (product category). Ouritervariable still points to the product category row, so we update it with the newly added value for our "Count" column, which we unfortunately call "quantity" in our code:

if buy
  qty_value = iter[QTY_INDEX]
  qty_value += quantity
  iter[QTY_INDEX] = qty_value
end

Combo Box

combo-box.png

Since combo boxes provide their own methods to manage additions and removal of their selectable elements, it is not immediately apparent that combo boxes are related to the tree model. Depending on how a combo box is initialized (either with Gtk::ComboBox.new(is_text_only = true) or with Gtk::ComboBox.new(model)), it can be used in two different ways, either with a custom Gtk::TreeModel or with a default model with only a single column of settings. In our example program (tview-selections.rb) above a new Gtk::ComboBox was created with Gtk::ComboBox.new(is_text_only = true), which creates a specialized combo box that contains only one column of strings. This is simply a convenience widget, since it too is managed internally with Gtk::TreeModel. It consists of the methods Gtk::ComboBox#append_text, Gtk::ComboBox#insert_text, Gtk::ComboBox#prepend_text, Gtk::ComboBox#remove_text and Gtk::ComboBox#active_text to bypass the complexity of the tree model.

But combo boxes can also be created with Gtk::ComboBox.new(model), requiring you to create a tree model to hold the selections, which by the way can have multiple columns. This does not assume anything about the content of the tree model or the types of each column. Adding and removing elements to a combo box created with Gtk::ComboBox.new(model) is handled entirely with the tree model. As explained in the API documentation, the Gtk::ComboBox uses the model-view-controller pattern; the list of valid choices is specified in the form of a tree model, and the display of the choices can be adapted to the data in the model by using cell renderers, as you would in a tree view. This is possible since Gtk::ComboBox implements the Gtk::CellLayout interface. The tree model holding the valid choices is not restricted to a flat list, it can be a real tree, and the popup will reflect the tree structure.

Therefore, as we have seen above, the Gtk::ComboBox in addition to the model-view API, offers a simple API which is suitable for text-only combo boxes, and hides the complexity of managing the data in a model.

Removing Multiple Rows

In this part of our application we find two remove methods. The main is the callback method called remove_products. Though most of the actual removal processing is done by the helper method remove_row, there are some important removal issues that need to be addressed when dealing with removing multiple rows, in the first, that is, "remove_products" method. Let us first look at the helper method, which is designed to remove a single row.

remove_row(ref, model):

The first thing to notice about this method is that it receives a reference rather than an iterator or a path to the row in the tree store, which will be removed. It then uses this reference to convert it to the iterator as well as path, first to adjust the value of the quantity total in the parent for the value that will be removed from the selected child row. Finally, the path to the child row is converted to iterator with which the row is removed from the tree store. All these conversions from reference to iterator and path look rather convoluted, however there is a good reason for all this. Namely, passing around ephemeral iterators and "hard-coded" or "burned-in" paths could potentially spell a disaster while inserting and/or removing rows on a massive scale.


The trouble is that as soon as we either insert or remove a row from the tree store all iterators and all paths after the inserted or removed row will be bumped up or down by one respectively. References on the other hand will not change they still point to the same records (rows), regardless of where they are in the store.

remove_products(treeview):

The main removal method has to figure out which item(s) have been selected for removal by the user. Obviously there may be multiple selections. This can be accomplished with the help of the Gtk::TreeSelection object, that is first obtained from the tree view. Next this method gathers all the selected rows and stores their references conveniently adding also the model into the collector array. Once we have references of the selected rows, nothing will change them, except removal will make them disappear. Finally we traverse the collected items passing each individual pair of relevant info to the helper method for the removal. A picture is worth a thousand words, and so is a piece of working code:

def remove_products(treeview)
  # Gtk::TreeRowReference.new(model, path)
  selection = treeview.selection

  paths2rm = Array.new
  selection.selected_each do |mod, path, iter|
    ref = Gtk::TreeRowReference.new(mod, path)
    paths2rm << [ref, mod]
  end
  paths2rm.each { |ref, mod| remove_row(ref, mod) }
end
Tree view removal issues:
Beside simple single level row removal, we also have to be aware of rows with children and perhaps whole hierarchy of children. We have already mentioned on previous pages, that if you remove a parent row that has a hierarchy of children, it too will be removed, i.e. removing a parent that has children, and grand children, all these descendants, as well as any of their descendants will also be removed. Normally, you will not have to wary about rows that have children, however if you have created some dependencies between the rows such as running totals this may become an issue. Shortly we will see the example where such relationships exist in the form of running totals.



Adding, Removing Rows In a Tree View & Imaginary Root Node

Finally, lets also address the two issues, mentioned earlier. The first when introducing the first version of the 'treev-MultiDim-load-asci-table.rb' program example under the title Tedious Job of Loading Multidimensional Tree Store, where we were concerned about adding and removing rows from a multi-level tree view and tree store, and the second about the 'imaginary root node' issue which we encountered in the segment called 'Imaginary, invisible root node'.

The last, 'imaginary root node issue', we have also seen in the above (adding-n-removing-SELECTED-rows.rb) example program, though we did not really expose it. But it will become much more obvious in the next example, when discovering that there exist no way that would allow us to add or create the top level tree view entries. As promised this last example is the extension of the 'treev-MultiDim-load-asci-table.rb' program in which we loaded the tree model and view, from the asci table residing in the separate file as the Ruby module called 'initialize-products-from-asci-table.rb'. We are going to use the same module file here too. What is new is the feature that allows us to add and remove rows. This time however, we use a different row selection mechanism than in the above (adding-n-removing-SELECTED-rows.rb) example program. In this program, when we insert or delete a row, we point to it by clicking on it. When adding a row we are adding a single child to an existing parent row. When removing a row, we are removing the selected row and all its children, as well as all children of children, if they exist.

mD-asciTab-rmNadd-products-s1.png

Before we look at this program more closely, let me first expose its obvious flaws. The most obvious one is the awkward dialogue mechanism used to add and remove new products. A more appropriate thing to do would be to employ a context menu on which the addition and removal would be two separate choices. However, we have not yet introduced the context menu widget, and hence, instead used what you should already be familiar with. Also, we did not care to ensure the parent rows did not allow price column to be filled initially. And the most obvious flaw is that we do not provide a way to enter another top level product. All these issues can be easily fixed, however, the implementation of these fixes would greatly obscure the basic issues we are trying to present in the first place. Nevertheless, the bare bones of this program have already been introduced two chapters back and pointed to at the beginning of this segment. If you gave trouble understanding how the program works I suggest you have a peak back into the 'Using Tree Store' chapter as mentioned above (Tedious Job of Loading Multidimensional Tree Store).

The new addition to this program is hidden from the user, and is only exposed in the top level tooltip, i.e. when a cursor hovers above this program's main window it triggers the tooltip explaining that double-clicking a row will allow adding or removing rows. Things are not that hidden for the programmers, though, nevertheless the additions are not so huge. The bulk of new code is tucked away into two methods: theadd_n_rm_productand thefix_parent_row_prices. This code is triggered by the following signal_connect method:

treeview.signal_connect("row-activated") do |view, path, column|
  add_n_rm_product(treeview, path)
end

The important difference between this and the previous example is how the selected rows are handled in the two. Here, the selection is implicit to the 'row-activated' signal, namely, it (the selection) is passed into the code block and subsequently to the insertion and removal management method as path parameter, owned by the 'row-activated' signal.

Lets look at the listing:

treev-MultiDim-loadASCItable-add-n-rm-rows.rb

#!/usr/bin/env ruby
require 'gtk2'

$: << "."
require "initialize-products-from-asci-table.rb"
include InitializeProductsFromASCItable

product_list = []
INIT_ARRAY.each_with_index do |row, i|
  product_list[i] = Products.new(*row)
end

def setup_tree_view(treeview)
  renderer = Gtk::CellRendererText.new
  column   = Gtk::TreeViewColumn.new("Product Name", renderer, :text => NAME_COLUMN)
  treeview.append_column(column)
  renderer = Gtk::CellRendererText.new
  column   = Gtk::TreeViewColumn.new("Price", renderer, :text => PRICE_COLUMN)
  column.set_cell_data_func(renderer) do |col, renderer, model, iter|
    renderer.text  = "%07.2f" % iter[PRICE_COLUMN]
  end
  treeview.append_column(column)
end

def fix_parent_row_prices(treeview, sel_path, added_price)
  model = treeview.model
  path = sel_path
  while path
    iter = model.get_iter(path)
    iter[PRICE_COLUMN] += added_price
    path = /(.+)(:\d+)/ =~ path ? $1 : $2 # Investigate problems with: "GTk::TreePath#up!"???
  end
end

def add_n_rm_product(treeview, selected_row_path)
  # Create a dialog that will be used to create a new product.

  dialog = Gtk::Dialog.new(
      "Add Or Remove Product(s)",
      nil,
      Gtk::Dialog::MODAL,
      [ Gtk::Stock::DELETE, Gtk::Dialog::RESPONSE_REJECT ],
      [ Gtk::Stock::ADD,    Gtk::Dialog::RESPONSE_OK ],
      [ Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL ]
  )
  # Create widgets that will be packed into the dialog.
  prod_name_entry = Gtk::Entry.new
  price_entry     = Gtk::Entry.new
  price_entry.text = "0"

  table = Gtk::Table.new(4, 2, false)
  table.row_spacings = 5
  table.column_spacings = 5
  table.border_width = 5

  # Pack the table that will hold the dialog widgets.
  fll_shr = Gtk::SHRINK | Gtk::FILL
  fll_exp = Gtk::EXPAND | Gtk::FILL
  table.attach(Gtk::Label.new("Product Name:"),  0, 1, 0, 1, fll_shr, fll_shr,  0, 0)
  table.attach(prod_name_entry,                  1, 2, 0, 1, fll_exp, fll_shr,  0, 0)
  table.attach(Gtk::Label.new("Product Price:"), 0, 1, 1, 2, fll_shr, fll_shr,  0, 0)
  table.attach(price_entry,                      1, 2, 1, 2, fll_exp, fll_shr,  0, 0)

  dialog.vbox.pack_start_defaults(table)
  dialog.show_all

  dialog.run do |response|
    # If the user presses OK, verify the entries and add the product.
    if response == Gtk::Dialog::RESPONSE_OK
      product = prod_name_entry.text
      price = price_entry.text.to_f

      if product == ""
        puts "No product name was entered!"
        puts "DEBUG:  prod=(#{product}), ctg=(#{price})"
        dialog.destroy
        return
      end

      model = treeview.model
      iter = model.get_iter(selected_row_path)
      child = model.append(iter)

      # child[COLUMN]=value # same as: model.set_value(child, COLUMN, value)
      child[NAME_COLUMN]   = product
      child[PRICE_COLUMN]  = price
      fix_parent_row_prices(treeview, selected_row_path, price) if price > 0
    elsif response == Gtk::Dialog::RESPONSE_REJECT

      model = treeview.model
      iter = model.get_iter(selected_row_path)
      removed_price = -iter[PRICE_COLUMN]
      puts "DELETING: #{iter[NAME_COLUMN]} ---> (#{removed_price})" 
      fix_parent_row_prices(treeview, selected_row_path, removed_price)

      model.remove(iter)

    end
    dialog.destroy
  end
end

def load_products(store, product_list, parent)

  # Add all of the products to the GtkTreeStore.
  product_list.each do |prod_obj|

    # If the product has children it's a parent
    if (prod_obj.children)
      # Add the category as a new root (parent) row (element).
      child_parent = store.append(parent)
      # store.set_value(parent, COL, value) # <= same as below
      child_parent[NAME_COLUMN]   = prod_obj.product
      child_parent[PRICE_COLUMN]  = prod_obj.price
      load_products(store, prod_obj.children, child_parent)

    # Otherwise, add the product as a child row of the category.
    else
      child = store.append(parent)
      # store.set_value(child, COL, value) # <= same as below
      child[NAME_COLUMN]   = prod_obj.product
      child[PRICE_COLUMN]  = prod_obj.price
      next
    end
  end
end

NAME_COLUMN, PRICE_COLUMN = 0, 1
treeview = Gtk::TreeView.new(store = Gtk::TreeStore.new(String, Float))
treeview.tooltip_text = "You can add new or remove existing rows\n" +
                        "by double-clicking on an existing row.\n\n" +
                        "(New rows will apear as children rows, while\n" +
                        "removing a row removes also its children.)"
load_products(store, product_list, nil)

treeview.signal_connect("row-activated") do |view, path, column|
  add_n_rm_product(treeview, path)
end
setup_tree_view(treeview)

window = Gtk::Window.new("Tree View Depth Demo")
window.resizable = true
window.border_width = 10
window.signal_connect('destroy') { Gtk.main_quit }
window.set_size_request(400, -1)
scrolled_win = Gtk::ScrolledWindow.new
scrolled_win.add(treeview)
scrolled_win.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
window.add(scrolled_win)
window.show_all
Gtk.main

As opposed to the earlier example program, adding a row here does not require to search for the category, or the parent row. Here any selected row becomes a potential parent. In real life program, this perhaps would be implemented more restrictively or for a tutorial like ours here prohibitively complicated with a much larger overhead, converting the selected leaf objects into the parent row holding the its initial contents as a child. So we opted for a very simplistic solution, only to convey the mechanics of converting a leaf row into a parent row with children. The cost of doing so is a bit sloppy handling of the initial price attribute for the parent row.

You can safely ignore the following comment:
(If you add a child called 'Key Pad' with the additional price $20.0, to the existing leaf product 'Keyboard' which originally had the price $50.0, the insertion would simply create a single child for the newly promoted parent, correctly adding the child's price the original price of the keyboard ($50.0), so the total value of the new keyboard would now be $70.0. The value of any other higher level totals as well as the grand total for the top level product would also be correctly updated.)

The accumulative nature of the product prices is handled with thefix_parent_row_pricesmethod, which is used for both additions and removal of products from our tree view. Programmer only needs to supply negative price argument to the method when removing a product. (Note, that the model is not effected by these price manipulation processes.)

Check if children are removed along with the parent:

In the early days, when the API documentation was scarce, I had to check that no children dangle around, when their parents were removed from the tree. This is also a reasonable exercise for a beginner GTK+ developer. Here is the original removal code from our example program:

elsif response == Gtk::Dialog::RESPONSE_REJECT
  model = treeview.model
  iter = model.get_iter(selected_row_path)
  removed_price = -iter[PRICE_COLUMN]
  puts "DELETING: #{iter[NAME_COLUMN]} ---> (#{removed_price})" 
  fix_parent_row_prices(treeview, selected_row_path, removed_price)
  model.remove(iter)
end

In order to check, if children are removed, we need to remember their position in the tree view before we remove their parent. We learned that the proper way to do this is to store the references to the pertinent row paths. We will not check for more than the first generation of descendants. Lets see how the above code is augmented in order to perform our test:

elsif response == Gtk::Dialog::RESPONSE_REJECT

  model = treeview.model
  iter = model.get_iter(selected_row_path)
  removed_price = -iter[PRICE_COLUMN]
  puts "DELETING: #{iter[NAME_COLUMN]} ---> (#{removed_price})" 
  fix_parent_row_prices(treeview, selected_row_path, removed_price)

  rmrow_refs = []
  if iter.has_child?
    c_iter = iter.first_child
    i = 0
    begin
      i += 1
      puts "Child ##{i} #{c_iter[NAME_COLUMN]} to be removed"
      rmrow_refs << Gtk::TreeRowReference.new(model, c_iter.path)
    end while c_iter.next!
  end

  model.remove(iter)

  rmrow_refs.each do |rowref|
    if (rmrow_ref_path = rowref.path)
      puts "#{model.get_iter(rmrow_ref_path)[NAME_COLUMN]}"
    else
      puts "After remove rowref.path=[#{rowref.path}] reference does not exist"
    end
  end

end



Finally in this session, let us look at theimaginary, invisible root node.Try to add another product, say a 'Scanner Printer SP100' to the list. You will soon realize that it can only be added as a "sub-product" of any of the three existing products. There simply is no row visible, which would contain these products as children.

There are two solutions to this problem. The easiest would be to add one higher level "Inventory" or "Computer Store" row to the ASCII table. This would not require any additional coding at all. But a better way would be to implement a context menu, with a new option, say: 'Generate New Product'. Now, a minor addition to the existing strategy would be the requirement that, when this option from the context menu is selected, we have a method like the following, and that it gets executed when 'Generate New Product' option from a context menu or some other button is clicked:

def generate_new_product(treeview, product)
  iter = treeview.model.append(nil)
  iter[NAME_COLUMN]   = product
  iter[PRICE_COLUMN]  = 0.0
  iter
end

In the above code thenilargument in Gtk::TreeStore#append(nil) is the most important thing. It creates a child for the elusive top levelimaginary, invisible root node.

Last modified:2012/11/24 12:02:58
Keyword(s):
References:[tut-gtk2-treev-crs] [tut-gtk] [tut-gtk2-treev-trees] [tut-gtk2-treev]