Skip to content

Instantly share code, notes, and snippets.

@virtuald
Last active August 16, 2016 06:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save virtuald/c03602dc7218ca0ecd3c to your computer and use it in GitHub Desktop.
Save virtuald/c03602dc7218ca0ecd3c to your computer and use it in GitHub Desktop.
Buggy DnD behavior in PyGObject (fix at https://github.com/virtuald/pygi-treeview-dnd)
#!/usr/bin/env python
#
# Demonstrates potential buggy behavior in PyGObject+Treeview DnD
#
# Bug is that when do_drag_data_get is called, the selection_data argument
# appears to be discarded and never used, regardless of the return value
#
# This sample program should be executed like so:
#
# python treetest.py ~/some/path/*
#
# And it will display the files in the treeview. Try to drag them around,
# and notice that MyTreeModel.drag_data_received never gets called.
#
import os.path
import sys
from gi.repository import Gio
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import GObject
targets = [
('text/plain', 0, 0) # have tried various combinations of these
]
class MyTreeView(Gtk.TreeView):
def __init__(self, args):
Gtk.TreeView.__init__(self)
self.model = MyTreeViewModel(GObject.TYPE_STRING, GObject.TYPE_STRING)
self.set_model(self.model)
self.append_column(Gtk.TreeViewColumn('name', Gtk.CellRendererText(), text=0))
self.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
targets,
Gdk.DragAction.MOVE)
self.enable_model_drag_dest(targets,
Gdk.DragAction.MOVE)
for row in args:
self.model.append(row)
#
# Debug code:
#
# If you enable this and call drag_data_get(), the selection data
# object is different than the one passed to MyTreeViewModel.do_drag_data_get,
# and doesn't contain any data that was set there
#
if False:
def do_drag_data_get(self, context, selection_data, info, time_):
print 'MyTreeView.do_drag_data_get', selection_data
# Call through to our model, just like the default impl
model = self.get_model()
retval = model.drag_data_get(self.get_path_at_pos(10, 10)[0],
selection_data)
print 'Returned', retval, selection_data.get_uris()
return retval
#
# Debug code:
#
# When enabled, this *does* get called, but the selection data is empty
# so of course the default implementation will never call
# TreeModel.do_drag_data_received
#
if False:
def do_drag_data_received(self, context, x, y, selection_data, info, time_):
print 'MyTreeView.do_drag_data_received', selection_data, selection_data.get_uris()
return True
class MyTreeViewModel(Gtk.ListStore):
#
# TreeDragSource
#
def do_drag_data_get(self, path, selection_data):
print 'MyTreeViewModel.do_drag_data_get', selection_data
uri = self[path][1]
print 'setting uri ', uri
print 'retval of set_text:', selection_data.set_text(uri, -1)
print 'just checking ', selection_data.get_text()
return True
def do_row_draggable(self, path):
print 'do_row_draggable'
return True
def do_drag_data_delete(self, path):
# This never gets called, presumably because the drag never succeeds
print 'do_drag_data_delete'
return True
#
# TreeDragDest -- these never get called
#
def do_drag_data_received(self, dest_path, selection_data):
print 'MyTreeViewModel.do_drag_data_received', selection_data
return True
def do_row_drop_possible(self, dest_path, selection_data):
print 'is possible?'
return True
if __name__ == '__main__':
args = []
#
# Grab file arguments to put into TreeView
#
import glob
def do(a):
args.append((os.path.basename(a), Gio.File.new_for_commandline_arg(a).get_uri()))
for arg in sys.argv[1:]:
if '*' in arg:
for a in glob.glob(arg):
do(a)
else:
do(arg)
#
# Construct demo window
#
window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL)
sw = Gtk.ScrolledWindow.new()
tv = MyTreeView(args)
sw.add(tv)
window.add(sw)
window.connect('destroy', Gtk.main_quit)
window.set_default_size(400, 300)
window.show_all()
#import sigint
#with sigint.InterruptibleLoopContext(Gtk.main_quit):
Gtk.main()
//
// This version works, and it's mostly the same as the python version
//
using Gtk;
int main(string[] args) {
Gtk.init(ref args);
var window = new Gtk.Window();
window.destroy.connect(Gtk.main_quit);
window.set_default_size(500, 500);
var sw = new Gtk.ScrolledWindow(null, null);
window.add(sw);
string[] items = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J"};
var list = new SimpleList(items);
sw.add(list);
window.show_all();
Gtk.main();
return 0;
}
class SimpleList: Gtk.TreeView {
public SimpleList(string[] items) {
var column = new Gtk.TreeViewColumn.with_attributes(
"Item", new Gtk.CellRendererText(),
"text", 0);
append_column(column);
model = new SimpleListModel(items);
const Gtk.TargetEntry[] targets = {{"text/plain", 0, 0}};
enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, targets, Gdk.DragAction.MOVE);
enable_model_drag_dest(targets, Gdk.DragAction.MOVE);
}
}
class SimpleListModel: Gtk.ListStore, Gtk.TreeDragSource, Gtk.TreeDragDest {
public SimpleListModel(string[] items) {
set_column_types({typeof(string)});
Gtk.TreeIter iter;
foreach (var item in items) {
append(out iter);
set_value(iter, 0, item);
}
}
// TreeDragSource
public bool row_draggable(Gtk.TreePath path) {
stdout.printf("row_draggable\n");
return true;
}
public bool drag_data_get(Gtk.TreePath path, Gtk.SelectionData selection_data) {
Gtk.TreeIter iter;
get_iter(out iter, path);
Value text;
get_value(iter, 0, out text);
selection_data.set_text(text.get_string(), -1);
stdout.printf("drag_data_get: sending %s\n", selection_data.get_text());
return true;
}
public bool drag_data_delete(Gtk.TreePath path) {
stdout.printf("drag_data_delete\n");
return true;
}
// TreeDragDest
public bool row_drop_possible(Gtk.TreePath path, Gtk.SelectionData selection_data) {
stdout.printf("row_drop_possible\n");
return true;
}
public bool drag_data_received(Gtk.TreePath path, Gtk.SelectionData selection_data) {
stdout.printf("drag_data_received: received %s\n", selection_data.get_text());
return true;
}
}
#!/usr/bin/env python
#
# Copyright (C) 2016 Dustin Spicuzza, LGPL license
#
# Demonstrates hacky workaround for buggy behavior in PyGObject+Treeview DnD
#
# Bug is that when do_drag_data_get is called, the selection_data argument
# is a copy, so the treeview caller never receives the modifications made to
# the selection data. This hacky solution uses ctypes + gobject introspection
# to find the correct vfunction in the class, and put our own ctypes callback
# function that has accesses the selection data directly.
#
# This sample program should be executed like so:
#
# python treetest.py ~/some/path/*
#
# And it will display the files in the treeview.
#
import os.path
import sys
import gi
gi.require_version('Gdk', '3.0')
gi.require_version('Gtk', '3.0')
gi.require_version('GIRepository', '2.0')
from gi.repository import Gio
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import Gtk
from gi.repository import GObject
targets = [
('text/plain', 0, 0)
]
#
# Begin hackyness
#
from gi.types import GObjectMeta
from gi.repository import GIRepository
import ctypes
# Load the libraries we need via GIRepository
def _get_shared_library(n):
repo = GIRepository.Repository.get_default()
return repo.get_shared_library(n).split(',')[0]
def _fn(dll, name, args, res=None):
fn = getattr(dll, name)
fn.restype = res
fn.argtypes = args
return fn
_gobject_dll = ctypes.CDLL(_get_shared_library('GObject'))
_gtk_dll = ctypes.CDLL(_get_shared_library('Gtk'))
g_value_set_object = _fn(_gobject_dll, 'g_value_set_object',
(ctypes.c_void_p, ctypes.c_void_p))
g_value_set_static_boxed = _fn(_gobject_dll, 'g_value_set_static_boxed',
(ctypes.c_void_p, ctypes.c_void_p))
gtk_selection_data_get_data_type = _fn(_gtk_dll, 'gtk_selection_data_get_data_type',
(ctypes.c_void_p,), ctypes.c_void_p)
gtk_selection_data_set = _fn(_gtk_dll, 'gtk_selection_data_set',
(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_int))
_drag_data_get_func = ctypes.CFUNCTYPE(ctypes.c_bool,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p)
def drag_data_get_thunk(cls):
'''
Constructs a custom thunk function for each class
This is in a closure so that we can use the class to construct a
GObject.Value for converting the widget object
'''
def _drag_data_get(raw_widget, raw_path, raw_selection_data):
'''
Static function that gets inserted as the drag_data_get vfunction,
instead of using the vfunction implementation provided by pygobject
This function exists so that we can modify the raw selection data
explicitly, which allows the high-level DnD API to work.
'''
# It turns out that GValue is really useful for converting raw pointers
# to/from python objects. We use it + ctypes to xform the values
# Grab the python wrapper for the widget instance
gv = GObject.Value(cls)
g_value_set_object(hash(gv), raw_widget)
widget = gv.get_object()
# Convert the path to a python object via GValue
v1 = GObject.Value(Gtk.TreePath)
g_value_set_static_boxed(ctypes.c_void_p(hash(v1)), raw_path)
path = v1.get_boxed()
# Convert the selection data too
v2 = GObject.Value(Gtk.SelectionData)
g_value_set_static_boxed(ctypes.c_void_p(hash(v2)), raw_selection_data)
selection_data = v2.get_boxed()
# Call the original virtual function with the converted arguments
retval = widget.do_drag_data_get(path, selection_data)
# At this point, selection_data has the information, but it's still just
# a copy. Copy the data back to the original selection data
data = selection_data.get_data()
gtk_selection_data_set(raw_selection_data,
gtk_selection_data_get_data_type(hash(selection_data)),
selection_data.get_format(),
data, len(data))
return retval
return _drag_data_get_func(_drag_data_get)
# GIRepository.vfunc_info_get_address almost does this... but it derefs the
# address, so it cannot be used for our purposes
def vfunc_info_get_address(vfunc_info, gtype):
container_info = vfunc_info.get_container()
if container_info.get_type() == GIRepository.InfoType.OBJECT:
object_info = container_info
interface_info = None
struct_info = GIRepository.object_info_get_class_struct(object_info)
else:
interface_info = container_info
object_info = None
struct_info = GIRepository.interface_info_get_iface_struct(interface_info)
field_info = GIRepository.struct_info_find_field(struct_info, vfunc_info.get_name())
if field_info is None:
raise AttributeError("Could not find struct field for vfunc")
implementor_class = GObject.type_class_ref(gtype)
if object_info:
implementor_vtable = implementor_class
else:
interface_type = GIRepository.registered_type_info_get_g_type(interface_info)
implementor_vtable = GObject.type_interface_peek(implementor_class, interface_type)
offset = GIRepository.field_info_get_offset(field_info)
return hash(implementor_vtable) + offset
class HackedMeta(GObjectMeta):
def __init__(cls, name, bases, dict_):
# Let GObjectMeta do it's initialization
GObjectMeta.__init__(cls, name, bases, dict_)
do_drag_data_get = dict_.get('do_drag_data_get')
if do_drag_data_get:
repo = GIRepository.Repository.get_default()
for base in cls.__mro__:
typeinfo = repo.find_by_gtype(base.__gtype__)
if typeinfo:
vfunc = GIRepository.object_info_find_vfunc_using_interfaces(typeinfo, 'drag_data_get')
if vfunc:
break
else:
raise AttributeError("Could not find vfunc for drag_data_get")
# Get the address of the vfunc so we can put our own callback in there
address = vfunc_info_get_address(vfunc[0], cls.__gtype__)
if address == 0:
raise AttributeError("Could not get address for drag_data_get")
# Make a thunk function closure, store it so it doesn't go out of scope
do_drag_data_get._thunk = drag_data_get_thunk(cls)
# Don't judge me... couldn't get a normal function pointer to work
dbl_pointer = ctypes.POINTER(ctypes.c_void_p)
addr = ctypes.cast(address, dbl_pointer)
addr.contents.value = ctypes.cast(do_drag_data_get._thunk, ctypes.c_void_p).value
class HackedListStore(Gtk.ListStore):
__metaclass__ = HackedMeta
class HackedTreeStore(Gtk.TreeStore):
__metaclass__ = HackedMeta
#
# End hackiness
#
class MyTreeView(Gtk.TreeView):
def __init__(self, args):
Gtk.TreeView.__init__(self)
self.model = MyTreeViewModel(GObject.TYPE_STRING, GObject.TYPE_STRING)
self.set_model(self.model)
self.set_reorderable(True)
self.append_column(Gtk.TreeViewColumn('name', Gtk.CellRendererText(), text=0))
self.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
targets,
Gdk.DragAction.MOVE)
self.enable_model_drag_dest(targets,
Gdk.DragAction.MOVE)
for row in args:
self.model.append(row)
class MyTreeViewModel(HackedListStore):
#
# TreeDragSource
#
if True:
def do_drag_data_get(self, path, selection_data):
print 'MyTreeViewModel.do_drag_data_get', selection_data
uri = self[path][1]
print '- setting uri ', uri
#print 'what text this be ', selection_data.get_text()
print '- retval of set_text:', selection_data.set_text(uri, -1)
#sprint 'just checking ', selection_data.get_text()
return True
if False:
def do_row_draggable(self, path):
print 'do_row_draggable', path
return True
if False:
# This actually performs the delete
def do_drag_data_delete(self, path):
# This never gets called without the hack, presumably because the
# drag never succeeds
print 'do_drag_data_delete', path
return False
#
# TreeDragDest -- these never get called without the hacked metaclass
#
if True:
def do_drag_data_received(self, dest_path, selection_data):
print 'MyTreeViewModel.do_drag_data_received'
print '-', selection_data.get_text()
return True
if True:
def do_row_drop_possible(self, dest_path, selection_data):
print 'is possible?'
return True
if __name__ == '__main__':
args = []
#
# Grab file arguments to put into TreeView
#
import glob
def do(a):
args.append((os.path.basename(a), Gio.File.new_for_commandline_arg(a).get_uri()))
for arg in sys.argv[1:]:
if '*' in arg:
for a in glob.glob(arg):
do(a)
else:
do(arg)
#
# Construct demo window
#
window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL)
sw = Gtk.ScrolledWindow.new()
tv = MyTreeView(args)
sw.add(tv)
window.add(sw)
window.connect('destroy', Gtk.main_quit)
window.set_default_size(400, 300)
window.show_all()
#import sigint
#with sigint.InterruptibleLoopContext(Gtk.main_quit):
Gtk.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment