14.9. Arrastrar y Soltar en TreeView

14.9.1. Reordenación mediante Arrastrar y Soltar

La reordenación de las filas de un TreeView (y de las filas del modelo de árbol subyacente) se activa usando el método set_reorderable() que se mencionó previamente. El método set_reorderable() fija la propiedad "reorderable" al valor especificado y permite o impide arrastrar y soltar en las filas del TreeView. Cuando la propiedad "reorderable" es TRUE es posible arrastar internamente filas del TreeView y soltarlas en una nueva posición. Esta acción provoca que las filas del TreeModel subyacente se reorganicen para coincidir con la nueva situación. La reordenación mediante arrastrar y soltar de filas funciona únicamente con almacenes no ordenados.

14.9.2. Arrastar y Soltar Externo

Si se quiere controlar el arrastar y soltar o tratar con el arrastrar y soltar desde fuentes externas de datos es necesario habilitar y controlar el arrastar y soltar con los siguientes métodos:

  treeview.enable_model_drag_source(start_button_mask, targets, actions)
  treeview.enable_model_drag_dest(targets, actions)

Estos métodos permiten utilizar filas como fuente de arrastre y como lugar para soltar respectivamente. start_button_mask es una máscara de modificación (véase referencia de constantes gtk.gtk Constants en el Manual de Referencia de PyGTK ) que especifica los botones o teclas que deben ser pulsadas para iniciar la operación de arrastre. targets es una lista de tuplas de 3 elementos que describen la información del objetivo que puede ser recibido o dado. Para que tenga éxito el arrastar o soltar, por lo menos uno de los objetivos debe coincidir en la fuente o destino del arrastre (p.e. el objetivo "STRING"). Cada tupla de 3 elementos del objetivo contiene el nombre del objetivo, banderas (una combinación de gtk.TARGET_SAME_APP y gtk.TARGET_SAME_WIDGET o ninguno) y un identificador entero único. actions describe cuál debería ser el resultado de la operación:

gtk.gdk.ACTION_DEFAULT, gtk.gdk.ACTION_COPY

Copiar los datos.

gtk.gdk.ACTION_MOVE

Mover los datos, es decir, primero copiarlos, luego borrarlos de la fuente utilizando el objetivo DELETE del protocolo de selecciones de las X.

gtk.gdk.ACTION_LINK

Añadir un enlace a los datos. Nótese que esto solamente es de utilidad si la fuente y el destino coinciden en el significado.

gtk.gdk.ACTION_PRIVATE

Acción especial que informa a la fuente que el destino va a hacer algo que el destino no comprende.

gtk.gdk.ACTION_ASK

Pide al usuario qué hacer con los datos.

Por ejemplo para definir un destino de un arrastrar y soltar:

  treeview.enable_model_drag_dest([('text/plain', 0, 0)],
                  gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE)

Entonces habrá que gestionar la señal del control Widget "drag-data-received" para recibir los datos recibidos - tal vez sustituyendo los datos de la fila en la que se soltó el contenido. La signatura de la retrollamada de la señal "drag-data-received" es:

  def callback(widget, drag_context, x, y, selection_data, info, timestamp)

donde widget es el TreeView, drag_context es un DragContext que contiene el contexto de la selección, x e y son la posición en dónde ocurrió el soltar, selection_data es la SelectionData que contiene los datos, info es un entero identificador del tipo, timestamp es la hora en la que sucedió el soltar. La fila puede ser identificada llamando al método:

  drop_info = treeview.get_dest_row_at_pos(x, y)

donde (x, y) es la posición pasada a la función de retrollamada y drop_info es una tupla de dos elementos que contiene el camino de una fila y una constante de posición que indica donde se produce el soltar respecto a la fila: gtk.TREE_VIEW_DROP_BEFORE, gtk.TREE_VIEW_DROP_AFTER, gtk.TREE_VIEW_DROP_INTO_OR_BEFORE o gtk.TREE_VIEW_DROP_INTO_OR_AFTER. La función de retrollamada podría ser algo parecido a:

  treeview.enable_model_drag_dest([('text/plain', 0, 0)],
                  gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE)
  treeview.connect("drag-data-received", drag_data_received_cb)
  ...
  ...
  def drag_data_received_cb(treeview, context, x, y, selection, info, timestamp):
      drop_info = treeview.get_dest_row_at_pos(x, y)
      if drop_info:
          model = treeview.get_model()
          path, position = drop_info
          data = selection.data
          # do something with the data and the model
          ...
      return
  ...

Si una fila se usa como fuente para arrastar debe manejar la señal de Widget "drag-data-get" que llena una selección con los datos que se devolverán al destino de arrastar y soltar con una función de retrollamada con la signatura:

  def callback(widget, drag_context, selection_data, info, timestamp)

Los parámetros de callback son similares a los de la función de retrollamada de "drag-data-received". Puesto que a la retrollamada no se le pasa un camino de árbol o una forma sencilla de obtener información sobre la fila que está siendo arrastrada, asumiremos que la fila que está siendo arrastrada está seleccionada y que el modo de selección es gtk.SELECTION_SINGLE o gtk.SELECTION_BROWSE de modo que podemos tener la fila obteniendo la TreeSelection, el modelo y el iterador TreeIter que apunta a la fila. Por ejemplo, se podría pasar texto así::

  ...
  treestore = gtk.TreeStore(str, str)
  ...
  treeview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
                  [('text/plain', 0, 0)],
                  gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE)
  treeview.connect("drag-data-get", drag_data_get_cb)
  ...
  ...
  def drag_data_get_cb(treeview, context, selection, info, timestamp):
      treeselection = treeview.get_selection()
      model, iter = treeselection.get_selected()
      text = model.get_value(iter, 1)
      selection.set('text/plain', 8, text)
      return
  ...

Un TreeView puede ser desactivado como fuente o destino para arrastrar y soltar utilizando los métodos:

  treeview.unset_rows_drag_source()
  treeview.unset_rows_drag_dest()

14.9.3. Ejemplo de Arrastrar y Soltar en TreeView

Es necesario un ejemplo para unir las piezas de código descritas más arriba. Este ejemplo (treeviewdnd.py) es una lista en la que se pueden arrastrar y soltar URLs. También se pueden reordenar las URLs de la lista arrastrando y soltando en el interior del TreeView. Un par de botones permiten limpiar la listar y eliminar un elemento seleccionado.

    1   #!/usr/bin/env python
    2
    3   # example treeviewdnd.py
    4
    5   import pygtk
    6   pygtk.require('2.0')
    7   import gtk
    8
    9   class TreeViewDnDExample:
   10
   11       TARGETS = [
   12           ('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_WIDGET, 0),
   13           ('text/plain', 0, 1),
   14           ('TEXT', 0, 2),
   15           ('STRING', 0, 3),
   16           ]
   17       # close the window and quit
   18       def delete_event(self, widget, event, data=None):
   19           gtk.main_quit()
   20           return gtk.FALSE
   21
   22       def clear_selected(self, button):
   23           selection = self.treeview.get_selection()
   24           model, iter = selection.get_selected()
   25           if iter:
   26               model.remove(iter)
   27           return
   28
   29       def __init__(self):
   30           # Create a new window
   31           self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
   32
   33           self.window.set_title("URL Cache")
   34
   35           self.window.set_size_request(200, 200)
   36
   37           self.window.connect("delete_event", self.delete_event)
   38
   39           self.scrolledwindow = gtk.ScrolledWindow()
   40           self.vbox = gtk.VBox()
   41           self.hbox = gtk.HButtonBox()
   42           self.vbox.pack_start(self.scrolledwindow, True)
   43           self.vbox.pack_start(self.hbox, False)
   44           self.b0 = gtk.Button('Clear All')
   45           self.b1 = gtk.Button('Clear Selected')
   46           self.hbox.pack_start(self.b0)
   47           self.hbox.pack_start(self.b1)
   48
   49           # create a liststore with one string column to use as the model
   50           self.liststore = gtk.ListStore(str)
   51
   52           # create the TreeView using liststore
   53           self.treeview = gtk.TreeView(self.liststore)
   54
   55          # create a CellRenderer to render the data
   56           self.cell = gtk.CellRendererText()
   57
   58           # create the TreeViewColumns to display the data
   59           self.tvcolumn = gtk.TreeViewColumn('URL', self.cell, text=0)
   60
   61           # add columns to treeview
   62           self.treeview.append_column(self.tvcolumn)
   63           self.b0.connect_object('clicked', gtk.ListStore.clear, self.liststore)
   64           self.b1.connect('clicked', self.clear_selected)
   65           # make treeview searchable
   66           self.treeview.set_search_column(0)
   67
   68           # Allow sorting on the column
   69           self.tvcolumn.set_sort_column_id(0)
   70
   71           # Allow enable drag and drop of rows including row move
   72           self.treeview.enable_model_drag_source( gtk.gdk.BUTTON1_MASK,
   73                                                   self.TARGETS,
   74                                                   gtk.gdk.ACTION_DEFAULT|
   75                                                   gtk.gdk.ACTION_MOVE)
   76           self.treeview.enable_model_drag_dest(self.TARGETS,
   77                                                gtk.gdk.ACTION_DEFAULT)
   78
   79           self.treeview.connect("drag_data_get", self.drag_data_get_data)
   80           self.treeview.connect("drag_data_received",
   81                                 self.drag_data_received_data)
   82
   83           self.scrolledwindow.add(self.treeview)
   84           self.window.add(self.vbox)
   85           self.window.show_all()
   86
   87       def drag_data_get_data(self, treeview, context, selection, target_id,
   88                              etime):
   89           treeselection = treeview.get_selection()
   90           model, iter = treeselection.get_selected()
   91           data = model.get_value(iter, 0)
   92           selection.set(selection.target, 8, data)
   93
   94       def drag_data_received_data(self, treeview, context, x, y, selection,
   95                                   info, etime):
   96           model = treeview.get_model()
   97           data = selection.data
   98           drop_info = treeview.get_dest_row_at_pos(x, y)
   99           if drop_info:
  100               path, position = drop_info
  101               iter = model.get_iter(path)
  102               if (position == gtk.TREE_VIEW_DROP_BEFORE
  103                   or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE):
  104                   model.insert_before(iter, [data])
  105               else:
  106                   model.insert_after(iter, [data])
  107           else:
  108               model.append([data])
  109           if context.action == gtk.gdk.ACTION_MOVE:
  110               context.finish(True, True, etime)
  111           return
  112
  113   def main():
  114       gtk.main()
  115
  116   if __name__ == "__main__":
  117       treeviewdndex = TreeViewDnDExample()
  118       main()

El resultado de la ejecución del programa de ejemplo treeviewdnd.py se ilustra en Figura 14.8, “Ejemplo de Arrastrar y Soltar en TreeView”:

Figura 14.8. Ejemplo de Arrastrar y Soltar en TreeView

Ejemplo de Arrastrar y Soltar en TreeView

La clave para permitir tanto arrastrar y soltar externo como la reorganización interna de filas es la organización de los objetivos (el atributo TARGETS de la línea 11). Se crea y usa un objetivo específico de la aplicación (MY_TREE_MODEL_ROW) para indicar un arrastar y soltar dentro del TreeView estableciendo la bandera gtk.TARGET_SAME_WIDGET. Estableciendo éste como el primer objetivo para el destino del arrastre hará que se intente hacerlo coincidir primero con los objetivos del origen de arrastre. Después, las acciones de fuente de arrastre deben incluir gtk.gdk.ACTION_MOVE y gtk.gdk.ACTION_DEFAULT (véanse las líneas 72-75). Cuando el destino recibe los datos de la fuente, si la acción DragContext es gtk.gdk.ACTION_MOVE, entonces se indica a la fuente que borre los datos (en este caso la fila) llamando al método del DragContext finish() (véanse las líneas 109-110). Un TreeView proporciona un conjunto de funciones internas que estamos aprovechando para arrastrar, soltar y eliminar los datos.