/* ***************************************************************************
# Compiled by:
#
src/lib/tk/src/toolkit/sources.sublib A small drag&drop package for tk.
It is generic over drag&items as given by api Drag_And_Drop_Items.
See the documentation for more details.
"tests+examples/boxes.pkg" contains a small example of how to use this
package.
$Date: 2001/03/30 13:39:38 $
$Revision: 3.0 $
Author: cxl (Last modification $Author: 2cxl $)
(C) 1996, 1998, Bremen Institute for Safe Systems, Universitaet Bremen
************************************************************************** */
generic package drag_and_drop_g (
#
drag_and_drop_items: Drag_And_Drop_Items # Drag_And_Drop_Items is from
src/lib/tk/src/toolkit/drag-and-drop.api)
: (weak) Drag_And_Drop # Drag_And_Drop is from
src/lib/tk/src/toolkit/drag-and-drop.api# where type item = drag_and_drop_items::item
{
include package tk;
include package basic_utilities;
Item = drag_and_drop_items::Item;
Dd_Canvas = Widget_Id;
exception DRAG_AND_DROP String;
# local variables
drop_zones = REF ([]: List( (Widget_Id, Item, Box) ) );
fun print_drop_zones ()
=
ddebug ("dropZones " +
(string::join ", "
((map (canvas_item_id_to_string o drag_and_drop_items::get_canvas_item_id o #2)
*drop_zones))))
also
fun ddebug (str) = debug::print 11 ("DD: " + str);
Grab_Item = (Item, null_or::Null_Or (Box), Coordinate);
grab_items = REF ([]: List( Grab_Item ));
sel_items = REF ([]: List( Item ));
lasso = REF (NULL: Null_Or( (Canvas_Item_Id, Coordinate) ) );
old_pos = REF (coordinate (0, 0));
can_drop = REF FALSE;
grab_pos = REF (coordinate (0, 0));
Enter_Status = ENTERED Item
| NOTHING_ENTERED
| LEFT_CANVAS;
entered_item = REF nothing_entered;
# initialize all the references above
fun init_refs ()
=
{ drop_zones := [];
grab_items := [];
sel_items := [];
lasso := NULL;
old_pos := (0, 0);
can_drop := FALSE;
grab_pos := (0, 0);
entered_item := nothing_entered;
};
# Equality for items:
fun eq item1 item2
=
( (drag_and_drop_items::get_canvas_item_id item1) ==
(drag_and_drop_items::get_canvas_item_id item2)
);
# Apply function on items on grabItem list
fun app_it f
=
apply
(\\ (it, _, _) = f it);
fun over_drop_zone cnv coords
=
map #2 (list::filter (\\ (c0, _, r) =
(inside coords r) and (c0 == cnv))
(*drop_zones));
# This could be done in a much more efficient way, eg.
# use btrees or something (oh aye ;-)
# find all items inside rectangle r on canvas cnv
fun drop_zones_in_box cnv r
=
map #2 (list::filter (\\ (c0, _, r0) =
(intersect r0 r) and (c0 == cnv))
*drop_zones);
# Get current drop zone (i.e. on-the-canvas coordinates) of item
# it (taken from *dropZones for efficiency, wow).
# Note that c_item id's are global, so there's no need to check
# the widget it (I hope :-)
fun get_current_drop_zone it
=
null_or::map #3 (list::find ((eq it) o #2) *drop_zones);
# Delete item from drop zone list
fun del_drop_zone item
=
drop_zones
:=
list::filter (not o (eq item) o #2) *drop_zones;
# Not clear wethere we want event_callbacks on *all* items of CANVAS_TAG or
# just the first one.. currently the former:
fun add_tag_naming wid id event_callbacks
=
{ item = get_canvas_item wid id;
case item
CANVAS_TAG { citem_ids=>ls, ... } => apply (\\ id=> add_tag_naming wid id event_callbacks; end ) ls;
_ => add_canvas_item_event_callbacks wid id event_callbacks; esac;
};
# "private" version of delete_canvas_item, this one deletes
# `subitems' of CANVAS_TAG-items (delete_canvas_item _doesn't_).
fun rec_delete wid cid
=
{ cit = get_canvas_item wid cid;
{ delete_canvas_item wid cid;
case cit
CANVAS_TAG { citem_ids=>subitems, ... } => apply (rec_delete wid) subitems;
_ => (); esac
;};
};
# default cursors
# the "hand" appearing when you are ready to grab an item
enter_cursor = CURSOR (XCURSOR (make_cursor_name("hand1"), NULL));
# the crosshair for dragging an item
drag_cursor = CURSOR (XCURSOR (make_cursor_name("fleur"), NULL));
# the clipboard -- unless we reexport it, this class is just
# a shortcut
package clipboard = drag_and_drop_items::clipboard;
# unfortunately, we cinnae use set_canvas_item_coordinates with CANVAS_TAG(..) items,
# so we have to move_canvas_item the buggers-- hence the following function:
fun move_item dd id w_here
=
move_canvas_item dd id (subtract_coordinates w_here (hd (get_tcl_canvas_item_coordinates dd id)));
fun btwyc dd (item, dz, old_item_pos)
=
# "back to whence you came",
# reinstall a grabbed item and dropZone at original position
{ case dz
NULL => ();
THE dz => drop_zones := (dd, item, dz) . *drop_zones;
esac;
move_item
dd
(drag_and_drop_items::get_canvas_item_id item)
old_item_pos;
drag_and_drop_items::release item;
};
fun move_grab_it dd off (item, THE dz, old_item_pos)
=>
# move a grabbed item, plus its drop zone, to a new position.
btwyc dd (item, THE (move_box dz off), add_coordinates old_item_pos off);
move_grab_it dd off (item, NULL, old_item_pos) =>
btwyc dd (item, NULL, add_coordinates old_item_pos off); end;
# enter/leave are bound to particular canvas items
# these _should_ only be called when there's no grab. Unfortunately,
# the wish seems to generate spurious enter/leave events, so we better
# check. Defensive programming an' aw.
fun enter_item dd_canvas it _
=
if ( not (drag_and_drop_items::is_immobile it)
and null *grab_items
)
add_trait dd_canvas [ enter_cursor ];
entered_item := entered it;
ddebug ("Entered " + canvas_item_id_to_string (drag_and_drop_items::get_canvas_item_id it));
fi
# Some wish´s (e.g. Tk8.0 under Linux KDE) generate Leave-Events
# if you press the button while over a Canvas_Item when the PressButton is bound
# to the canvas below, before they generate a PressButton event (if you
# can follow, I admit it confused either me or Tcl as well). Hence,
# we only generate a "leave" here if we have really left (i.e. the
# coordinates are outside the dropzone of the item).
also
fun leave_item dd_canvas it (TK_EVENT(_, _, x, y, _, _))
=
if (null *grab_items)
case (get_current_drop_zone it)
THE dz
=>
if (not (inside (x, y) dz))
add_trait dd_canvas [ CURSOR NO_CURSOR ];
entered_item := nothing_entered;
ddebug ("Left " + canvas_item_id_to_string (drag_and_drop_items::get_canvas_item_id it));
fi;
NULL => ();
esac;
fi
# press/releaseGrabButton are bound to the canvas
also
fun press_sel_button dd_canvas (TK_EVENT(_, _, x, y, _, _))
=
case *entered_item
entered over_it
=>
if (not (list::exists (eq over_it) *sel_items))
sel_items := over_it . *sel_items;
drag_and_drop_items::select over_it;
ddebug ("selected item: " + canvas_item_id_to_string (drag_and_drop_items::get_canvas_item_id over_it));
fi;
nothing_entered
=>
{ # Click over empty canvas: deselect items
apply drag_and_drop_items::deselect *sel_items;
sel_items := [];
};
_ => ();
esac
also
fun press_grab_button dd_canvas (TK_EVENT(_, _, x, y, _, _))
=
case *entered_item
entered over_it
=>
{ fun w_here it
=
hd (get_tcl_canvas_item_coordinates dd_canvas
(drag_and_drop_items::get_canvas_item_id it));
# lose grabbed item if also selected:
items = (list::filter (not o (eq over_it)) *sel_items);
# Grab a selected item
#
fun grab_it it
=
{ cur_dz = get_current_drop_zone it;
del_drop_zone it;
(it, cur_dz, w_here it);
};
{ # Reverse map of grabbed items, since
# they are "the wrong way around" --
# last ones selected first:
#
grab_items := reverse (map grab_it (over_it . items));
entered_item := nothing_entered;
old_pos := coordinate (x, y);
grab_pos := coordinate (x, y);
sel_items := [];
add_trait dd_canvas [drag_cursor];
app_it drag_and_drop_items::grab *grab_items;
ddebug ("grabbed items: " +
(string::join " "
(map (canvas_item_id_to_string o drag_and_drop_items::get_canvas_item_id o #1)
*grab_items)));
};
};
_ => # start new lasso.
{ rid = make_canvas_item_id();
lasso_box = CANVAS_BOX { citem_id=> rid,
coord1=> coordinate (x, y),
coord2=> coordinate (x, y),
traits=> [WIDTH 2],
event_callbacks=> [] };
add_canvas_item dd_canvas lasso_box;
lasso := THE (rid, coordinate (x, y));
};
esac
also
fun grabbed_motion dd_canvas (TK_EVENT(_, _, x, y, _, _))
=
if (null *grab_items)
case *lasso
THE (box_id, lhc)
=>
set_canvas_item_coordinates dd_canvas box_id [lhc, coordinate (x, y)];
NULL => ();
esac;
else
grab_ids = map #1 *grab_items;
fun mv_grab_item it
=
drag_and_drop_items::move it (subtract_coordinates (coordinate (x, y))
*old_pos);
cs = coordinate (x, y);
apply mv_grab_item grab_ids;
old_pos := cs;
case *entered_item
entered it
=>
case (get_current_drop_zone it)
THE dz
=>
if (not (inside *old_pos dz))
# Have left entered item:
#
entered_item := nothing_entered;
if *can_drop drag_and_drop_items::leave it; fi;
can_drop := FALSE;
fi;
NULL => ();
esac;
_ => # Have we entered an item?
{ over = over_drop_zone dd_canvas cs;
case over
oo . _
=>
{ entered_item := entered oo;
can_drop := drag_and_drop_items::enter oo grab_ids;
ddebug ( "have entered " +
(canvas_item_id_to_string (drag_and_drop_items::get_canvas_item_id oo)) +
": " + (bool::to_string *can_drop));
};
[] => ();
esac;
};
esac;
fi
also
fun release_grab_button dd_canvas (ev as TK_EVENT(_, _, x, y, _, _))
=
if (null *grab_items)
case *lasso
THE (rid, llc as (x0, y0))
=>
{ # Delete lasso:
#
delete_canvas_item dd_canvas rid;
lasso := NULL;
# throw lasso: if the lasso has not been thrown
# further than five units, ignore it. (This is
# in particular to avoid having the release-event after
# a double-click causing a lasso throw-- we always end
# up with the construction object selected!)
#
if ((int::abs (x0- x) > 5) and
(int::abs (y0- y) > 5)
)
# valid throw: find selected items, delete lasso
#
selits= drop_zones_in_box dd_canvas
(make_box (llc, coordinate (x, y)));
apply drag_and_drop_items::select selits;
sel_items := *sel_items @ selits;
ddebug ("Caught " + (string::join ", "
(map (canvas_item_id_to_string o
drag_and_drop_items::get_canvas_item_id)
selits)));
else
ddebug("Invalid lasso throw: not far enough");
fi;
};
NULL => ();
esac;
else
case *entered_item
entered it
=>
if *can_drop
# First, do the drop operation:
#
if (drag_and_drop_items::drop it (map #1 *grab_items) )
# Non-destructive drop, reinstall item
# at original position
#
apply (btwyc dd_canvas) *grab_items;
else
# Destructive, argument items vanish:
#
app_it ((rec_delete dd_canvas) o drag_and_drop_items::get_canvas_item_id)
*grab_items;
fi;
# Don't need to delete dropZone
# generate leaving event for entered item:
#
drag_and_drop_items::leave it;
else
# Can't drop, reinstall at grab position
# w/ original dropzone:
#
apply (btwyc dd_canvas) *grab_items;
fi;
nothing_entered
=>
# Have note entered anything, so we must move the items:
#
apply (move_grab_it dd_canvas (subtract_coordinates (coordinate (x, y))
*grab_pos)) *grab_items;
left_canvas
=>
# off the canvas, put items into clipboard
# This is awkward -- we reinstall the item on the
# DDcanvas, complete with dropZone, and have it
# deleted by the callback function of the clipboard.
# Thus, only objects appearing elsewhere are
# deleted from the DDcanvas.
#
{ fun del_it it
=
{ del_drop_zone it;
rec_delete dd_canvas (drag_and_drop_items::get_canvas_item_id it);
# except exceptions here ?!
};
{ apply (btwyc dd_canvas) *grab_items;
drag_and_drop_items::clipboard::put (drag_and_drop_items::item_list_abs (map #1 *grab_items)) ev
(\\() = app_it del_it *grab_items);
};
};
esac;
# In any case, reset variables and the cursor.
#
entered_item := nothing_entered;
grab_items := [];
can_drop := FALSE;
add_trait dd_canvas [CURSOR (NO_CURSOR)];
fi
also
fun dd_item_namings can_id item
=
[ EVENT_CALLBACK (ENTER, enter_item can_id item),
EVENT_CALLBACK (LEAVE, leave_item can_id item)
]
also
fun place dd_canvas item
=
{ cid = drag_and_drop_items::get_canvas_item_id item;
wher = get_tcl_canvas_item_coordinates dd_canvas cid;
nudz = move_box (drag_and_drop_items::sel_drop_zone item) (hd wher);
# (hd (get_tcl_canvas_item_coordinates ddCanvas cid))
ddebug ("place " + (canvas_item_id_to_string cid) + ", dropZone " + (show_box nudz));
drop_zones := (dd_canvas, item, nudz) . *drop_zones;
add_tag_naming dd_canvas cid (dd_item_namings dd_canvas item);
};
fun leave_canvas _
=
entered_item := left_canvas;
# I forget why I thought this function was necessary :-)
# Anyway, it´s definitely harmful since some wish´s seem to generate an enter
# event for the canvas widget when pressing the button over a canvas item
# (see the comments above-- I can´t see no sense in this sense either)
#
# fun enterCanvas _ =
# enteredItem := NothingEntered
# plus
# EVENT_CALLBACK (MODIFIER_BUTTON (1, ENTER), enterCanvas)];
# below.
#
# Reset the drag&drop module -- i.e. don't reset it
# to initial value, but reset the grabbed items etc. to some sane
# values so we can continue.
# This function can be bound to an interrupt handler, and called
# if the drag&drop for some reason buggers up. Since it's very state-
# based, and makes assumptions on the order in which events are generated
# which may not hold on a particular wish, this may happen.
fun reset dd_canvas
=
{ # reset currently grabbed items
apply (btwyc dd_canvas) *grab_items;
grab_items := [];
/* reset enteredItem */
case *entered_item entered eit => drag_and_drop_items::leave eit; _ => (); esac;
entered_item := nothing_entered;
# Deselect items
apply drag_and_drop_items::deselect *sel_items;
sel_items := [];
can_drop := FALSE;
add_trait dd_canvas [CURSOR NO_CURSOR];
# Delete lasso
case *lasso THE (rid, _) => delete_canvas_item dd_canvas rid; NULL => (); esac;
lasso := NULL
;};
fun canvas_event_callbacks can_id
=
[ EVENT_CALLBACK (BUTTON_PRESS (THE 1), press_grab_button can_id),
EVENT_CALLBACK (BUTTON_RELEASE (THE 1), release_grab_button can_id),
EVENT_CALLBACK (BUTTON_PRESS (THE 2), press_sel_button can_id),
EVENT_CALLBACK (MODIFIER_BUTTON (1, MOTION), grabbed_motion can_id),
EVENT_CALLBACK (MODIFIER_BUTTON (1, LEAVE), leave_canvas)
];
fun init canvas_id
=
/* Raises an exception of passed a widget which isnae a canvas, or
* a canvas wi' items on it.
*/
case (get_widget canvas_id)
CANVAS { widget_id=>wid, citems=>cids, ... }
=>
{ map ((tk::delete_canvas_item wid) o tk::get_canvas_item_id) cids;
# fast remove of citems in DD
init_refs();
# implicit remove of potential items in dragZone
add_event_callbacks wid (canvas_event_callbacks wid);
# DropZones := []; should be superfluous - initRefs!. bu
ddebug("init " + (widget_id_to_string wid));
wid;};
/*
| CANVAS { widget_id=wid, citems= x . xs, ... } =>
raise exception DRAG_AND_DROP "init: called with non-empty canvas."
CHANGED - bu
*/
w => raise exception DRAG_AND_DROP "init: argument not a canvas."; esac;
fun delete dd_canvas item
=
{ case *entered_item
entered it => if (eq item it )
{ entered_item := nothing_entered;
add_trait dd_canvas [CURSOR (NO_CURSOR)]
;};
fi;
_ => ();
esac;
grab_items := list::filter (not o (eq item) o #1) *grab_items;
sel_items := list::filter (not o (eq item)) *sel_items;
if (null *grab_items)
add_trait dd_canvas [CURSOR (NO_CURSOR)];
fi;
del_drop_zone item;
# And delete the Canvas_Item:
rec_delete
dd_canvas
(drag_and_drop_items::get_canvas_item_id item);
};
fun selected_items ()
=
*sel_items
@
(map
(\\ (x, _, _) = x)
*grab_items
);
fun all_items dd_canvas
=
map #2 (list::filter (\\ (cnv, _, _) = cnv == dd_canvas) *drop_zones);
# --- Some implementation notes. -------------------------------
#
# - Although it would undoubtedly be better to export DDCanvas as an
# abstract enum, this is not possible because generate_gui_g uses the fact
# that DDCanvas is a widget to install forward references to the
# exported functions from within the argument class of D&D.
#
# - More than one d&d canvas: I'm note sure this works the way the
# d&d module is implemented just now. One might have to do some more checks
# to ensure that grabbing/entering etc. does only effect items on the same
# canvas. (All of this would be very easy indeed if SML had dynamic modules.
# Oh well.)
#
# ---cxl.
};