Last active
August 16, 2016 06:11
-
-
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)
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/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 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
// | |
// 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; | |
} | |
} |
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/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