Skip to content

Instantly share code, notes, and snippets.

@achadwick
Created March 1, 2015 21:30
Show Gist options
  • Save achadwick/cea0836d1606efe32e16 to your computer and use it in GitHub Desktop.
Save achadwick/cea0836d1606efe32e16 to your computer and use it in GitHub Desktop.
Draggable treemodel (test apparent regression between gtk 3.12 and 3.14)
#!/usr/bin/python
# Custom draggable TreeModel demo code
# Author: Andrew Chadwick <[email protected]>
# To the extent possible under law, the author has waived all copyright
# and related or neighboring rights to this work.
# https://creativecommons.org/publicdomain/zero/1.0/
## Module imports
import logging
logger = logging.getLogger(__name__)
import gi
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import Pango
from gi.repository import GObject
## Class defs
class CustomListModel (GObject.GObject,
Gtk.TreeDragSource,
Gtk.TreeDragDest,
Gtk.TreeModel):
## Initialization
def __init__(self, rows):
super(CustomListModel, self).__init__()
rows = list(rows)
for row in rows:
self._check_row(row)
self._rows = rows
self._drag = None
## GtkTreeModel vfunc implementation
def do_get_flags(self):
return Gtk.TreeModelFlags.LIST_ONLY
def do_get_n_columns(self):
return 2
def do_get_column_type(self, n):
return (int, str)[n]
def do_get_iter(self, path):
path_indices = path.get_indices()
index = path_indices[0]
if index < 0 or index >= len(self._rows):
return (False, None)
else:
it = Gtk.TreeIter()
it.user_data = index
return (True, it)
def do_get_path(self, it):
return Gtk.TreePath([it.user_data])
def do_get_value(self, it, column):
index = it.user_data
assert 0 <= index < len(self._rows)
assert 0 <= column < 2
return self._rows[index][column]
def do_iter_next(self, it):
index = it.user_data
index += 1
if index >= len(self._rows):
return False
else:
it.user_data = index
return True
def do_iter_previous(self):
if it.user_data <= 0:
return False
else:
it.user_data -= 1
return True
def do_iter_children(self, parent):
if parent is None:
it = Gtk.TreeIter()
it.user_data = 0
return (True, it)
else:
return (False, None)
def do_iter_has_child(self, it):
return it is None
def do_iter_n_children(self, it):
if it is None:
return len(self._rows)
else:
return 0
def do_iter_nth_child(self, parent, n):
if parent is not None or n >= len(self._rows):
return (False, None)
elif parent is None:
it = Gtk.TreeIter()
it.user_data = n
return (True, it)
def do_iter_parent(self, child):
return (False, None)
## GtkTreeDragSourceIface vfunc implementation
def do_row_draggable(self, path):
"""Checks whether a row can be dragged"""
logger.info("do_row_draggable: %r", tuple(path))
i = tuple(path)[0]
return i >= 0 and i < len(self._rows)
def do_drag_data_get(self, path, selection_data):
"""Extracts source row data for a view's active drag"""
logger.info("do_drag_data_get: %r", tuple(path))
# HACK: fill in the GtkSelectionData so that the drag protocol
# can proceed. Need atomicity/undoability though, so fill in
# details during the protocol exchange.
Gtk.tree_set_row_drag_data(selection_data, self, path)
self._drag = {
"src": tuple(path),
"targ": None,
}
return True
def do_drag_data_delete(self, path):
"""Final deletion stage in the high-level DnD protocol"""
logger.info("do_drag_data_delete: %r", tuple(path))
del_path = tuple(path)
if self._drag is None:
logger.error("failed: no active _drag")
return False
src_path = self._drag.get("src")
targ_path = self._drag.get("targ")
self._drag = None
if del_path != src_path:
logger.error("failed: weird! Expect del_path == src_path")
return False
# What the user actually wanted instead of this huge broken API *sigh*
self._row_dragged(src_path, targ_path)
def _row_dragged(self, p1, p2):
logger.info("**ROW_DRAGGED** %r %r", p1, p2)
logger.info("(not going to actually reorder the model here)")
## GtkTreeDragDestIface vfunc implementation
def do_row_drop_possible(self, path, selection_data):
"""Checks whether a row can be dragged"""
logger.info("do_drag_row_drop_possible: %r", tuple(path))
if self._drag is None:
logger.error("failed: no active _drag")
return False
path = tuple(path)
i = path[0]
return i >= 0 and i <= len(self._rows)
def do_drag_data_received(self, path, selection_data):
"""Receives data at the drop phase of the DnD proto"""
logger.info("do_drag_data_received: %r", tuple(path))
# gtk_tree_get_row_drag_data turns out to be quite buggy in GTK
# 3.12, often screwing up the view's idea of tree even when it's
# not changed. Another reason to build up details as we go.
if self._drag is None:
logger.error("failed: no active _drag")
return False
self._drag["targ"] = tuple(path)
return True
## App-specific functions
def add_row(self, row, path=None):
self._check_row(row)
if path is None:
nrows = len(self._rows)
path = Gtk.TreePath([nrows])
self._rows.append(row)
else:
insert_idx = path.get_indices()[0]+1
self._rows.insert(insert_idx, row)
it = self.get_iter(path)
self.row_inserted(path, it)
def _check_row(self, row):
assert len(row) == 2
assert isinstance(row[0], int)
assert isinstance(row[1], str)
class DemoApp (object):
## Sample data
_LIST_DATA = [ [0, "zero"],
[1, "one"],
[2, "two"],
[3, "three"],
[4, "four"],
[5, "five"],
[6, "six"],
[7, "seven"],
[8, "eight"],
[9, "nine"],
[10, "ten"],
[11, "eleven"] ]
## Initialization
def __init__(self):
super(DemoApp, self).__init__()
model = self._init_custom_store()
self._model = model
view = Gtk.TreeView(model)
view.set_reorderable(True)
self._view = view
cell = Gtk.CellRendererText()
cell.set_property("ellipsize", Pango.EllipsizeMode.END)
col = Gtk.TreeViewColumn("#", cell, text=0)
view.append_column(col)
cell = Gtk.CellRendererText()
cell.set_property("ellipsize", Pango.EllipsizeMode.END)
col = Gtk.TreeViewColumn("Name", cell, text=1)
view.append_column(col)
view_scroll = Gtk.ScrolledWindow()
view_scroll.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
pol = Gtk.PolicyType.AUTOMATIC
view_scroll.set_policy(pol, pol)
view_scroll.add(view)
view_scroll.set_size_request(150, 100)
view_scroll.set_vexpand(True)
view_scroll.set_hexpand(True)
add_row_btn = Gtk.Button("Add Row")
add_row_btn.connect("clicked", self._add_row_clicked_cb)
add_row_btn.set_vexpand(False)
add_row_btn.set_hexpand(True)
grid = Gtk.Grid()
grid.attach(view_scroll, 0, 0, 1, 1)
grid.attach(add_row_btn, 0, 1, 1, 1)
win = Gtk.Window()
win.connect("destroy", Gtk.main_quit)
win.add(grid)
win.set_default_size(250, 300)
self._win = win
def _init_custom_store(self):
"""Create and return a CustomListModel"""
return CustomListModel(self._LIST_DATA)
def _init_liststore(self):
"""Create and return an unsurprising ListStore model"""
model = Gtk.ListStore(GObject.TYPE_INT, GObject.TYPE_STRING)
for row in self._LIST_DATA:
model.append(row)
return model
## Callbacks
def _add_row_clicked_cb(self, btn):
selection = self._view.get_selection()
model, selected_paths = selection.get_selected_rows()
ins_path = None
pstr = "hello"
pnum = -1
if selected_paths:
ins_path = selected_paths[0]
pstr = "after row %s" % (ins_path.to_string(),)
pnum = ins_path.get_indices()[0]
model.add_row([pnum, pstr], path=ins_path)
## Runtime
def run(self):
self._win.show_all()
Gtk.main()
## Testing
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
app = DemoApp()
app.run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment