Rearrange visual elements by drag & drop

I was in need of a visual representation of nodes on the server and a way to rearranged these by mouse-dragging. I could not find any extensions doing this so I cooked up some code for that purpose. It works quite well on my system (Mac M1, OSX, SC in Rosetta mode) but the drawback is that it requires the server to be on for the MouseX ugen. Is there a way to poll the position of the mouse in sclang? Also, are there any better/alternative ways of achieving a similar result, maybe even a quark that does this already?

(
s.waitForBoot{
	b = Bus.control(s); // for MouseX values
	{ Out.kr(b, MouseX.kr()) }.play;
	~screenWidth = Window.screenBounds.width;
	~mouseResponsiveness = 0.8; // user can change
	~size = 100; // user can change
	~spacing = 10; // user can change
	~numViews = 5; // user can change
	v = ~numViews.collect{|i| View().fixedSize_((~size - ~spacing)@(~size - ~spacing))
		.layout_(HLayout(StaticText().string_(i).align_(\center))).background_(Color.rand) 
	};
	w = View().layout_(VLayout(HLayout(*v), StaticText().string_("Drag squares to rearrange")).spacing_(~spacing).margins_(~spacing)).front;
	~order = (0..v.size - 1);
	
	v.collect{|view, i|
		view.mouseDownAction_{
			~r.reset;
			~r.play;
			~curIndex = i;
		};
		view.mouseUpAction_{ 
			~r.stop;
			v[~curIndex].moveTo(~order.indexOfEqual(~curIndex) * ~size + ~spacing, ~spacing)
		};
	};
	
	
	
	~swap = {|i, type = 1|
		var me = ~order.indexOfEqual(i);
		var target = (~order[me + type]);
		if (target.notNil)
		{ 
			var targetPos = ((~order.indexOfEqual(target) * ~size) + (type * -1 * ~size) + ~spacing); 
			v[target].moveTo(targetPos, ~spacing);
			~order.swap(me, ~order.indexOfEqual(target)).debug(\order);
		}
	};
	
	~r = Routine{
		var initX = b.getSynchronous;
		var flag = false;
		var offset =  ( ~order.indexOfEqual(~curIndex) * ~size);
		s.sync;
		0.1.wait;
		loop{
			var mouseX = b.getSynchronous;
			var pos = ~screenWidth  * ( (mouseX - initX) * ~mouseResponsiveness) + offset;
			var relativePos = pos%~size;
			var upperEdge = ~size * 0.8;
			var lowerEdge = ~size * 0.2;
			v[~curIndex].front;
			
			{ v[~curIndex].moveTo(pos, ~spacing) }.defer;
			
			case 
			{ (relativePos > upperEdge) && flag }
			{ 
				flag = false; 
				{ ~swap.(~curIndex, 1) }.defer 
			}	
			{ (relativePos < lowerEdge) && flag }
			{ 
				flag = false; 			
				{ ~swap.(~curIndex, -1) }.defer	
			}
			{ ( ( (relativePos < upperEdge) && (relativePos > lowerEdge)) ) && flag.not }
			{ flag = true };
			0.05.wait
		}
	}
}
)

All mouse actions (e.g. .mouseDownAction) for View give you the x and y coordinates of the mouse in sclang, so you don’t need to get it from the Server.
See Mouse Actions:
https://doc.sccode.org/Classes/View.html#Mouse%20actions
Hope that helps,
Paul

Thanks, it’s strange, I just looked at that recently and then I forgot about, thanks for reminding me! It would be easy to get the initial mouse position using .mouseDownAction but how would I manage to get continuous mouse-updates after a mousedown event?

mouseMoveAction is what you’re looking for.

1 Like

There’s the view :

(
var win = Window("", Rect(500, 500, 300, 150));

var myCustomFunction = {
	var squareNames = List(0);
	"Node order has been rearranged".postln;
	"Order is now :".postln;
	squares.do({ |square|
		squareNames.add(square[\name]); });
	squareNames.postln;
	"".postln;
};

var selectedSquare = -1;
var previousSquare = -1;
var margin = 5;
var mouseOffset = 0;
var mousePos = 0;

var squares = Array.fill(5,
	{ |index|
		(
			\name: index.asString,
			\color: Color(1.0.rand, 1.0.rand, 1.0.rand)
		)
});

var dragView = UserView();

dragView.drawFunc_({ |view|

	var caseWidth = view.bounds.width;
	caseWidth = caseWidth - (margin * (squares.size + 1));
	caseWidth = caseWidth / squares.size;

	squares.do({ |square, index|
		if(selectedSquare != index) {
			var rect = Rect(
				margin + ((caseWidth + margin) * index),
				margin,
				caseWidth,
				view.bounds.height - (margin * 2);
			);
			Pen.fillColor_(squares[index][\color]);
			Pen.fillRect(rect);
			Pen.stringCenteredIn(
				squares[index][\name],
				rect,
				Font.default,
				Color.black
			);
		};
	});

	if(selectedSquare != -1) {
		var rect = Rect(
			mousePos - mouseOffset,
			margin,
			caseWidth,
			view.bounds.height - (margin * 2)
		);
		Pen.fillColor_(squares[selectedSquare][\color]);
		Pen.fillRect(rect);
		Pen.stringCenteredIn(
			squares[selectedSquare][\name],
			rect,
			Font.default,
			Color.black
		);
	};
});

dragView.mouseDownAction_({ |view, x|
	var caseWidth = (view.bounds.width -
		(margin * (squares.size + 1)) / squares.size);

	selectedSquare = x.linlin(
		0, view.bounds.width,
		0, squares.size).asInteger;
	previousSquare = selectedSquare;

	mouseOffset = x%(caseWidth + margin);
	mouseOffset = mouseOffset - margin;
	mousePos = x;

	view.refresh;
});

dragView.mouseMoveAction_({ |view, x|
	if(selectedSquare != -1) {
		var hoveredSquare = x.linlin(
			0, view.bounds.width,
			0, squares.size).asInteger;

		if(hoveredSquare == squares.size)
		{ hoveredSquare = hoveredSquare - 1 };

		if(hoveredSquare != selectedSquare) {
			var squareToMove = squares[hoveredSquare];
			squares[hoveredSquare] = squares[selectedSquare];
			squares[selectedSquare] = squareToMove;
			selectedSquare = hoveredSquare;
			view.refresh;
		};

		mousePos = x;
	};
	view.refresh;
});

dragView.mouseUpAction_({ |view, x|
	if(previousSquare != selectedSquare) {
		myCustomFunction.value;
	};
	selectedSquare = -1;
	view.refresh;
});

win.layout_(
	VLayout(
		dragView
	).margins_(0);
);

win.front;

CmdPeriod.doOnce({ win.close });
)

Yes! That’s is the one, don’t know why I couldn’t find it in the first place. I just briefly tested your code and it works as needed. The thing is that the actual views I will be pushing around already have layouts so it is not practical for me to use drawFunc, but I suppose your code could be modified to work with a view with a layout. I will try it later today. Thanks for posting.

I never managed views using another view, but it turned out to be easier than I though.

I first tried to put everything in a StackLayout, with mode set as \stackAll, but it prevents from moving views, they’re stuck at 0@0.

You instead need to simply add them as a child, without a layout enclosing them.

One of my concern was that if your top view has some action, and your child views also have actions, they will take precedence over the parent. So I made the example with Buttons, which would normally prevent the ‘drag’ action because they’ll evaluate their action when you click them (which is the correct behaviour).

Here, I use View.globalKeyDownActions (and Up) to disable buttons and allow the drag to take place when user is holding CTRL.
If your views do not have any widget that responds to mouseAction, my example is to be simplified.

I didn’t find out how to draw the selected view on top of the other ones, they stay in their order of instantiation. This could be possible within a StackLayout, but as mentioned earlier, causes other problems.

(
var win = Window("", Rect(500, 500, 300, 150));

var myCustomFunction = {
	var viewsOrder = List(0);
	"Views order modified :".postln;
	views.do({ |view| viewsOrder.add(view.states[0][0]); });
	viewsOrder.postln;
	"".postln;
};

var myButtonFunction = { |name|
	"You clicked : ".post;
	name.postln;
	"".postln;
};

var selectedView = -1;
var previousView = -1;
var margin = 5;
var mouseOffset = 0;
var mousePos = 0;
var canDrag = true;

var dragView = UserView(
).background_(Color.grey);

var views = Array.fill(5,
	{ |index|
		var button = Button(dragView
		).states_([[
			index.asString,
			Color.black,
			Color(1.0.rand, 1.0.rand, 1.0.rand)]]
		).action_({ myButtonFunction.value(index.asString) });
		button
});

var reArrangeViews = {
	var caseWidth = (dragView.bounds.width -
		(margin * (views.size + 1))) / views.size;

	views.do({ |view, index|

		if(selectedView != index) {
			view.moveTo(
				margin + ((caseWidth + margin) * index),
				margin);

			view.fixedSize_(
				Point(
					caseWidth,
					dragView.bounds.height - (margin * 2)));
		} {
			view.moveTo(
				mousePos - mouseOffset,
				margin);

			view.fixedSize_(
				Point(
					caseWidth,
					dragView.bounds.height - (margin * 2)));
		};
	});
};

dragView.onResize_({ reArrangeViews.value; });

dragView.mouseDownAction_({ |view, x|
	if(canDrag) {
		var caseWidth = (view.bounds.width -
			(margin * (views.size + 1)) / views.size);

		selectedView = x.linlin(
			0, view.bounds.width,
			0, views.size).asInteger;
		previousView = selectedView;

		mouseOffset = x%(caseWidth + margin);
		mouseOffset = mouseOffset - margin;
		mousePos = x;
	};
});

dragView.mouseMoveAction_({ |view, x|
	if(selectedView != -1) {
		var hoveredView = x.linlin(
			0, view.bounds.width,
			0, views.size).asInteger;

		if(hoveredView == views.size)
		{ hoveredView = hoveredView - 1 };

		if(hoveredView != selectedView) {
			var viewToMove = views[hoveredView];
			views[hoveredView] = views[selectedView];
			views[selectedView] = viewToMove;
			selectedView = hoveredView;
			view.refresh;
		};

		mousePos = x;

		reArrangeViews.value;
	};

});

dragView.mouseUpAction_({ |view, x|
	if(previousView != selectedView) {
		myCustomFunction.value;
	};
	selectedView = -1;
	reArrangeViews.value;
});

View.globalKeyDownAction = FunctionList();
View.globalKeyDownAction.addFunc({
	|view, char, mod, unicode, keycode, key|

	if(key == 16777249) {
		views.do({ |view|
			view.acceptsMouse_(false);
		});
		canDrag = true;
	};
});

View.globalKeyUpAction = FunctionList();
View.globalKeyUpAction.addFunc({
	|view, char, mod, unicode, keycode, key|

	if(key == 16777249) {
		views.do({ |view|
			view.acceptsMouse_(true);
		});
		canDrag = false;
	};
});

win.layout_(
	VLayout(
		dragView
	).margins_(0);
);

win.front;

CmdPeriod.doOnce({ win.close });
)

Thanks for the suggestions. I think there is a simpler way around this, I will work on when I got some more time. For one thing it is ok to drag a view with a layout on it as long as you don’t try to drag it by a button or something like that, just grab somewhere in the view there there isn’t already another gui element and assign the mouseaction to the view.

I can’t get your code to move views on my setup. Here is the adaptation of my original code, where MouseX is replaced with .mouseMoveAction. The button prevents propagation of mouseMoveActions to the parent view, so the view doesn’t respond to drag when the button is pressed and held.

(
~size = 100;
~spacing = 10;
~order = [\In, \Delay, \Ring, \Reverb, \Out];
~createView = {|n|
	var e = (
		name: StaticText().string_(n).stringColor_(Color.white).align_(\center),
		but: Button().states_([[\OFF], [\ON, \, Color.green]]).mouseMoveAction_{ true } // prevents propagation to parent view
		.fixedHeight_(20).value_(2.rand)
	);
	View().layout_(VLayout(e[\name], e[\but])
		.spacing_(5).margins_(5)).background_(Color.rand).fixedSize_((~size - ~spacing)@(~size - ~spacing))
};
v = ();
~order.do{|n|v.add(n -> ~createView.(n))};
~layout = HLayout(*~order.collect{|n|v[n] }).spacing_(~spacing).margins_(~spacing);
w = View().layout_(~layout).front.alwaysOnTop_(true);


~mouseDrag = {|order, v, size = 100, spacing = 10|
	var leftInit, offset, flag = false, upperEdge = size * 0.8, lowerEdge = size * 0.2, curKey;
	var swap = {|key, type = 1|
		var me = order.indexOfEqual(key);
		var target = (order[me + type]);
		\swap.postln;
		if (target.notNil)
		{
			var targetPos = ((order.indexOfEqual(target) * size) + (type * -1 * size) + spacing);
			v[target].moveTo(targetPos, spacing);
			order.swap(me, order.indexOfEqual(target)).debug(\order);
		}
	};
	order.do{|n|
		v[n].mouseDownAction_{|view, x, y|
			\mouseDown.postln;
			leftInit = v[n].bounds.left;
			offset = x;
			flag = false;
			curKey = n;
		};
		
		v[n].mouseUpAction_{
			v[curKey].moveTo(order.indexOfEqual(curKey) * size + spacing, spacing)
		};
		
		v[n].mouseMoveAction_{|view, x, y|
			var pos = v[n].bounds.left - leftInit;
			var relativePos = pos%size;
			
			{ v[n].moveTo((v[n].bounds.left + x - offset).max(0).min(order.size - 1 * size + (2 * spacing)), 10) }.defer;
			v[n].front;
			
			case
			{ (relativePos > upperEdge) && flag }
			{
				flag = false;
				{ swap.(curKey, 1) }.defer
			}
			{ (relativePos < lowerEdge) && flag }
			{
				flag = false;
				{ swap.(curKey, -1) }.defer
			}
			{ ( ( (relativePos < upperEdge) && (relativePos > lowerEdge)) ) && flag.not }
			{ flag = true };
		}
	}
};
~mouseDrag.(~order, v, ~size, ~spacing);
)

Nice it works :slight_smile: !

Propagating the action isn’t as difficult as I thought. StaticText does it by default, and you only have to set the actions of a view to false to pass it back to the parent (i.e. view.mouseDownAction_(false)).

Here’s how I adapted your example to my approach :

(
var win = Window("", Rect(500, 500, 300, 150));

var myCustomFunction = {
	"Views order modified.".postln;
	"".postln;
};

var myButtonFunction = { |index, buttonState|
	order[index].post;
	if(buttonState == 0)
	{ " is OFF".postln; }
	{ " is ON".postln; };
	"".postln;
};

var selectedView = -1;
var previousView = -1;
var margin = 5;
var mouseOffset = 0;
var mousePos = 0;

var dragView = UserView(
).background_(Color.grey);

var order = [\In, \Delay, \Ring, \Reverb, \Out];
var views;

var reArrangeViews = {
	var caseWidth = (dragView.bounds.width -
		(margin * (views.size + 1))) / views.size;

	views.do({ |view, index|

		if(selectedView != index) {
			view.moveTo(
				margin + ((caseWidth + margin) * index),
				margin);

			view.fixedSize_(
				Point(
					caseWidth,
					dragView.bounds.height - (margin * 2)));
		} {
			view.moveTo(
				mousePos - mouseOffset,
				margin);

			view.fixedSize_(
				Point(
					caseWidth,
					dragView.bounds.height - (margin * 2)));
		};
	});
};

dragView.onResize_({ reArrangeViews.value; });

dragView.mouseDownAction_({ |view, x|
	var caseWidth = (view.bounds.width -
		(margin * (views.size + 1)) / views.size);

	selectedView = x.linlin(
		0, view.bounds.width,
		0, views.size).asInteger;
	previousView = selectedView;

	mouseOffset = x%(caseWidth + margin);
	mouseOffset = mouseOffset - margin;
	mousePos = x;
});

dragView.mouseMoveAction_({ |view, x|
	if(selectedView != -1) {
		var hoveredView = x.linlin(
			0, view.bounds.width,
			0, views.size).asInteger;

		if(hoveredView == views.size)
		{ hoveredView = hoveredView - 1 };

		if(hoveredView != selectedView) {
			var viewToMove = views[hoveredView];
			views[hoveredView] = views[selectedView];
			views[selectedView] = viewToMove;
			selectedView = hoveredView;
			view.refresh;
		};

		mousePos = x;

		reArrangeViews.value;
	};
});

dragView.mouseUpAction_({ |view, x|
	if(previousView != selectedView) {
		myCustomFunction.value;
	};
	selectedView = -1;
	reArrangeViews.value;
});

views = Array.fill(5,
	{ |index|
		var button = Button();

		var view = UserView(dragView)
		.layout_(
			VLayout(

				StaticText()
				.string_(order[index])
				.stringColor_(Color.white)
				.align_(\center),

				button
				.states_([[\OFF], [\ON, \, Color.green]])
				.action_({ myButtonFunction.value(index, button.value) })

			).spacing_(5).margins_(5)
		).background_(Color.rand)
		.mouseDownAction_(false)
		.mouseUpAction_(false)
		.mouseMoveAction_(false);

		view
});

win.layout_(
	VLayout(
		dragView
	).margins_(0);
);

win.front;

CmdPeriod.doOnce({ win.close });
)

There was a problem with my previous code: if the mouse dragging is fast, the view and the order might not properly update. This is due to elements ‘bubbling’ from one spot to the next, ie. swapping places with its neighbor (left or right). In the adapted code below I use a different approach: positions and order of all elements are calculated on every mouse drag, so even if some spots where missed due to rapid mouse movement, the next update will fix this. The resulting code is both shorter and more concise - don’t you just love when that happens:)

(
~size = 100;
~spacing = 10;
~order = [\In, \Delay, \Ring, \Reverb, \Out];
~createView = {|n|
	var e = (
		name: StaticText().string_(n).stringColor_(Color.white).align_(\center),
		but: Button().states_([[\OFF], [\ON, \, Color.green]]).mouseMoveAction_{ true } // prevents propagation to parrent view
		.fixedHeight_(20).value_(2.rand)
	);
	View()
	.layout_(VLayout(e[\name], e[\but])
	.spacing_(5).margins_(5)).background_(Color.rand).fixedSize_((~size - ~spacing)@(~size - ~spacing))
	// for real life usage you would output the event 'e' from the function and have access to both 
	// the parrent view and the subviews: e[\view], e[\name], e[\but]
};
v = ();
~order.do{|n|v.add(n -> ~createView.(n))};
~layout = HLayout(*~order.collect{|n|v[n] }).spacing_(~spacing).margins_(~spacing);
w = View().layout_(~layout).front.alwaysOnTop_(true);

~mouseDrag = {|order, v, size = 100, spacing = 10|
	var leftInit, offset, curKey, oldI, newI;
	order.do{|n|
		v[n].mouseDownAction_{|view, x, y|
			leftInit = v[n].bounds.left;
			offset = x;
			curKey = n;
		};

		v[n].mouseUpAction_{
			v[curKey].moveTo(order.indexOfEqual(curKey) * size + spacing, spacing)
		};

		v[n].mouseMoveAction_{|view, x, y|
			var pos = v[n].bounds.left - leftInit;
			var relativePos = pos%size;

			{ v[n].moveTo((v[n].bounds.left + x - offset).max(0).min(order.size - 1 * size + (2 * spacing)), spacing) }.defer;
			v[n].front;

			newI = (v[n].bounds.left / size).round.asInteger;
			oldI = order.indexOfEqual(n);
			order.move(oldI, newI);
			order.debug(\order);
			order.do {|name, i|
				if (i != newI)
				{ { v[name].moveTo(spacing + (i * size), spacing ) }.defer }
				// you could add logic to only call .moveTo if the calculated new bounds are different fromt the current bounds
			};
		}
	}
};
~mouseDrag.(~order, v, ~size, ~spacing);
)
1 Like