StefanSalewski / gintro

High level GObject-Introspection based GTK3/GTK4 bindings for Nim language
MIT License
297 stars 20 forks source link

GDK DragContext listTargets function is not defined #198

Closed adokitkat closed 2 years ago

adokitkat commented 2 years ago

Hi, I was trying to use these functions from gdk.nim, however listTargets()'s body is just discarded, even when it's exported function.

proc gdk_drag_context_list_targets(self: ptr DragContext00): ptr glib.List {.
    importc, libprag.}

proc listTargets*(self: DragContext): seq[Atom] =
  discard

How so? Can I fix it somehow?

StefanSalewski commented 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.

adokitkat commented 2 years ago

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.

StefanSalewski commented 2 years ago

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.

StefanSalewski commented 2 years ago

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 :-(

StefanSalewski commented 2 years ago

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.

StefanSalewski commented 2 years ago

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

adokitkat commented 2 years ago

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.

adokitkat commented 2 years ago

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()
adokitkat commented 2 years ago

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?

image

StefanSalewski commented 2 years ago

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.

adokitkat commented 2 years ago

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.

StefanSalewski commented 2 years ago

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.

StefanSalewski commented 2 years ago

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.

adokitkat commented 2 years ago

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.

StefanSalewski commented 2 years ago

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.

StefanSalewski commented 2 years ago

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

adokitkat commented 2 years ago

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

StefanSalewski commented 2 years ago

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):
StefanSalewski commented 2 years ago

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.

StefanSalewski commented 2 years ago

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"

adokitkat commented 2 years ago

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.

StefanSalewski commented 2 years ago

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.

StefanSalewski commented 2 years ago

I suspect the problem is in dragDataGet()

Yes, I had the same feeling. Corrupted file name for gedit.

adokitkat commented 2 years ago

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!

StefanSalewski commented 2 years ago

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 :-)

adokitkat commented 2 years ago

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 :)

StefanSalewski commented 2 years ago

Yes, looks like one more gintro bug. Will try to fix it tomorrow.

StefanSalewski commented 2 years ago

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.

StefanSalewski commented 2 years ago

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()
StefanSalewski commented 2 years ago

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.

StefanSalewski commented 2 years ago

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.

StefanSalewski commented 2 years ago

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
adokitkat commented 2 years ago

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 :)