DnD
Тут разбор вот этого кастомного виджета
using Gtk;
using Gdk;
namespace DHGWidgets {
/**
* "Список заказов Dnd" - это / / Gtk.ListBox / / потомок, позволяющий переупорядочивать его строки
* with pointer or keyboard actions.
* Contrary to some other implementations that use child widgets to enable drag'n'drop behavior
* the ''DndOrderListBox'' is widget agnostic and operates only on native rows.
* The only caveat here is that child widgets must be able to receive drag events
* and thus must have own X11 windows.
*
* = Limitations =
*
* The ''DndOrderListBox'' construction methods allow it to be populated only with
* a //GLib.ListModel// descendant (most likely //GLib.ListStore//).
* However, as this component is a //work in progress// it may be extended
* to accept common //Gtk.ListBox// add, remove and insert methods
* at a later date.
* There is neither way nor sense to enable multiselection abilities
* in this particular widget. So, they remain masked.
*
* = Keyboard bindings =
*
* This widget provides users with extended keyboard bindings. Apart from standard
* //Gtk.ListBox// bindings (that are remapped) it comes with Vim-like ones by default.<<BR>>
* Bindings are as follows:
*
* == //moving selection// ==
*
* ''up'' and ''down'' cursors<<BR>>
* ''k'' and ''j'' letters (//vim//)
*
* == //initiating keyboard "drag"// ==
*
* ''<Ctrl>up'' and ''<Ctrl>down''
* ''i'' letter (//vim//)
*
* == //moving "drop" marker// ==
*
* ''<Ctrl>up'' and ''<Ctrl>down'' (so, to move marker <Ctrl> must stay depressed, it's like this to
* be consistent with native //Gtk.ListBox// behavior of these keys)
* ''k'' and ''j'' letters (//vim//)
*
* == //confirming// ==
*
* ''Enter'' key
*
* == //interrupting// ==
*
* ''Escape'' key or pointer d'n'd action
*
*/
public class DndOrderListBox : ListBox {
// The cached CssProvider.
static CssProvider _provider = new CssProvider ();
// The identifier of data that is transferred during pointer drag.
const string DATA_ID = "DND_ORDER_LIST_BOX_ROW_DATA";
// The default CSS stylesheet resource location.
const string DEFAULT_CSS_RESOURCE = "/me/dhg/dndorderlistbox/DndOrderListBox.css";
/**
* CSS classes that are used to change appearance of rows and show drag indicator.
* Left as //public// for convenience.
*/
public static string[] mgr_css_classes {get; set; default = {"place_up", "place_dn", "dragged", "dragging"};}
// Data must somehow be transferred on drop via pointer. While all could
// be accomplished via DndMgr, defining universal way to do it may allow
// for acceptance of drops from outside this widget.
static TargetEntry[] entries = {
TargetEntry () {
target = DATA_ID, flags = TargetFlags.SAME_APP, info = 0
}
};
// Private field storing reference to DndMgr
DndMgr _dnd_mgr;
/**
* Stores a model reference for easier access, as parent has it //bound_model//
* boxed and thus private.
*/
// <caveat> There is also a Gtk.ListStore so care is advised. That's why it's
// declared with prefix.
public GLib.ListStore model {get; set;}
/**
* Stores a reference to the keyboard bindings initializer. This class
* takes care about overriding and setting new bindings.
*/
public static DndOrderListBoxBindings kbd_bindings {get; set;}
static construct {
// The below method must be run during class registration to allow the widget being identified by
// a string element name in css selectors.
set_css_name ("dnd-order-list-box");
// Load default css here and add it to the Screen.
_provider.load_from_resource(DEFAULT_CSS_RESOURCE);
StyleContext.add_provider_for_screen (Screen.get_default(), _provider, STYLE_PROVIDER_PRIORITY_APPLICATION);
// And last, set up keybinding.
kbd_bindings = new DndOrderListBoxBindings(BindingSet.by_class((ObjectClass) ( typeof ( DndOrderListBox )).class_ref()));
}
/**
* Plain constructor. Mind to attach a model with bind_model method.
*/
public DndOrderListBox() {}
/**
* This construction method initializes ''DndOrderListBox'' with a //GLib.ListModel//
* derivative.
* Parameters are the same as in //Gtk.ListBox.bind_model//
*/
public DndOrderListBox.with_list(GLib.ListStore list_model, ListBoxCreateWidgetFunc fn) {
// Why this way, not via Object call?
// ListBoxCreateWidgetFunc is a delegate and Vala doesn't like copying
// it so it's not allowed in GObject type constructor -
// otherwise it could be stored and managed via own peoperties.
bind_list(list_model, fn);
}
construct {
// Only DndMgr is initialized here as it's common for both construction
// calls available.
_dnd_mgr = new DndMgr(this);
}
// Utility method that initiates keyboard action if it isn't yet initiated.
void kbd_init () {
// keyboard driven operation is made up of two phases
// any pointer drag operation will cancel it when initiated between said
// phases therefore we must make sure the keyboard driven operation
// is initiated
if ( _dnd_mgr.keyboard_action != true ) {
_dnd_mgr.keyboard_drag(get_selected_row());
}
}
// Utility method that exchanges entries in the model effectively
// rearranging rows.
// An additional benefit is keeping a reference to the source object,
// so saved another line otherwise sacrificed for a variable.
void exchange_rows(int remove, int add, Object? source) {
model.remove(remove);
model.insert(add, source);
}
// It took me a while to figure out this [Signal (action = true)] as signals
// in vala have implicit constructors with no obvious place to put flags.
// it turns out that signal parameters can be set via attributes
// for clarity, signals triggered via BindingEntry must be have
// G_BINDING_ACTION flag set to avoid being them managed as usual signals.
/**
* Manage starting keyboard action in Vim style.
*/
[Signal (action = true)]
public virtual signal void kbd_vim () {
if ( _dnd_mgr.keyboard_vim_mode ) {
_dnd_mgr.dragged(false);
} else {
kbd_init();
_dnd_mgr.keyboard_vim_mode = true;
}
}
/**
* Manage moving "drag" indicator when the keyboard action is pending.
*/
[Signal (action = true)]
public virtual signal void kbd_move_indicator (bool dir) {
kbd_init();
_dnd_mgr.keyboard_move_mark(dir);
}
/**
* Manage moving cursor (selection) by keyboard. If the vim mode is active
* it calls //kbd_move_indicator//.
*/
[Signal (action = true)]
public virtual signal void kbd_move_cursor (MovementStep step, int count) {
// If keyboard action is off just move the cursor normally.
if ( _dnd_mgr.keyboard_action != true ) {
move_cursor(step, count);
}
// If vim mode is on and movement step indicates single line movement
// (DISPLAY_LINES) call //kbd_move_indicator//.
if ( _dnd_mgr.keyboard_vim_mode && step == MovementStep.DISPLAY_LINES ) {
kbd_move_indicator(count != 1);
}
}
/**
* Interrupts the keyboard action.
*/
[Signal (action = true)]
public virtual signal void kbd_break () {
if ( _dnd_mgr.keyboard_action == true ) {
_dnd_mgr.dragged(false);
}
}
/**
* Confirms "drag" by keyboard action and moves the item dragged to a new place.
*/
[Signal (action = true)]
public virtual signal void kbd_confirm () {
if ( _dnd_mgr.keyboard_action == true ) {
// Cancel the keyboard action and remove styling
// (all done by _dnd_mgr.dragged).
_dnd_mgr.dragged(false);
// Remove source row from the list (cache it so a reference remains)
// and insert it at a new position.
exchange_rows(_dnd_mgr.source_position, _dnd_mgr.target_position, model.get_item(_dnd_mgr.source_position));
}
}
/**
* This method binds external model with the ''DndOrderListBox''.<<BR>>
* It also initializes internal operations that are dependent on the model.
*/
public void bind_list (GLib.ListStore list_model, ListBoxCreateWidgetFunc fn) {
// First push the model to a property.
model = list_model;
// Then bind model to the DndOrderListBox using provided creation function using
// parent's class method (as it's masked on this level).
base.bind_model(model, ( obj ) => {return fn (obj);});
// Then get a number of model items needed later to clamp operations.
// Update the drag manager so keyboard operations won't get too far.
_dnd_mgr.list_max = model.get_n_items();
// Then connect to changes in the model to update the state of this widget.
model.items_changed.connect(items_changed_cb);
// Finally get every new row and pass it to a method
// that manages signal handlers.
for ( int i = 0; i < _dnd_mgr.list_max; i++ ) {
add_cb(get_row_at_index(i));
}
}
// Internal method that adds all important callbacks to a row.
// It also sets every row to be both source and target of a drag operation.
internal void add_cb (Widget row) {
drag_source_set(row, BUTTON1_MASK, entries, DragAction.MOVE);
drag_dest_set(row, DestDefaults.ALL, entries, DragAction.MOVE);
row.drag_begin.connect(drag_begin_cb);
row.button_press_event.connect(button_press_event_cb);
row.drag_motion.connect(drag_motion_cb);
row.drag_data_get.connect(drag_data_get_cb);
row.drag_data_received.connect(drag_data_received_cb);
row.drag_end.connect(drag_end_cb);
}
// A final pointer drag handler that runs when drag ends.
void drag_end_cb (Widget row, DragContext c) {
// This event fires when drag ends other way than via drag_data_received_cb.
// Clears a visual state of the source by removing a css class marking
// it as being dragged and inactive.
_dnd_mgr.dragged(false);
}
// A signal handler that fires when there was drop and some data
// has been transferred to a target.
void drag_data_received_cb (Widget row, DragContext c, int x, int y, SelectionData selection_data, uint info, uint time_) {
// This event gets fired when the drop target receives data
// (so drop happened).
// First, remove a css class marking the source row as being dragged
// and inactive.
_dnd_mgr.dragged(false);
// Then test whether the target and the source are equal - if so,
// skip the data transfer
if ( _dnd_mgr.source == (ListBoxRow)row ) return;
// Grab the data and remove the source row from the list and insert it
// at a new position.
exchange_rows(_dnd_mgr.source_position, _dnd_mgr.target_position, ((Object[])selection_data.get_data ())[0]);
}
// A signal handler that gets data from the source row.
void drag_data_get_cb (Widget row, DragContext c, SelectionData selection_data, uint info, uint time_) {
// To enable data transfer during dnd operation use a SelectionData object.
// In case of this widget it would be possible to use a property
// of DndMgr object (which would be more straight forward), however the usual
// method is left in place to enable cross widget communication.
uchar[] row_obj = new uchar[( sizeof ( Object ))];
var data = model.get_item(_dnd_mgr.source_position);
((Object[])row_obj )[0] = data;
selection_data.@set(Atom.intern_static_string (DATA_ID), 32, row_obj);
}
// A signal handler that determines position of a pointer over the row
// it's called on.
bool drag_motion_cb (Widget row, DragContext c, int x, int y, uint time_) {
// To save some cycles a number of times the update gets trigerred is limited.
// This way instead of many times it happens just twice a row in each pass.
// Test for equality of this row and a cached one is performed in a DndMgr
// and blocks update if there is no need for it.
// Then the DndMgr determines whether a half of the row has been crossed -
// if so toggle css classes to show a bar that indicates possible next
// placement of a drop.
// Short explanation here - there are two ways (or more, but let's focus
// on the following) to mark a place where a dropped row can be placed
// (by marking I mean adding and removing css classes that add
// a visual indicator).
// One is to mark current or previous/next rows depending on a direction
// in a list and position of cursor over the current row.
// Another is to mark only the current row either atop or on its bottom
// depending on cursor position. I prefer the former solution as
// it greatly simplifies management of indication.
_dnd_mgr.update((ListBoxRow)row, y);
return false;
}
void drag_begin_cb (Widget row, DragContext context) {
// Initiating DndMgr's pointer driven drag.
_dnd_mgr.pointer_drag((ListBoxRow)row);
// Temporatily set a background filled css class so a Cairo surface
// can get the image properly rendered.
_dnd_mgr.drag_icon(true);
// Grab widget's rectangle.
var rect = Allocation();
_dnd_mgr.source.get_allocation(out rect);
// Create and paint the surface.
Cairo.ImageSurface surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, rect.width, rect.height);
Cairo.Context ctx = new Cairo.Context (surface);
_dnd_mgr.source.draw_to_cairo_context(ctx);
// Shift the surface icon so it appears in correct relation to the pointer.
surface.set_device_offset(-_dnd_mgr.start_x, -_dnd_mgr.start_y);
// Display the surface.
drag_set_icon_surface(context, surface);
// Switch off the temporary class that was set above.
_dnd_mgr.drag_icon(false);
// Setting a css class that renders the widget in quasi-disabled manner
// indicating its inactive state.
// <notice> While it could be tempting to set state flags of this widget
// to inactive it would make it unresponsive and logically detached
// from the parent.
// It's then better to have it responsive and only visually greyed
// to retain the ability to properly catch events and count passes.
// For instance, one of unwanted effects that occur when the row
// has StateFlags.INSENSITIVE is a sudden jump of indicator when
// a pointer passes over it.
_dnd_mgr.dragged(true);
}
bool button_press_event_cb (Widget row, EventButton event) {
// For unknown reasons the row must be selected in button_press_event_cb
// explicitly as after the drag'n'drop operation first pointer action doesn't
// select the row (regardless of this event being actually fired).
select_row((ListBoxRow)row);
// Getting coordinates and storing them in _dnd_mgr, so in case a drag occurs
// the widget will know the starting point of a pointer action.
_dnd_mgr.start_x = (int) event.x;
_dnd_mgr.start_y = (int) event.y;
// Grab focus on the row so keyboard and pointer states remain consistent.
get_selected_row().grab_focus();
return false;
}
void items_changed_cb (uint position, uint removed, uint added) {
// Make sure the number of rows is updated and stored in the drag manager
// so keyboard operations won't go too far.
_dnd_mgr.list_max = model.get_n_items();
if ( added > 0 ) {
// Newly added rows must be bound to rows' specific signal handlers.
for ( uint i = 0; i < added; i++ ) {
add_cb(get_row_at_index((int)( i + position )));
}
// Select a row that was presumably added due to d'n'd action.
// If it was added from outside no harm is inflicted as the below
// condition makes sure the row number is lower than number of rows.
if ( added == 1 && _dnd_mgr.target_position<_dnd_mgr.list_max ) {
// Select and grab focus on newly added row so keyboard and pointer states
// remain consistent
var new_row = get_row_at_index(_dnd_mgr.target_position);
select_row(new_row);
new_row.grab_focus();
}
}
}
// Mask ListBox methods that enable manual manipulation on rows and loading
// a model. Only locally managed ListStore based operations are allowed.
// NOT SURE IF I DID IT RIGHT - suggestions are welcome.
private new void add(){}
private new void remove() {}
private new void insert() {}
private new void set_selection_mode (SelectionMode m) {}
private new void bind_model (GLib.ListStore m, ListBoxCreateWidgetFunc f) {}
}
}
Last updated