Closed adokitkat closed 2 years ago
Thanks for reporting.
In the case that you are totally new to GTK and gintro bindings, I would just suggest you that you use some other GUI toolkit, for Nim we have more than 20. Araq mostly recommends fidget.
For gintro, we have unfortunately very few users still after all these years, and no hope that it ever will improve. And we have actually some serious problems, two arch users seems to have problems with libsoup, which is not easily to fix, and we know that we should rewrite the mconnect macro to use only AST manipulation.
For me my priority is currently more the Nim book, I would like to finish it this year.
For your concrete question: For functions returning a glist or gslist we have no fully automatic conversion to a high level function with seq result. For a few functions of this shape we provided manually created functions. For that, we have to know what the low level function does, and how it is called from C. There are a few examples in file gen.nim. When you have some basic knowledge of Nim and GTK, then it is not difficult. Generally you first edit ~/.nimble/pkgs/gintro-0.9.6/gintro/gdk.nim to make the low level function exported, that is by adding the * export marker to the function name. Then you create the high level function manually, I do that generally first direct in my test program. When we have a working function, we add it to gen.nim. A bonus step would be to think about how that high level function could be generated automatically by use of gobject-introspection. You should find examples of such functions returning a seq in file gen.nim. Maybe you have luck and find a perfect fit, that is likely as the parameter list of gdk_drag_context_list_targets is short. Do test your code with --gc:arc, as that shows problems best. And of course try to use GTK4 when possible, not outdated GTK3.
I just asked Google about gdk_drag_context_list_targets(), got no useful result. GTK API doc was recently changed, so Google works not good for GTK currently. So I have no idea what that function may do or how it is used. Of course, a different but ugly approach is, to just export gdk_drag_context_list_targets() and use it as from C low level, but that is really ugly, and maybe not really easier for you.
This week I will try to finish the async/await section in the book, so on sunday I may have time to work on your issue again. Let me know if you really intent to use gintro and need more help.
Thank you for such elaborate answer.
In fact yes I am a newbie with GTK and gintro, however I worked with Qt so it's kind of understandable for me.
I wanted to use gintro because I was trying to replicate this C program (dragon) in Nim which is using GTK+ 3. It is a "drag and drop" source/target meant to be run from terminal to quickly access files via this manner when using e.g. terminal and some other program which supports drag and drop.
This is the code where gdk_drag_context_list_targets()
function is used:
gboolean drag_drop (GtkWidget *widget,
GdkDragContext *context,
gint x,
gint y,
guint time,
gpointer user_data) {
GtkTargetList *targetlist = gtk_drag_dest_get_target_list(widget);
GList *list = gdk_drag_context_list_targets(context);
if (list) {
while (list) {
GdkAtom atom = (GdkAtom)g_list_nth_data(list, 0);
if (gtk_target_list_find(targetlist,
GDK_POINTER_TO_ATOM(g_list_nth_data(list, 0)), NULL)) {
gtk_drag_get_data(widget, context, atom, time);
return true;
}
list = g_list_next(list);
}
}
gtk_drag_finish(context, false, false, time);
return true;
}
I'll try looking into it, thank you very much.
In fact yes I am a newbie with GTK
Well than I can really not recommend GTK for you. Learning GTK is very much work, maybe comparable to learning C++ or Rust? I started in 2007 with the book of A. Krause, and still know only parts of it. A good book would help, but there is none. The Nim GTK4 book is only 20% finished. The other problem are the users, the productive ones that really produce fresh tools. Watching the Gnome forum, the number is tiny. GTK4 has not really improved it. For gintro Nim 2.0 may be one more issue, maybe 2.0 will finally kill gintro. ARC support was already not that easy.
But well I will try to investigate your issue on unday, or at least next week.
As we would need a test for listTargets() and I have currently no other file available I have ported his file from https://github.com/mwh/dragon/blob/master/dragon.c to Nim. Well the largest part. Unfortunately he used 4 spaces for indentation, but not fully consistently, sometime he falls back to only 2. Well at least no tabs. But it would have been better to reformat it I think. I have done all the conversion manually this time. In the past I often used c2nim, which most of the time works well, but sometimes just leave out whole lines, which is again a lot of work to fix. His code uses a lot of functions which have never been tested before, so I assume some more problems unfortunately. And the fact that I have still no idea for what his program is needed, and that I never used drag and drop myself does not really help.
A big issue is static void readstdin(void) -- I have not really an idea what is does and how to convert it to Nim.
Code like
closure = g_cclosure_new(G_CALLBACK(do_quit), NULL, NULL);
accelgroup = gtk_accel_group_new();
gtk_accel_group_connect(accelgroup, GDK_KEY_Escape, 0, 0, closure);
can also be a problem, I am not sure if g_cclosure_new() is supported by gintro, but I think there are substitutes in GTK4. And finally, the fork() and execlp("xdg-open", "xdg-open", dd->uri, NULL); Hard to find in Nim docs at all, seems to be from std/posix. I have still no idea what execlp() exactly is, and Nims version seems to have only two parameters.
Well I will continue in the next days, unfortunately it is not that easy as I initially hoped for. First I was going to provide you with only an implementation for listTargets() with a tiny example, maybe from GTK sources at gitlab. But I have not that much hope that you would be able to port all of his code to Nim yourself -- it is not that easy, and some untested functions which I never heard of before :-(
I got the first draft compiling late yesterday evening:
# https://github.com/mwh/dragon
# // dragon - very lightweight DnD file source/target
# // Copyright 2014 Michael Homer.
import gintro/[gobject, glib, gio, gtk, gdk, gdkpixbuf]
#import std/segfaults # c_strcmp and some more
#from std/segfaults import c_strcmp
import std/posix
from std/os import paramCount, paramStr
from strutils import `%`, startsWith
# #define _POSIX_C_SOURCE 200809L
# #define _XOPEN_SOURCE 500
proc c_strcmp(a, b: string): int = 0 # dummy
proc gtk_main_quit(w: gtk.Window) =
mainQuit()
#echo "Bye..."
const VERSION = "1.1.1"
var
window: gtk.Window
vbox: gtk.Box
iconTheme: gtk.IconTheme
progname: string
verbose = false
mode = 0
thumb_size = 96
and_exit: bool
keep: bool
print_path = false
icons_only = false
always_on_top = false
stdin_files: string
const
MODE_HELP = 1
MODE_TARGET = 2
MODE_VERSION = 4
const
TARGET_TYPE_TEXT = 1
TARGET_TYPE_URI = 2
type
Draggable_thing = ref object
text: string
uri: string
# // MODE_ALL
const MAX_SIZE = 100
var
uri_collection: seq[string]
uri_count = 0
drag_all = false
# // ---
proc add_target_button()
proc do_quit(widget: gtk.Widget) =
quit(0)
proc button_clicked(widget: Button; dd: Draggable_thing) =
if posix.fork() == 0:
discard posix.execlp("xdg-open", "xdg-open", dd.uri, nil)
#[
void
drag_data_get (
GtkWidget* self,
GdkDragContext* context,
GtkSelectionData* data,
guint info,
guint time,
gpointer user_data
)
]#
proc drag_data_get(widget: Button; context: gdk.DragContext; data: gtk.SelectionData; info: int; time: int; dd: Draggable_thing) =
if info == TARGET_TYPE_URI:
var uris: seq[string]
var single_uri_data: array[2, string] = [dd.uri, ""]
if drag_all:
uri_collection[uri_count] = ""
uris = uri_collection
else:
uris = @single_uri_data
if verbose:
if drag_all:
stderr.write("Sending all as URI\n")
else:
stderr.write("Sending as URI: $1\n" % [dd.uri])
discard gtk.set_uris(data, uris)
gobject.signal_stop_emission_by_name(widget, "drag-data-get")
elif info == TARGET_TYPE_TEXT:
if verbose:
write(stderr, "Sending as TEXT: $1\n" % [dd.text])
discard gtk.set_text(data, dd.text, -1)
else:
write(stderr, "Error: bad target type $1\n" % [$info])
proc drag_end(widget: Button; context: gdk.DragContext) =
if verbose:
let succeeded = gdk.drag_drop_succeeded(context)
let action: gdk.DragAction = gdk.get_selected_action(context)
var action_str: string
case action
of gdk.DragAction.copy: # GDK_ACTION_COPY:
action_str = "COPY"
of DragAction.move:#GDK_ACTION_MOVE:
action_str = "MOVE"
of DragAction.link:#GDK_ACTION_LINK:
action_str = "LINK"
of DragAction.ask:#GDK_ACTION_ASK:
action_str = "ASK"
else:
#action_str = malloc(sizeof(char) * 20);
#snprintf(action_str, 20, "invalid (%d)", action);
action_str = "invalid ($1)" % [$action]
stderr.write("Selected drop action: $1; Succeeded: $2\n" % [$action_str, $succeeded])
#if (action_str[0] == 'i')
# free(action_str);
if and_exit:
gtk.main_quit()
proc add_button(label: string; dragdata: Draggable_thing; typee: int): gtk.Button =
var button: gtk.Button
if icons_only:
button = gtk.newButton()
else:
button = gtk.newButton(label)
var targetlist: gtk.TargetList = gtk.drag_source_get_target_list(button)
if targetlist != nil:
discard # gtk_target_list_ref(targetlist);
else:
targetlist = gtk.new_target_list(newSeq[TargetEntry]());
if typee == TARGET_TYPE_URI:
gtk.add_uri_targets(targetlist, TARGET_TYPE_URI)
else:
gtk.add_text_targets(targetlist, TARGET_TYPE_TEXT)
#gtk.drag_source_set(button, {ModifierFlag.button1}, newSeq[TargetEntry](), {gdk.DragAction.copy, link, ask}) # bug in gintro!
gtk.drag_source_set(button, {ModifierFlag.button1}, newSeq[TargetEntry](), cast[gdk.DragAction](gdk.DragAction.copy.ord or gdk.DragAction.link.ord or gdk.DragAction.ask.int))
gtk.drag_source_set_target_list(button, targetlist)
button.connect("drag-data-get", drag_data_get, dragdata)
button.connect("clicked", button_clicked, dragdata)
button.connect("drag-end", drag_end)#, dragdata)
vbox.add(button)
if drag_all:
if uri_count < MAX_SIZE:
uri_collection[uri_count] = dragdata.uri
else:
stderr.write("Exceeded maximum number of files for drag_all ($1)\n" % [$MAX_SIZE])
uri_count += 1
return button;
proc left_align_button(button: Button) =
discard
#[ strabnge, no idea
GList *child = g_list_first(
gtk_container_get_children(GTK_CONTAINER(button)));
if child != nil:
gtk.set_halign(GTK_WIDGET(child->data), GTK_ALIGN_START)
]#
proc icon_info_from_content_type(content_type: string): gtk.IconInfo =
var icon: gio.Icon = gio.content_type_get_icon(content_type)
return gtk.lookup_by_gicon(icon_theme, icon, 48, {})
proc add_file_button(file: gio.GFile) =
let filename: string = gio.get_path(file)
if not gio.query_exists(file, nil):
stderr.write("The file `$1' does not exist.\n" % [filename])
quit(1)
let uri: string = gio.get_uri(file)
let dragdata: Draggable_thing = Draggable_thing() # malloc(sizeof(struct draggable_thing));
dragdata.text = filename
dragdata.uri = uri
let button: gtk.Button = add_button(filename, dragdata, TARGET_TYPE_URI)
let pb: gdkpixbuf.Pixbuf = gdkpixbuf.new_pixbuf_from_file_at_size(filename, thumb_size, thumb_size)
if pb != nil:
let image: Widget = new_image_from_pixbuf(pb);
gtk.set_always_show_image(button, true);
gtk.set_image(button, image);
gtk.set_always_show_image(button, true);
else:
let fileinfo: gio.FileInfo = gio.query_info(file, "*", {})
let icon: gio.Icon = gio.get_icon(fileinfo)
var icon_info: gtk.IconInfo = gtk.lookup_by_gicon(icon_theme, icon, 48, {})
# // Try a few fallback mimetypes if no icon can be found
if icon_info == nil:
icon_info = icon_info_from_content_type("application/octet-stream")
if icon_info == nil:
icon_info = icon_info_from_content_type("text/x-generic")
if icon_info == nil:
icon_info = icon_info_from_content_type("text/plain")
if icon_info != nil:
let image: gtk.Widget = gtk.new_image_from_pixbuf(gtk.load_icon(icon_info))
gtk.set_image(button, image)
gtk.set_always_show_image(button, true)
if not icons_only:
left_align_button(button)
proc add_filename_button(filename: string) =
let file: gio.GFile = new_gfile_for_path(filename)
add_file_button(file)
proc add_uri_button(uri: string) =
let dragdata: Draggable_thing = Draggable_thing() # malloc(sizeof(struct draggable_thing));
dragdata.text = uri
dragdata.uri = uri
let button: gtk.Button = add_button(uri, dragdata, TARGET_TYPE_URI)
left_align_button(button)
proc is_uri(uri: string): bool =
for i in uri.low .. uri.high:
# for (int i=0; uri[i]; i++)
if uri[i] == '/':
return false;
elif uri[i] == ':' and i > 0:
return true
elif (not(uri[i] >= 'a' and uri[i] <= 'z')) or
(uri[i] >= 'A' and uri[i] <= 'Z') or
(uri[i] >= '0' and uri[i] <= '9' and i > 0) or
(i > 0 and (uri[i] == '+' or uri[i] == '.' or uri[i] == '-')): # // RFC3986 URI scheme syntax
return false
return false
proc is_file_uri(uri: string): bool =
uri.startsWith("file:")
#let prefix: string = "file:"
#return strncmp(prefix, uri, strlen(prefix)) == 0
proc drag_drop(widget: Button; context: gdk.DragContext; x, y: int; time: int): bool =
let targetlist: gtk.TargetList = gtk.drag_dest_get_target_list(widget)
#[
let list: GList = gdk.drag_context_list_targets(context)
if list != nil:
while list != nil:
let atom: gdk.Atom = (GdkAtom)g_list_nth_data(list, 0);
if gtk.find(targetlist, GDK_POINTER_TO_ATOM(g_list_nth_data(list, 0)), NULL)):
gtk.drag_get_data(widget, context, atom, time)
return true
list = g_list_next(list)
gtk.drag_finish(context, false, false, time)
]#
return true
proc drag_data_received(widget: gtk.Button, context: gdk.DragContext; x, y: int; data: gtk.SelectionData; info, time: int) =
let uris: seq[string] = gtk.get_uris(data)
let text: string = gtk.get_text(data)
if uris.len == 0 and text.len == 0:
gtk.drag_finish(context, false, false, time.int)
if uris.len > 0:
if verbose:
stderr.write("Received URIs\n")
gtk.remove(vbox, widget)
for uri in uris:
#for (; *uris; uris++) {
if is_file_uri(uri):
let file: GFile = new_gfile_for_uri(uri)
if print_path:
let filename: string = gio.get_path(file)
echo filename
else:
echo uri
if keep:
add_file_button(file)
else:
echo uri
if keep:
add_uri_button(uri)
add_target_button()
gtk.show_all(window)
elif text.len > 0:
if verbose:
stderr.write("Received Text\n")
echo text
elif verbose:
stderr.write("Received nothing\n")
gtk.drag_finish(context, true, false, time.int);
if and_exit:
gtk.main_quit()
proc add_target_button() =
let label: gtk.Button = gtk.new_button()
gtk.set_label(label, "Drag something here...")
gtk.add(vbox, label)
var targetlist: gtk.TargetList = gtk.drag_dest_get_target_list(label)
if targetlist != nil:
discard # gtk_target_list_ref(targetlist);
else:
targetlist = gtk.new_target_list(newSeq[TargetEntry]())
gtk.add_text_targets(targetlist, TARGET_TYPE_TEXT)
gtk.add_uri_targets(targetlist, TARGET_TYPE_URI)
#gtk.drag_dest_set(label, GTK_DEST_DEFAULT_MOTION or GTK_DEST_DEFAULT_HIGHLIGHT, newSeq[TargetEntry](), GDK_ACTION_COPY)
gtk.drag_dest_set(label, cast[DestDefaults](DestDefaults.motion.ord or DestDefaults.highlight.ord), newSeq[TargetEntry](), gdk.DragAction.copy)
gtk.drag_dest_set_target_list(label, targetlist)
connect(label, "drag-drop", drag_drop)
connect(label, "drag-data-received", drag_data_received)
proc target_mode() =
add_target_button()
gtk.show_all(window)
gtk.main()
proc make_btn(filename: string) =
if not is_uri(filename):
add_filename_button(filename)
elif is_file_uri(filename):
let file: gio.GFile = gio.new_gfile_for_uri(filename)
add_file_button(file)
else:
add_uri_button(filename)
proc readstdin() =
discard
#[ no idea currently
char *write_pos = stdin_files, *newline;
size_t max_size = BUFSIZ * 2, cur_size = 0;
// read each line from stdin and add it to the item list
while (fgets(write_pos, BUFSIZ, stdin)) {
if (write_pos[0] == '-')
continue;
if ((newline = strchr(write_pos, '\n')))
*newline = '\0';
else
break;
make_btn(write_pos);
cur_size = newline - stdin_files + 1;
if (max_size < cur_size + BUFSIZ) {
if (!(stdin_files = realloc(stdin_files, (max_size += BUFSIZ))))
fprintf(stderr, "%s: cannot realloc %lu bytes.\n", progname, max_size);
newline = stdin_files + cur_size - 1;
}
write_pos = newline + 1;
}
]#
proc main() =
#let argv = paramStr
let argc = paramCount() + 1
var argv = newSeq[string](argc)
for i in 0 .. paramCount():
argv.add(paramStr(i))
var from_stdin = false;
###stdin_files = malloc(BUFSIZ * 2);
progname = argv[0];
for i in 1 .. argc:
#for (int i=1; i<argc; i++) {
if c_strcmp(argv[i], "--help") == 0:
mode = MODE_HELP
echo("dragon - lightweight DnD source/target")
echo("Usage: $1 [OPTION] [FILENAME]" % [argv[0]])
echo(" --and-exit, -x exit after a single completed drop")
echo(" --target, -t act as a target instead of source")
echo(" --keep, -k with --target, keep files to drag out")
echo(" --print-path, -p with --target, print file paths instead of URIs")
echo(" --all, -a drag all files at once")
echo(" --icon-only, -i only show icons in drag-and-drop windows")
echo(" --on-top, -T make window always-on-top")
echo(" --stdin, -I read input from stdin")
echo(" --thumb-size, -s set thumbnail size (default 96)")
echo(" --verbose, -v be verbose")
echo(" --help show help")
echo(" --version show version details")
quit(0)
elif c_strcmp(argv[i], "--version") == 0:
mode = MODE_VERSION
echo("dragon " & VERSION)
echo("Copyright (C) 2014-2018 Michael Homer")
echo("This program comes with ABSOLUTELY NO WARRANTY.")
echo("See the source for copying conditions.")
quit(0)
elif c_strcmp(argv[i], "-v") == 0 or c_strcmp(argv[i], "--verbose") == 0:
verbose = true
elif c_strcmp(argv[i], "-t") == 0 or c_strcmp(argv[i], "--target") == 0:
mode = MODE_TARGET
elif c_strcmp(argv[i], "-x") == 0 or c_strcmp(argv[i], "--and-exit") == 0:
and_exit = true
elif c_strcmp(argv[i], "-k") == 0 or c_strcmp(argv[i], "--keep") == 0:
keep = true;
elif c_strcmp(argv[i], "-p") == 0 or c_strcmp(argv[i], "--print-path") == 0:
print_path = true
elif c_strcmp(argv[i], "-a") == 0 or c_strcmp(argv[i], "--all") == 0:
drag_all = true
elif c_strcmp(argv[i], "-i") == 0 or c_strcmp(argv[i], "--icon-only") == 0:
icons_only = true
elif c_strcmp(argv[i], "-T") == 0 or c_strcmp(argv[i], "--on-top") == 0:
always_on_top = true
elif c_strcmp(argv[i], "-I") == 0 or c_strcmp(argv[i], "--stdin") == 0:
from_stdin = true
#[
elif c_strcmp(argv[i], "-s") == 0 or c_strcmp(argv[i], "--thumb-size") == 0:
if (argv[i + 1] == NULL or (thumb_size = atoi(argv[i + 1])) <= 0) {
fprintf(stderr, "%s: error: bad argument for %s `%s'.\n",
progname, argv[i], argv[i + 1]);
exit(1);
}
argv[i][0] = '\0';
]#
elif argv[i][0] == '-':
stderr.write("$1: error: unknown option `$2'.\n" % [progname, argv[i]])
###setvbuf(stdout, NULL, _IOLBF, BUFSIZ)
var accelgroup: gtk.AccelGroup
###var closure: glib.GClosure
gtk.init() # (&argc, &argv)
icon_theme = gtk.get_default_icon_theme()
window = gtk.newWindow() # WindowType.toplevel)
#[
closure = g_cclosure_new(G_CALLBACK(do_quit), NULL, NULL);
accelgroup = gtk_accel_group_new();
gtk_accel_group_connect(accelgroup, GDK_KEY_Escape, 0, 0, closure);
closure = g_cclosure_new(G_CALLBACK(do_quit), NULL, NULL);
gtk_accel_group_connect(accelgroup, GDK_KEY_q, 0, 0, closure);
gtk_window_add_accel_group(GTK_WINDOW(window), accelgroup);
]#
gtk.set_title(window, "Run")
gtk.set_resizable(window, false)
gtk.set_keep_above(window, always_on_top)
connect(window, "destroy", gtk_main_quit)
vbox = gtk.newBox(Orientation.vertical, 6)
gtk.add(window, vbox)
gtk.set_title(window, "dragon")
if mode == MODE_TARGET:
target_mode()
quit(0)
if from_stdin:
# uri_collection = malloc(sizeof(char*) * (MAX_SIZE + 1));
uri_collection = newSeq[string](MAX_SIZE + 1) # +1 ?
elif drag_all:
# uri_collection = malloc(sizeof(char*) * ((argc > MAX_SIZE ? argc : MAX_SIZE) + 1));
uri_collection = newSeq[string]((if argc > MAX_SIZE: argc else: MAX_SIZE) + 1)
#for (int i=1; i<argc; i++) {
for i in 1 .. argc:
if argv[i][0] != '-' and argv[i][0] != '\0':
make_btn(argv[i])
if from_stdin:
readstdin()
if uri_count == 0:
echo("Usage: $1 [OPTIONS] FILENAME" % [progname])
quit(0)
gtk.show_all(window)
gtk.main()
quit(0)
main()
Of course there is still some work to do, but the boring part at least is done now. posix.execlp() seems to compile, but I have no idea what setvbuf() is.
What do you finally desire, a GTK4 or GTK3 version? Because we have to fix the g_cclosure_new() stuff for keyboard support, that may be different for GTK3 or GTK4.
I have found the reason why listTargets() is not supported. In gen.nim
if ngrRet.infoType == GIInfoType.STRUCT:
if gBaseInfoGetName(gTypeInfoGetInterface(gTypeInfoGetParamType(ret2, 0))) notin ["Atom", "Variant", "TimedValue"]: # care later
methodBuffer.writeLine(" result = glistStructs2seq[$1](resul0, $2)" % [ngrRet.childName, $(
gCallableInfoGetCallerOwns(minfo) != GITransfer.EVERYTHING)])
# caution TimedValue is a "light" entity, so we need a different glistStructs2seq() for that!
The GList which gdk_drag_context_list_targets() returns contains GdkAtom elements, and that are light entities which are generally allocated on the stack, so we use no Nim proxy elements for them:
type
Atom* {.pure, byRef.} = object
So all that is not straight forward, we have to test it carefully. We may create a variant of glistStructs2seq() for light entities, or maybe we can make GdkAtom a heavy entity? But that may break existing code.
Have you found out what we can do with setvbuf()? And what is with c_strcmp()? From docs it is from segfaults module, but import seems to be possible only as: from system/ansi_c import c_strcmp
Wow I am so surprised you took a such large portion of your free time to work on this! You basically did all the hard work... I am so sorry, I didn't mean to bother you with this. However thank you very much!
Unfortunately I haven't been able to work on this properly yet because I have exams and office work to do also, but after this Friday I'll finally have time.
I've only did a bit of research in GTK docs & on X11 drag&drop mechanism/protocol (it should be called XDND I think).
On setvbuf()
- I don't think we need it at all, it just sets buffering method for printing to stdout
and it's set to _IOLBF
anyway, which is line buffering, i.e. print on new line or full buffer => that's just what Nim's echo
does.
c_strcmp()
again is not needed at all in Nim version as it it only used in argument parsing, which I'll do in a different way, probably via Nim's std/parseopt
.
As for GTK version, I don't really care (or better said: I don't know enough to differentiate).
One key difference with my project and original dragon program will be I don't want it to have 2 different modes of drag & drop, it should be bi-directional (so no -T / --target flag). I think it won't be hard to achieve. Also readstdin()
is not needed (I'll implement it later - or never, I have not used it once). My main problem are those missing functions.
I am going to try out your version. I'll text back later. Thank you again.
My code looks like this (this is what I've got when I stumbled upon listTargets()
and made this GH issue):
import std/[bitops, os, parseopt, parsecfg, strformat, strutils, unicode]
import gintro/[gtk, gdk, glib, gobject, gio] # gdkpixbuf ?
const
VERSION = "0.1.0"
var # Program defaults
app_name = getAppFilename().rsplit("/", maxsplit=1)[1]
cfg_file = "dnd.cfg"
cfg_preset = "Default" # You can modify presets in dnd.cfg file
w = 200 # app width
h = 200 # app height
keep = true
always_on_top = true
center = true
center_screen = false
var
input : seq[string]
type
TargetType {.pure.} = enum
Text, Uri
ArgParseOutput = tuple
keep, always_on_top, center, center_screen: bool
proc arg_parse() : ArgParseOutput =
proc writeHelp() =
echo fmt"{app_name} - drag and drip source / target"
echo fmt"Usage: {app_name} [options] [file...]"
let help = block:
[
"-k, --keep\t\tKeep dropped files in for a drag out",
"-t, --top\t\tKeep the program window always on top",
"-c, --center\t\tOpen the program window in the middle of the parent window",
"-C, --center-screen\tOpen program the window in the middle of the screen",
"-p, --preset=NAME\tLoad different preset from config file"
]
for line in help:
echo " " & line
var
k = keep
t = always_on_top
c = center
C = center_screen
for kind, key, val in commandLineParams().getopt():
case kind
of cmdEnd: break
of cmdArgument: input.add key
of cmdLongOption, cmdShortOption:
case key
of "preset", "p":
if val != "":
cfg_preset = val
of "keep", "k":
k = true
of "top", "t":
t = true
of "center", "c":
c = true
of "center-screen", "C":
C = true
of "help", "h":
writeHelp()
quit 0
of "version", "v":
echo &"{app_name} {VERSION}"
quit 0
result = (k, t, c, C)
proc dragDrop(widget: Widget, context: DragContext, x: int, y: int, time: int) : bool =
var
target_list = dragDestGetTargetList(widget)
#list = gdk_drag_context_list_targets(context.addr)#listTargets(context)
#if list.isNil == false:
# ... missing code
return true
#proc dragDataRecieved() = discard
proc add_dnd_button(box: var Box) =
var dnd_area = newButton("Drag and drop here")
box.packStart(dnd_area, true, true, 0)
var target_list = dnd_area.dragDestGetTargetList()
if target_list.isNil:
target_list = newTargetList(@[])
else:
discard target_list.`ref`
target_list.addTextTargets(TargetType.Text.ord)
target_list.addUriTargets(TargetType.Uri.ord)
dnd_area.dragDestSet(
bitor(DestDefaults.motion.ord, DestDefaults.highlight.ord).DestDefaults,
@[],
gdk.DragAction.copy
)
dnd_area.dragDestSetTargetList(targetlist)
#dnd_area.connect("drag-drop", dragDrop)
#dnd_area.connect("drag-data-recieved", dragDataRecieved)
proc appActivate(app: Application) =
let window = newApplicationWindow(app)
window.title = app_name.cstring
window.defaultSize = (w, h)
window.resizable = true
window.keepAbove = always_on_top
if center_screen:
window.position = WindowPosition.center
var vbox = newBox(Orientation.vertical, 0)
window.add(vbox)
vbox.add_dnd_button()
# ... missing code
showAll(window)
proc main() =
let args = arg_parse()
if cfg_file.fileExists:
var cfg = cfg_file.loadConfig
try: w = cfg.getSectionValue(&"{cfg_preset}", "w").parseInt
except: discard
try: h = cfg.getSectionValue(&"{cfg_preset}", "h").parseInt
except: discard
try: keep = cfg.getSectionValue(&"{cfg_preset}", "keep").toLower.parseBool
except: discard
try: always_on_top = cfg.getSectionValue(&"{cfg_preset}", "always_on_top").toLower.parseBool
except: discard
try: center = cfg.getSectionValue(&"{cfg_preset}", "center").toLower.parseBool
except: discard
try: center_screen = cfg.getSectionValue(&"{cfg_preset}", "center_screen").toLower.parseBool
except: discard
# Flags override the preset
keep = args.keep
always_on_top = args.always_on_top
center = args.center
center_screen = args.center_screen
let app = newApplication("org.gtk.example")
app.connect("activate", appActivate)
discard app.run()
when isMainModule:
main()
I've put it on Github: dnd
EDIT: Also I've merged a part of your code with mine: dnd devel1 branch
Now it creates a window which looks like I want it to. Run it with arguments (names of files) e.g. ./dnd file1.txt file2.jpg
and it displays them correctly, however when you drag and drop into file explorer (I use Nautilus) then my Nautilus crashes. Presumably because drag_drop()
which uses listTargets()
is not implemented yet?
Fine that you have not yet retired -- I had that fear, as you may have discovered that this task can not be done on one single evening. I had a lot of people requesting something, and then they vanish soon and never came back :-(
The GdkAtom is unfortunately hard. It seems to be basically just a string, but one with only one instance, like the Symbols in Ruby, I think most C compilers handle string constants internally in a similar way, when we use the same string constant twice, only one is internally stored.
The strange thing is that gtk4.nim seems not to use Atom at all, so maybe GdkAtom is some legacy?
My current feeling is, that GdkAtom is not a pure stack entity like GtkTextIter. So the current definition is not really good. We could make it a ptr object. Or create Nim proxy object for it, that is our generally split into a GdkAtom00 as low level entity and gdk.Atom as the Nim object with impl field pointing to a GdkAtom00. The later should work, but is is a lot overhead when it is basically just a string.
And please try to do some investigations about setvbuf() and c_strcmp(), see my previous post.
Fine that you have not yet retired -- I had that fear, as you may have discovered that this task can not be done on one single evening. I had a lot of people requesting something, and then they vanish soon and never came back :-(
Even if I cannot do this in one evening, I think I (or we) can manage :) It's not easy, but also not super hard.
My current feeling is, that GdkAtom is not a pure stack entity like GtkTextIter. So the current definition is not really good. We could make it a ptr object. Or create Nim proxy object for it, that is our generally split into a GdkAtom00 as low level entity and gdk.Atom as the Nim object with impl field pointing to a GdkAtom00. The later should work, but is is a lot overhead when it is basically just a string.
I think a little overhead won't matter, this is not a performance based program, human factor is the largest latency anyway. Even a few milliseconds won't matter.
The strange thing is that gtk4.nim seems not to use Atom at all, so maybe GdkAtom is some legacy?
Maybe in GTK 4 we could do it in another way, the original program is written in GTK 3 but since then maybe there is a better way.
And please try to do some investigations about setvbuf() and c_strcmp(), see my previous post.
I already did, please check my first reply of the last three.
Sorry, still no real progress.
The GdkAtom is really special, see https://docs.gtk.org/gdk3/struct.Atom.html. Luckely it is not used for GTK4 at all.
The problem is, that we can not make it a light value object like GtkTextIter as sometimes like in the GList ptr GdkAtom occurs. So I tried to make it a heavy symbol with a Nim proxy. But that does not work, as for GdkAtom there is no free function, so in template genDestroyFreeUnref the logic after "if freeMe != nil" fails and we get no proc newWithFinalizer() so that proc glistStructs2seq() does not work.
Actually the issue with listTargets() was known already for a long time, see bottom of gen.nim:
salewski@nuc ~/gintrotest/tests $ grep -A6 "listTargets*" nim_gi/*
That function seems to be the only one which generates the trouble, and I guess that one is not available for GTK4 at all. The whole drag and drop stuff seems to be very different for GTK4 now, so it was a bad decision from me to try to convert dragon.c to Nim.
I will see how I can fix the GdkAtom issue somehow. As this implies larger changes to gintro, I will tag the last version at github v0.9.7, and then ship the fixes in the next days, which may then get the version 0.9.8.
OK, I have shipped the fixed gen.nim file to github. You can test it with
nimble uninstall gintro
nimble install gintro@#head
The changes are not that large, so it should break not much, but I have still to test all the examples shipped with gintro and the examples from the GTK4 book. With these changes this fixed file of you compiles:
{.warning[CStringConv]: off.}
import std/[bitops, os, parseopt, parsecfg, strformat, strutils, unicode]
import gintro/[gtk, gdk, glib, gobject, gio] # gdkpixbuf ?
const
VERSION = "0.1.0"
var # Program defaults
app_name = getAppFilename().rsplit("/", maxsplit=1)[1]
cfg_file = "dnd.cfg"
cfg_preset = "Default" # You can modify presets in dnd.cfg file
w = 200 # app width
h = 200 # app height
keep = true
always_on_top = true
center = true
center_screen = false
var
input : seq[string]
type
TargetType {.pure.} = enum
Text, Uri
ArgParseOutput = tuple
keep, always_on_top, center, center_screen: bool
proc arg_parse() : ArgParseOutput =
proc writeHelp() =
echo fmt"{app_name} - drag and drip source / target"
echo fmt"Usage: {app_name} [options] [file...]"
let help = block:
[
"-k, --keep\t\tKeep dropped files in for a drag out",
"-t, --top\t\tKeep the program window always on top",
"-c, --center\t\tOpen the program window in the middle of the parent window",
"-C, --center-screen\tOpen program the window in the middle of the screen",
"-p, --preset=NAME\tLoad different preset from config file"
]
for line in help:
echo " " & line
var
k = keep
t = always_on_top
c = center
C = center_screen
for kind, key, val in commandLineParams().getopt():
case kind
of cmdEnd: break
of cmdArgument: input.add key
of cmdLongOption, cmdShortOption:
case key
of "preset", "p":
if val != "":
cfg_preset = val
of "keep", "k":
k = true
of "top", "t":
t = true
of "center", "c":
c = true
of "center-screen", "C":
C = true
of "help", "h":
writeHelp()
quit 0
of "version", "v":
echo &"{app_name} {VERSION}"
quit 0
result = (k, t, c, C)
proc dragDrop(widget: Button; context: DragContext; x, y, time: int) : bool =
var
target_list = dragDestGetTargetList(widget)
#list = gdk_drag_context_list_targets(context.addr)#listTargets(context)
var list: seq[gdk.Atom] = gdk.listTargets(context)
echo list.len
for n in list:
echo n.name
#if list.isNil == false:
# ... missing code
return true # Whether the cursor position is in a drop zone.
#proc dragDataRecieved() = discard
proc add_dnd_button(box: var Box) =
var dnd_area = newButton("Drag and drop here")
box.packStart(dnd_area, true, true, 0)
var target_list = dnd_area.dragDestGetTargetList()
if target_list.isNil:
target_list = newTargetList(@[])
else:
discard # target_list.`ref`
target_list.addTextTargets(TargetType.Text.ord)
target_list.addUriTargets(TargetType.Uri.ord)
dnd_area.dragDestSet(
{DestFlag.motion, highlight},
#bitor(DestDefaults.motion.ord, DestDefaults.highlight.ord).DestDefaults,
@[],
{gdk.DragFlag.copy}
)
dnd_area.dragDestSetTargetList(targetlist)
dnd_area.connect("drag-drop", dragDrop)
#dnd_area.connect("drag-data-recieved", dragDataRecieved)
proc appActivate(app: Application) =
let window = newApplicationWindow(app)
# we may use with
window.title = app_name # .cstring # .cstring is ugly, we can use {.warning[CStringConv]: off.}. There was discussion to use Nim strings instead.
window.defaultSize = (w, h)
window.resizable = true
window.keepAbove = always_on_top
if center_screen:
window.position = WindowPosition.center
var vbox = newBox(Orientation.vertical)#, 0)
window.add(vbox)
vbox.add_dnd_button()
# ... missing code
showAll(window)
proc main =
let args = arg_parse()
if cfg_file.fileExists:
var cfg = cfg_file.loadConfig
try: w = cfg.getSectionValue(&"{cfg_preset}", "w").parseInt
except: discard
try: h = cfg.getSectionValue(&"{cfg_preset}", "h").parseInt
except: discard
try: keep = cfg.getSectionValue(&"{cfg_preset}", "keep").toLower.parseBool
except: discard
try: always_on_top = cfg.getSectionValue(&"{cfg_preset}", "always_on_top").toLower.parseBool
except: discard
try: center = cfg.getSectionValue(&"{cfg_preset}", "center").toLower.parseBool
except: discard
try: center_screen = cfg.getSectionValue(&"{cfg_preset}", "center_screen").toLower.parseBool
except: discard
# Flags override the preset
keep = args.keep
always_on_top = args.always_on_top
center = args.center
center_screen = args.center_screen
let app = newApplication("org.gtk.example")
app.connect("activate", appActivate)
discard app.run()
when isMainModule:
main()
I was not able to really test it, as I have still now idea about what it shall do or about drag and drop at all. But I tried to drag a file displayed in the Gnome file manager onto your window, and got
salewski@nuc ~/kitkat $ ./ado
8
x-special/gnome-icon-list
text/uri-list
UTF8_STRING
COMPOUND_TEXT
TEXT
STRING
text/plain;charset=utf-8
text/plain
I have no idea if that makes any sense, but at least it is some output, and there is no crash, so you can start debugging yourself. Works with --gc:arc too.
Let me know if there are more problems.
Thank you very much! GtkAtom part now works.
I have found a little bug in gintro through - it's just missing nil
checks in some places.
Traceback:
Traceback (most recent call last)
/home/ado/git/dnd/src/dnd.nim(359) dnd
/home/ado/git/dnd/src/dnd.nim(356) main
/home/ado/.nimble/pkgs/gintro-#head/gintro/gio.nim(31146) run
/home/ado/.choosenim/toolchains/nim-1.6.2/lib/core/macros.nim(554) connect_for_signal_cdecl_drag_data_received5
/home/ado/git/dnd/src/dnd.nim(193) dragDataRecieved
/home/ado/.nimble/pkgs/gintro-#head/gintro/gtk.nim(241) getUris
/home/ado/.nimble/pkgs/gintro-#head/gintro/glib.nim(97) cstringArrayToSeq
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
Segmentation fault (core dumped)
I had to change this code in gtk.nim
:
proc getUris*(self: SelectionData): seq[string] =
let resul0 = gtk_selection_data_get_uris(cast[ptr SelectionData00](self.impl))
result = cstringArrayToSeq(resul0)
g_strfreev(resul0)
To:
proc getUris*(self: SelectionData): seq[string] =
let resul0 = gtk_selection_data_get_uris(cast[ptr SelectionData00](self.impl))
if not resul0.isNil():
result = cstringArrayToSeq(resul0)
g_strfreev(resul0)
There are more places where this could be needed. I found this bug when testing the functionality of drag and drop into my program - files from file explorer work, but something like grabbing picture from web browser and dropping it into my program caused SIGSEGV. After the change drag and drop into my program from various sources works.
Dragging and dropping from my program into file explorer doesn't work just now for some reason (and into browser only sometimes) so there is also an another bug I have to find.
gtk_selection_data_get_uris
Thanks for reporting, seems you are right:
https://docs.gtk.org/gtk3/method.SelectionData.get_uris.html
If the result is non-NULL it must be freed with g_strfreev().
Will fix it soon.
It was easy to find the location for the issue in the gen.nim file, but I had to ask Mr. Bassi again, as I do not really understand why my test fails:
https://discourse.gnome.org/t/gtk-selection-data-get-uris-null-result/8867
I was trying to find out the last bug but no luck so far.
The problem is occuring when dragging out of the program into e.g. file explorer (mine crashes) or to web browser (in Chrome nothing happens).
I compared the code with the original dragon
C code but I cannot find any difference.
DragAction should be copy, same as in dragon
, yet it works while mine doesn't. gtk.setUris(uris)
returns true in both programs too.
I think the emitted X11 drag and drop signal is corrupted somehow. However I don't know how to inspect it.
You can see my code here
OK, I have added the nil check.
You can test it with
nimble uninstall gintro nimble install gintro@#head
From the discussion with Mr. Droege and my own investigations I got the impression that gobject-introspection does not support the gCallableInfoMayReturnNull(minfo) reliable. So we have to do the nil check for all gchar** results, that is pointer to array of cstrings. Of course that increases the total code size a bit. It is basically a gobject-introspectionn bug, but there are no serious chances that it will get fixed. I still have to look for other locations if more fixes may be necessary. This fix generates already a few dozen more tests.
For your other issue, I will look into later.
diff ~/gintrotest/tests/gen.nim gen.nim
2334,2335c2334
< # if gCallableInfoMayReturnNull(minfo): # see https://discourse.gnome.org/t/gtk-selection-data-get-uris-null-result/8867
< if gCallableInfoMayReturnNull(minfo) or ngrRet.flags.contains(RecResFlag.array):
---
> if gCallableInfoMayReturnNull(minfo):
I compared the code with the original dragon C code but I cannot find any difference.
OK, then I will continue my dragon.c port to Nim. It is mostly finished already.
Well, maybe it is that easy:
proc dragEnd(widget: Button; context: gdk.DragContext) =
if verbose:
let
succeeded = context.dragDropSucceeded()
action: DragAction = context.getSelectedAction()
var action_str: string
case cast[DragFlag](action)
of DragFlag.copy:
action_str = "COPY"
of DragFlag.move:
action_str = "MOVE"
of DragFlag.link:
action_str = "LINK"
of DragFlag.ask:
action_str = "ASK"
else:
action_str = "invalid action"
We generally avoid casts. The flags are sets in Nim, and ored ints in C. Instead of cast, and using case, you may use if and test for bits. See my first draft above. Just a guess -- I tried to drag into firefox window and got message "invalid action"
Have you tried to drag and drop from dnd
into something like this https://www.dragdrop.com/test? I think the dragEnd()
is not related to this, it just says what operation it has done. The cast should be OK? I suspect the problem is in dragDataGet()
. But I may be wrong.
I am getting action MOVE, although it should be COPY.
The cast is not ok.
With my fix
var action_str: string
#let
#case cast[DragFlag](action)
if DragFlag.copy in action:
action_str = "COPY"
elif DragFlag.move in action:
action_str = "MOVE"
elif DragFlag.link in action:
action_str = "LINK"
elif DragFlag.ask in action:
action_str = "ASK"
else:
action_str = "invalid action"
I get a valid action when dragging into firefox. But no action. When dragging into gedit I get an corrupted file name.
Problem is, I know nothing about drag and drop at all, so it is hard for me to debug.
I suspect the problem is in dragDataGet()
Yes, I had the same feeling. Corrupted file name for gedit.
I got it! data.setUris(uris)
doesn't unpack seq and the seq gets literally mangled into filename like "/home/abc/@[/home/abc/filename.txt]"
. When only passing string into it like discard data.setUris(dd.uri.cstring)
it works!
Great! My last test gave
drag data get
info == TargetType.Uri.ord
Sending as URI: file:///home/salewski/hhh27.txt
Selected drop action: LINK; Succeeded: true
drag data get
info == TargetType.Uri.ord
Sending as URI: file:///home/salewski/hhh27.txt
Selected drop action: COPY; Succeeded: true
for dragging into firefox and gedit, with no result for firefox and Could not find the file “/home/salewski/@["file:/…salewski/hhh27.txt", nil]”. for gedit.
I think you have managed it yourself, so I will do a break now :-)
I guess the last bug in gintro related to this issue is this weird non-unpacking of a seq when it's passed into varargs?
In gtk.nim
:
proc setUris*(self: SelectionData; uris: varargs[string, `$`]): bool =
var fs469n23x: array[256, pointer]
var fs469n23: cstringArray = cast[cstringArray](addr fs469n23x)
toBool(gtk_selection_data_set_uris(cast[ptr SelectionData00](self.impl), seq2CstringArray(uris, fs469n23)))
When a seq is passed there the weird mangling happens e.g. like: /home/abc/@["/home/abc/filename.txt", "/home/abc/filename.txt", nil]"
.
And thank you for your help, it's much appreciated. I wouldn't have done it without you, thanks :)
Yes, looks like one more gintro bug. Will try to fix it tomorrow.
My current feeling is that it is no gintro bug. With
echo uris
var xxx: seq[string] = @["file:///home/salewski/hhh27.txt", "XXX"]
discard data.setUris(xxx)
it seems to work.
My seq2CstringArray() may look strange, as it returns a ptr cstring, and not a cstringarray. But that should be not a problem. What you should not do is to use cstrings, and to add nil to the seq. Gintro is a high level binding. Unfortunately I have still to understand a bit what is going on at all, I have no idea, and no motivation to really learn it, as all that seems to be legacy stuff, for GTK4 all is very different. I have to learn what the uri_collection means, I think I have to study dragon.c a bit more.
This seems to work better. But I think I have to convert dragon.c to Nim tomorrow, as I have no real idea how the program should behave in detail, no idea how dragging multiple files may work, so I can not test that.
Try to remove all cstrings. Araq may try to generate trouble for us, but as I wrote earlier we can disable the warnings. And caution with your discard target_list.ref
, the ref is executed still, I told you in my first code example.
import std/[os, parseopt, parsecfg, posix, strformat, strutils, unicode, uri]
import gintro/[gtk, gdk, gdkpixbuf, glib, gobject, gio]
const
NimblePkgVersion {.strdefine.} = "Unknown"
Version = NimblePkgVersion
MAX_SIZE = 1024
var # Program default settings
app_name = getAppFilename().rsplit("/", maxsplit=1)[1]
cfg_file = "dnd.cfg"
cfg_preset = "Default" # You can modify presets in dnd.cfg file
w = 200 # app width
h = 200 # app height
keep = false
always_on_top = true
center = false
center_screen = true
verbose = true
print_path = true
var # Variables
window: ApplicationWindow
vbox: Box
input: seq[string]
uri_collection: seq[string]
#uri_count = 0
drag_all = false
#iconTheme: gtk.IconTheme
#thumb_size = 96
type # Custom types
TargetType {.pure.} = enum
Text = 1
Uri = 2
DraggableThing = ref object
text: string
uri: string
ArgParseOutput = tuple
keep, always_on_top, center, center_screen: bool
proc addDndButton(box: var Box)
proc isFileUri(uri: string): bool = uri.parseUri().scheme == "file"
proc buttonClicked(widget: gtk.Button; dd: DraggableThing) =
when defined(posix):
if posix.fork() == 0:
discard posix.execlp("xdg-open", "xdg-open", dd.uri.cstring, nil)
else:
{.warning: "Click to open won't work in not POSIX enviroment".}
proc dragDataGet(widget: gtk.Button, context: gdk.DragContext,
data: gtk.SelectionData, info: int, time: int, dd: DraggableThing) =
echo "drag data get"
if info == TargetType.Uri.ord:
echo "info == TargetType.Uri.ord"
var
uris: seq[string]
#single_uri_data: array[2, cstring] = [dd.uri.cstring, nil]
if drag_all:
#uri_collection[uri_count] = nil
uris = uri_collection
else:
uris.add(dd.uri)
#uris.add nil
if verbose:
if drag_all:
stderr.write &"Sending all as URI\n"
else:
stderr.write &"Sending as URI: {dd.uri}\n"
#echo uris
#var xxx: seq[string] = @["file:///home/salewski/hhh27.txt", "XXX"]
discard data.setUris(uris)
widget.signalStopEmissionByName("drag-data-get")
elif info == TargetType.Text.ord:
if verbose:
stderr.write &"Sending as TEXT: {dd.text}\n"
discard data.setText(dd.text.cstring, -1)
else:
stderr.write &"Error: bad target type {info}\n"
proc dragEnd(widget: Button; context: gdk.DragContext) =
if verbose:
let
succeeded = context.dragDropSucceeded()
action: DragAction = context.getSelectedAction()
var action_str: string
#let
#case cast[DragFlag](action)
if DragFlag.copy in action:
action_str = "COPY"
elif DragFlag.move in action:
action_str = "MOVE"
elif DragFlag.link in action:
action_str = "LINK"
elif DragFlag.ask in action:
action_str = "ASK"
else:
action_str = "invalid action"
echo &"Selected drop action: {action_str}; Succeeded: {succeeded}"
#if and_exit:
# gtk.main_quit()
proc addButton(box: var gtk.Box, label: string; dragdata: DraggableThing; typee: int): gtk.Button =
var button: gtk.Button = newButton(label)
#if icons_only:
# button = gtk.newButton()
#else:
var target_list: gtk.TargetList = button.dragSourceGetTargetList()
if target_list != nil:
discard target_list.`ref`
else:
target_list = gtk.new_target_list(newSeq[TargetEntry]())
if typee == TargetType.Uri.ord:
target_list.addUriTargets(TargetType.Uri.ord)
else:
target_list.addTextTargets(TargetType.Text.ord)
button.dragSourceSet({ModifierFlag.button1}, newSeq[TargetEntry](),
{DragFlag.copy, DragFlag.link, DragFlag.ask})
button.dragSourceSetTargetList(target_list)
button.connect("drag-data-get", dragDataGet, dragdata)
button.connect("clicked", buttonClicked, dragdata)
button.connect("drag-end", dragEnd)
box.add(button)
if drag_all:
#if uri_count < MAX_SIZE:
uri_collection.add(dragdata.uri)
#else:
# stderr.write &"Exceeded maximum number of files for drag_all ({MAX_SIZE})\n"
#uri_count.inc
return button
proc addFileButton(box: var gtk.Box, file: gio.GFile): gtk.Button =
let
filename: string = file.get_path()
uri: string = file.get_uri()
dragdata: DraggableThing = DraggableThing()
if not file.query_exists(nil):
stderr.write &"The file {filename} does not exist.\n"
else:
dragdata.text = filename
dragdata.uri = uri
let button = box.addButton(filename, dragdata, TargetType.Uri.ord)
result = button
proc addUriButton(box: var gtk.Box, uri: string): gtk.Button =
let dragdata: DraggableThing = DraggableThing()
dragdata.text = uri
dragdata.uri = uri
let button = box.addButton(uri, dragdata, TargetType.Uri.ord)
result = button
# proc addTextButton()
proc makeButton(box: var gtk.Box, filename: string): gtk.Button =
# decodeUrl ?
let uri = filename.parseUri()
var file: gio.GFile
if uri.scheme == "":
file = new_gfile_for_path(filename)
result = box.add_file_button(file)
elif uri.scheme == "file":
file = gio.new_gfile_for_uri(filename)
result = box.add_file_button(file)
else:
result = box.add_uri_button(filename)
proc dragDataRecieved(widget: gtk.Button, context: gdk.DragContext; x, y: int; data: gtk.SelectionData; info, time: int) =
let
uris: seq[string] = data.getUris()
text: string = data.getText()
var file: GFile
if uris.len == 0 and text.len == 0:
context.drag_finish(false, false, time)
if uris.len > 0:
if verbose:
stderr.write "Received URIs\n"
for uri in uris:
if uri.isFileUri():
file = gio.new_gfile_for_uri(uri.cstring)
if print_path:
let filename: string = gio.get_path(file)
echo filename
else:
echo uri
if keep:
vbox.remove(widget)
discard vbox.add_file_button(file)
vbox.add_dnd_button()
window.show_all()
else:
echo uri
#if keep:
vbox.remove(widget)
discard vbox.addUriButton(uri)
vbox.add_dnd_button()
window.show_all()
if text.len > 0:
if verbose:
stderr.write "Received Text\n"
if keep:
var text_uri = text.parseUri()
if $text_uri.scheme != "":
vbox.remove(widget)
discard vbox.addUriButton($text_uri)
vbox.add_dnd_button()
window.show_all()
echo text
if verbose and uris.len == 0 and text.len == 0:
stderr.write "Received nothing\n"
# TODO: keep files from args
context.drag_finish(true, false, time)
#if and_exit:
# gtk.main_quit()
proc dragDrop(widget: gtk.Button; context: DragContext; x, y, time: int) : bool =
let
target_list = dragDestGetTargetList(widget)
list: seq[gdk.Atom] = gdk.listTargets(context)
var success: bool = false
for atom in list:
if target_list.findTargetList(atom):
widget.dragGetData(context, atom, time)
success = true
if not success:
context.dragFinish(false, false, time)
result = true
proc addDndButton(box: var gtk.Box) =
var
dnd_area = newButton("Drag and drop here")
target_list = dnd_area.dragDestGetTargetList()
box.packStart(dnd_area, true, true, 0)
if target_list.isNil:
target_list = newTargetList(newSeq[TargetEntry]())
else:
target_list = target_list.`ref`
target_list.addTextTargets(TargetType.Text.ord)
target_list.addUriTargets(TargetType.Uri.ord)
dnd_area.dragDestSet(
{DestFlag.motion, DestFlag.highlight},
@[],
{gdk.DragFlag.copy}
)
dnd_area.dragDestSetTargetList(targetlist)
dnd_area.connect("drag-drop", dragDrop)
dnd_area.connect("drag-data-received", dragDataRecieved)
proc appActivate(app: Application) =
window = newApplicationWindow(app)
window.title = app_name.cstring
window.defaultSize = (w, h)
window.resizable = true
window.keepAbove = always_on_top
if center_screen:
window.position = WindowPosition.center
# Create window content
vbox = newBox(Orientation.vertical, 0)
window.add(vbox)
# Populate if input files
for arg in input:
discard vbox.makeButton(arg)
vbox.add_dnd_button() # Add drag and drop area
window.showAll()
proc parseCfg() =
if cfg_file.fileExists:
var cfg = cfg_file.loadConfig()
try: w = cfg.getSectionValue(&"{cfg_preset}", "w").parseInt
except: discard
try: h = cfg.getSectionValue(&"{cfg_preset}", "h").parseInt
except: discard
try: keep = cfg.getSectionValue(&"{cfg_preset}", "keep").toLower.parseBool
except: discard
try: always_on_top = cfg.getSectionValue(&"{cfg_preset}", "always_on_top").toLower.parseBool
except: discard
try: center = cfg.getSectionValue(&"{cfg_preset}", "center").toLower.parseBool
except: discard
try: center_screen = cfg.getSectionValue(&"{cfg_preset}", "center_screen").toLower.parseBool
except: discard
proc argParse() : ArgParseOutput =
proc writeHelp() =
echo &"{app_name} - bi-directional drag and drip source / target"
echo &"Usage: {app_name} [options] [file...]"
let help = block:
[
"-k, --keep\t\tKeep dropped files in for a drag out",
"-t, --top\t\tKeep the program window always on top",
"-c, --center\t\tOpen the program window in the middle of the parent window",
"-C, --center-screen\tOpen program the window in the middle of the screen",
"-p, --preset=NAME\tLoad different preset from config file"
]
for line in help:
echo " " & line
var
k = keep
t = always_on_top
c = center
C = center_screen
for kind, key, val in commandLineParams().getopt():
case kind
of cmdEnd: break
of cmdArgument: input.add key
of cmdLongOption, cmdShortOption:
case key
of "preset", "p":
if val != "":
cfg_preset = val
of "keep", "k":
k = true
of "top", "t":
t = true
of "center", "c":
c = true
of "center-screen", "C":
C = true
of "help", "h":
writeHelp()
quit 0
of "version", "v":
echo &"{app_name} {Version}"
quit 0
result = (k, t, c, C)
proc main() =
let args = argParse() # Parse arguments (flags)
parseCfg() # Parse config file
# Flags override the preset
keep = args.keep
always_on_top = args.always_on_top
center = args.center
center_screen = args.center_screen
# Run the GUI
let app = newApplication("org.gtk.example")
app.connect("activate", appActivate)
discard app.run()
when isMainModule:
main()
I got the Nim version of dragon.c mostly working late yesterday night, but still have no idea for static void readstdin(void). The dragon.c with --stdin seems to be possible to read multiple lines, lines separated by RETURN, and total input terminated by CTRL-D. No idea for Nim yet. There is a rdstdin module, but that terminates with CTRL-D. And I think linenoise is Linux only? Unfortunately there is no fgets in Nim.
I think I found a solution with CTRL-D:
https://nim-by-example.github.io/
for l in stdin.lines: # CTRL-D to terminate
echo l
echo "done"
Now we have only to support keyboard shortcuts. I think I will make the program a GTK app, then it should be easy. For the original g_cclosure_new() we may need a macro.
OK, here is the final dragon.nim. Should behave identical to dragon.c, you may test it yourself.
# https://github.com/mwh/dragon
# dragon - very lightweight DnD file source/target
# Copyright 2014 Michael Homer.
# Nim version by S. Salewski 2022
import gintro/[gobject, glib, gio, gtk, gdk, gdkpixbuf]
import std/posix
from std/os import paramCount, paramStr
from strutils import `%`, startsWith, dedent
from parseUtils import parseInt
const Version = "1.1.1"
type
Mode {.pure.} = enum
invalid, help, target, version
const # ID for gtkTargetListAddTextTargets
TargetTypeText = 1
TargetTypeUri = 2
var
window: ApplicationWindow
vbox: gtk.Box
iconTheme: gtk.IconTheme
progname: string
verbose = false
mode = Mode.invalid
thumbSize = 96
andExit: bool
keep: bool
printPath = false
iconsOnly = false
alwaysOnTop = false
uriCount = 0
type
DraggableThing = ref object
text: string
uri: string
var
uriCollection: seq[string]
dragAll = false
proc addTargetButton()
proc buttonClicked(widget: Button; dd: DraggableThing) =
if posix.fork() == 0:
discard posix.execlp("xdg-open", "xdg-open", dd.uri, nil) # do we need the nil?
proc dragDataGet(widget: Button; context: gdk.DragContext; data: gtk.SelectionData; info, time: int; dd: DraggableThing) =
if info == TargetTypeUri:
var uris: seq[string]
if dragAll:
uris = uriCollection
else:
uris.add(dd.uri)
if verbose:
if dragAll:
stderr.write("Sending all as URI\n")
else:
stderr.write("Sending as URI: $1\n" % [dd.uri])
discard gtk.setUris(data, uris)
gobject.signalStopEmissionByName(widget, "drag-data-get")
elif info == TargetTypeText:
if verbose:
write(stderr, "Sending as TEXT: $1\n" % [dd.text])
discard gtk.setText(data, dd.text, -1)
else:
write(stderr, "Error: bad target type $1\n" % [$info])
proc dragEnd(widget: Button; context: gdk.DragContext) =
if verbose:
let succeeded = gdk.dragDropSucceeded(context)
let action: gdk.DragAction = gdk.getSelectedAction(context)
var actionStr: string
if gdk.DragFlag.copy in action:
actionStr = "COPY"
elif DragFlag.move in action:
actionStr = "MOVE"
elif DragFlag.link in action:
actionStr = "LINK"
elif DragFlag.ask in action:
actionStr = "ASK"
else:
actionStr = "invalid ($1)" % [$action]
stderr.write("Selected drop action: $1; Succeeded: $2\n" % [$actionStr, $succeeded])
if andExit:
quit() # gtk.mainQuit()
proc addButton(label: string; dragdata: DraggableThing; ttype: int): gtk.Button =
var button: gtk.Button
if iconsOnly:
button = gtk.newButton()
else:
button = gtk.newButton(label)
var targetlist: gtk.TargetList = gtk.dragSourceGetTargetList(button)
if targetlist == nil:
targetlist = gtk.newTargetList(newSeq[TargetEntry]())
if ttype == TargetTypeUri:
gtk.addUriTargets(targetlist, TargetTypeUri)
else:
gtk.addTextTargets(targetlist, TargetTypeText)
gtk.dragSourceSet(button, {ModifierFlag.button1}, @[], {gdk.DragFlag.copy, gdk.DragFlag.link, gdk.DragFlag.ask})
gtk.dragSourceSetTargetList(button, targetlist)
button.connect("drag-data-get", dragDataGet, dragdata)
button.connect("clicked", buttonClicked, dragdata)
button.connect("drag-end", dragEnd)
vbox.add(button)
if dragAll:
uriCollection.add(dragdata.uri)
inc(uriCount) # for the final fail test only!
return button
proc leftAlignButton(button: Button) =
#[
for ch in button.getChildren: # well maybe only for first child, see dragon.c
gtk.setHalign(ch, Align.start)
]#
let ch = button.getChildren # should better match dragon.c
if ch.len > 0:
gtk.setHalign(ch[0], Align.start)
proc iconInfoFromContentType(contentType: string): gtk.IconInfo =
var icon: gio.Icon = gio.contentTypeGetIcon(contentType)
return gtk.lookupByGicon(iconTheme, icon, 48, {})
proc addFileButton(file: gio.GFile) =
let filename: string = gio.getPath(file)
if not gio.queryExists(file, nil):
stderr.write("The file `$1' does not exist.\n" % [filename])
quit(1)
let uri: string = gio.getUri(file)
let dragdata = DraggableThing()
dragdata.text = filename
dragdata.uri = uri
let button: gtk.Button = addButton(filename, dragdata, TargetTypeUri)
var pb: gdkpixbuf.Pixbuf
try:
pb = gdkpixbuf.newPixbufFromFileAtSize(filename, thumbSize, thumbSize)
except:
discard
if pb != nil:
let image: Widget = newImageFromPixbuf(pb)
gtk.setAlwaysShowImage(button)
gtk.setImage(button, image)
gtk.setAlwaysShowImage(button)
else:
let fileinfo: gio.FileInfo = gio.queryInfo(file, "*", {})
let icon: gio.Icon = gio.getIcon(fileinfo)
var iconInfo: gtk.IconInfo = gtk.lookupByGicon(iconTheme, icon, 48, {})
# // Try a few fallback mimetypes if no icon can be found
if iconInfo == nil:
iconInfo = iconInfoFromContentType("application/octet-stream")
if iconInfo == nil:
iconInfo = iconInfoFromContentType("text/x-generic")
if iconInfo == nil:
iconInfo = iconInfoFromContentType("text/plain")
if iconInfo != nil:
let image: gtk.Widget = gtk.newImageFromPixbuf(gtk.loadIcon(iconInfo))
gtk.setImage(button, image)
gtk.setAlwaysShowImage(button)
if not iconsOnly:
leftAlignButton(button)
proc addFilenameButton(filename: string) =
let file: gio.GFile = newGfileForPath(filename)
addFileButton(file)
proc addUriButton(uri: string) =
let dragdata = DraggableThing()
dragdata.text = uri
dragdata.uri = uri
let button: gtk.Button = addButton(uri, dragdata, TargetTypeUri)
leftAlignButton(button)
proc isUri(uri: string): bool =
for i in uri.low .. uri.high:
if uri[i] == '/':
return false
elif uri[i] == ':' and i > 0:
return true
elif not ((uri[i] >= 'a' and uri[i] <= 'z') or (uri[i] >= 'A' and uri[i] <= 'Z') or
(uri[i] >= '0' and uri[i] <= '9' and i > 0) or (i > 0 and (uri[i] == '+' or uri[i] == '.' or uri[i] == '-'))): # // RFC3986 URI scheme syntax
return false
return false
proc isFileUri(uri: string): bool =
uri.startsWith("file:")
proc dragDrop(widget: Button; context: gdk.DragContext; x, y: int; time: int): bool =
let targetlist: gtk.TargetList = gtk.dragDestGetTargetList(widget)
let list = gdk.listTargets(context)
for atom in list:
if gtk.findTargetList(targetlist, atom):
gtk.dragGetData(widget, context, atom, time)
return true # no finish here?
gtk.dragFinish(context, false, false, time)
return true # Whether the cursor position is in a drop zone.
proc dragDataReceived(widget: gtk.Button, context: gdk.DragContext; x, y: int; data: gtk.SelectionData; info, time: int) =
let uris: seq[string] = gtk.getUris(data)
let text: string = gtk.getText(data)
if uris.len == 0 and text.len == 0:
gtk.dragFinish(context, false, false, time.int)
if uris.len > 0:
if verbose:
stderr.write("Received URIs\n")
gtk.remove(vbox, widget)
for uri in uris:
if isFileUri(uri):
let file: GFile = newGfileForUri(uri)
if printPath:
let filename: string = gio.getPath(file)
echo filename
else:
echo uri
if keep:
addFileButton(file)
else:
echo uri
if keep:
addUriButton(uri)
addTargetButton()
gtk.showAll(window)
elif text.len > 0:
if verbose:
stderr.write("Received Text\n")
echo text
elif verbose:
stderr.write("Received nothing\n")
gtk.dragFinish(context, true, false, time.int)
if andExit:
quit() # gtk.mainQuit()
proc addTargetButton() =
let button: gtk.Button = gtk.newButton()
gtk.setLabel(button, "Drag something here...")
gtk.add(vbox, button)
var targetlist: gtk.TargetList = gtk.dragDestGetTargetList(button)
if targetlist == nil:
targetlist = gtk.newTargetList(newSeq[TargetEntry]())
gtk.addTextTargets(targetlist, TargetTypeText)
gtk.addUriTargets(targetlist, TargetTypeUri)
gtk.dragDestSet(button, {DestFlag.motion, DestFlag.highlight}, @[], {gdk.DragFlag.copy})
gtk.dragDestSetTargetList(button, targetlist)
connect(button, "drag-drop", dragDrop)
connect(button, "drag-data-received", dragDataReceived)
proc makeBtn(filename: string) =
if not isUri(filename):
addFilenameButton(filename)
elif isFileUri(filename):
let file: gio.GFile = gio.newGfileForUri(filename)
addFileButton(file)
else:
addUriButton(filename)
proc readstdin =
for l in lines(io.stdin): # CTRL-D to terminate
makeBtn(l)
proc quitCb(action: SimpleAction; v: Variant) =
quit()
proc appActivate(app: Application) =
let argc = paramCount() + 1
var argv = newSeqOfCap[string](argc)
for i in 0 .. paramCount():
argv.add(paramStr(i))
var fromStdin = false
progname = argv[0]
for i in 1 ..< argc:
case argv[i]
of "--help":
mode = Mode.help
echo """
dragon - lightweight DnD source/target
Usage: $1 [OPTION] [FILENAME]
--and-exit, -x exit after a single completed drop
--target, -t act as a target instead of source
--keep, -k with --target, keep files to drag out
--print-path, -p with --target, print file paths instead of URIs
--all, -a drag all files at once
--icon-only, -i only show icons in drag-and-drop windows
--on-top, -T make window always-on-top
--stdin, -I read input from stdin
--thumb-size, -s set thumbnail size (default 96)
--verbose, -v be verbose
--help show help
--Version show Version details
""".dedent % progname
quit(0)
of "--Version":
mode = Mode.version
echo """
dragon $1
Copyright (C) 2014-2018 Michael Homer
This program comes with ABSOLUTELY NO WARRANTY.
See the source for copying conditions.
"""".dedent % $Version
quit(0)
of "-v", "--verbose":
verbose = true
of "-t", "--target":
mode = Mode.target
of "-x", "--and-exit":
andExit = true
of "-k", "--keep":
keep = true
of "-p", "--print-path":
printPath = true
of "-a", "--all":
dragAll = true
of "-i", "--icon-only":
iconsOnly = true
of "-T", "--on-top":
alwaysOnTop = true
of "-I", "--stdin":
fromStdin = true
of "-s", "--thumb-size":
if i + 1 == argc or parseInt(argv[i + 1], thumbSize) == 0:
write(stderr, "$1: error: bad argument for $2 `$3'.\n" % [progname, argv[i], argv[i + 1]])
quit(1)
argv[i] = ""
of "": discard # from -s
elif argv[i][0] == '-':
stderr.write("$1: error: unknown option `$2'.\n" % [progname, argv[i]])
iconTheme = gtk.getDefaultIconTheme()
window = newApplicationWindow(app)
# window.title = "Run"
window.setResizable(false)
window.setKeepAbove(alwaysOnTop)
vbox = gtk.newBox(Orientation.vertical, 6)
window.add(vbox)
window.title = "dragon"
if mode == Mode.target:
addTargetButton()
else:
#[
if fromStdin:
uriCollection = newSeq[string]()
elif dragAll:
uriCollection = newSeq[string]()
]#
for i in 1 ..< argc:
if argv[i][0] != '-' and argv[i][0] != '\0':
makeBtn(argv[i])
if fromStdin:
readstdin()
# if vbox.getChildren.len == 0: # an expensive test
if uriCount == 0:
echo("Usage: $1 [OPTIONS] FILENAME" % [progname])
quit(0)
let action = newSimpleAction("quit")
discard action.connect("activate", quitCB)
window.actionMap.addAction(action)
setAccelsForAction(app, "win.quit", "Q", "Escape") # see /usr/include/gtk-3.0/gdk/gdkkeysyms.h
showAll(window)
proc main =
let app = newApplication("org.gtk.example")
connect(app, "activate", appActivate)
discard run(app)
main() # 369 lines
Thank you again for your help. All my problems and questions have been solved therefore I am closing this issue. You were a huge help :)
Hi, I was trying to use these functions from
gdk.nim
, howeverlistTargets()
's body is just discarded, even when it's exported function.How so? Can I fix it somehow?