Created
March 1, 2015 21:30
-
-
Save achadwick/cea0836d1606efe32e16 to your computer and use it in GitHub Desktop.
Draggable treemodel (test apparent regression between gtk 3.12 and 3.14)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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