BoxLayout(X_AXIS_SCROLLING) layout manager for Codename One - codenameone

Note for the readers: this question is specific for Codename One only.
My use case is to have one or more horizontally scrollable bars with Buttons.
I need a layout manager that:
puts all the Components on the X axis;
gives each Component its preferred size;
if there is enough horizontal space, it acts exactly as BoxLayout(X_AXIS_NO_GROW);
if there isn't enough horizontal space, it allows a horizontal scrolling to see all the Components;
in the last case, it adds a small arrow on the left and on the right to indicate that a scrolling is possible.
For example, the following code produces the following screenshot:
Form hi = new Form("BoxLayout X scrolling", BoxLayout.y());
Container exampleCnt = new Container();
exampleCnt.setLayout(new BoxLayout(BoxLayout.X_AXIS_NO_GROW));
for (int i=0; i<=10; i++) {
int j = i; // for lambda expression
Button button = new Button("Button " + i);
button.addActionListener(l -> {Log.p("Button " + j + " tapped");});
exampleCnt.add(button);
}
hi.add(exampleCnt);
hi.show();
Instead I need something like in the following screenshots. I suppose to horizontally scroll the buttons bar (the exampleCnt) with the finger. The app should be enough smart to don't confuse the tapping on a Button with the horizontal swiping to scroll the Buttons:

There are several approaches to doing this and they each have their advantages/disadvantages. Let's start by asking this:
Should the arrow buttons be clickable?
Should they draw as an overlay (you would see the button under them as they come into view) or should they act as separate components?
Based on your responses you can pick one of the following.
Builtin Scrolling Support
This would generate something that would render as an overlay and won't be clickable as scrolling assumes a scrollbar style UI but should work for this case too.
You might need to invoke setScrollVisible(true) on the Container.
Then override in the Container:
protected void paintScrollbarX(Graphics g) {
float scrollW = getScrollDimension().getWidth();
float block = ((float) getWidth()) / scrollW;
float offset;
if(getScrollX() + getWidth() == scrollW) {
// normalize the offset to avoid rounding errors to the bottom of the screen
offset = 1 - block;
} else {
offset = (((float) getScrollX() + getWidth()) / scrollW) - block;
}
// here paint the arrows to the left/right based on the values
}
Use a Wrapper
With a wrapper you can just use buttons to represent the arrows. You can place them either in a layered layout to create an overlay effect or in the sides to create separation e.g. this would result in separate buttons:
BorderLayout wrap = BorderLayout.centerEastWest(scrollableCnt, right, left);
This would result in overlay:
BorderLayout border = BorderLayout.centerEastWest(null, right, left);
LayeredLayout wrap = LayeredLayout(scrollableCnt, border);
To make this work we'd want to hide/show the components based on scroll. isScrollableX() will return false for a non-scrollable component so:
if(scrollableCnt.isScrollableX()) {
left.setVisible(false);
scrollableCnt.addScrollListener((scrollX, scrollY, oldscrollX, oldscrollY) -> {
left.setVisible(scrollX > 0);
// not 100% sure about this line but it should be something close to this
right.setVisible(scrollX < getScrollDimension().getWidth() - scrollableCnt.getWidth());
});
} else {
right.setVisible(false);
left.setVisible(false);
}

Related

Make grid of buttons fixed width & height, and wrap text

We have a GTK3 user interface (coded in C) in an embedded system that must stay fixed width/height in order to fit the screen.
Basically it's a fixed grid of buttons on a touchscreen, where the buttons can contain a text label or a graphical image - and the grid MUST stay fixed, all buttons the same width & height, and fitting the screen 100%.
However, on updating button text/labels with longer strings the button expands to fit which is not what we want at all.
I can't post all the code but I believe this is the relevant button creation code that would affect what we're trying to do;
// Number buttons 1..n
buttonId = n;
bid = buttonId - 1; // Because zero-indexing, natch
dbp = &_context->buttons[bid]; // Data about button
// Create new button, attach to global buttons
dbp->btn = gtk_button_new();
// Button name != Button CSS#ID
snprintf(value, SHORT_STR, "BTN_%02d", buttonId );
gtk_widget_set_name(dbp->btn, value);
gtk_button_set_relief(GTK_BUTTON(dbp->btn), GTK_RELIEF_NORMAL/*GTK_RELIEF_NONE*/);
// Set text label
strncpy(dbp->text, value, MAX_BUTTON_TEXT);
gtk_button_set_label (GTK_BUTTON(dbp->btn), dbp->text);
// Attempt to make labels wrap - does nothing
label = gtk_bin_get_child(GTK_BIN(dbp->btn));
gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
//gtk_label_set_max_width_chars(GTK_LABEL(label), 0);
// Create new blank (&transprent) image
dbp->pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, _context->innerButton.width, _conte$
gdk_pixbuf_fill(dbp->pixbuf, 0x00000000);
// Image holds pixbuf
dbp->image = (GtkImage*)gtk_image_new_from_pixbuf(dbp->pixbuf);
dbp->image_modified = 0;
// Attach image to button
gtk_button_set_image(GTK_BUTTON(dbp->btn), GTK_WIDGET(dbp->image));
// Adds an individual CSS provider per button to allow style changes
GtkStyleContext *context = gtk_widget_get_style_context(dbp->btn);
dbp->bp = gtk_css_provider_new();
// Set default colour from table of default colours
dbp->bg = get_default_colour(bid);
dbp->fg = get_label_colour(dbp->bg);
BuildButtonCSS("btnid_", buttonId, dbp->bg, dbp->fg, tstr, NULL);
// Convert CSS to provider
gtk_css_provider_load_from_data(dbp->bp, tstr, -1, NULL);
// Add provider to button
gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER(dbp->bp), GTK_STYLE_PROVIDER_PRIO$
// Give the button the CSS style/class corresponding to the one we just created for it
snprintf(value, SHORT_STR, "btnid_%02d", buttonId);
gtk_style_context_add_class(context, value);
g_object_unref(dbp->bp);
// Discourage expansion (again, doesn't appear to work when label gets too long)
gtk_widget_set_hexpand(dbp->btn, FALSE);
gtk_widget_set_vexpand(dbp->btn, FALSE);
// Attach button to grid
gtk_grid_attach(GTK_GRID(_grid), dbp->btn, c, r, 1, 1);
g_signal_connect(dbp->btn, "button_press_event", G_CALLBACK(button_press_callback), GINT_TO_P$
g_signal_connect(dbp->btn, "button_release_event", G_CALLBACK(button_release_callback), GINT_$
dbp->initialised = 1;
However when we update text within a button, it renders as a single (non-wrapped) line of text and causes the button to expand and totally bu&&er up the layout.
There's two problems here;
The text should wrap, the buttons have plenty of height but the text remains a single line
The buttons should be absoltuely FIXED width & height no matter what
This feels like it should be simple to do, but I'm not very familiar with GTK and all the examples out there seem to assume that you'd want everything to flex and resize itself all the time.
Edit: Tried Alexander Dmitriev's suggestion to use ellipsize - didn't seem to work although threw no errors;
// Experiment; ellipsize
label = gtk_bin_get_child(GTK_BIN(dbp->btn));
gtk_label_set_ellipsize(GTK_LABEL(label),PANGO_ELLIPSIZE_END);
gtk_label_set_max_width_chars(GTK_LABEL(label), 14);
I also tried this exmaple by creating a GtkWidget label, applying the properties to it and then adding it to the button - but it seems gtk_button_set_label will only accept a string not a label.

How to detect a 'pinch out' in a list of containers?

I want to be able to pinch two containers in a list of containers away from each other to insert a new empty container between them. Similar to how the iPhone app “Clear” inserts new tasks (see for example the very first picture on this page https://www.raywenderlich.com/22174/how-to-make-a-gesture-driven-to-do-list-app-part-33 - the small red container is inserted when the two sorounding containers are pinched away from each other). Any hints on how I can achieve this in Codename One?
Normally you would override the pinch method to implement pinch to zoom or similar calls. However, this won't work in this case as the pinch will exceed component boundaries and it wouldn't work.
The only way I can think of doing this is to override the pointerDragged(int[],int[]) method in Form and detect the pinch motion as growing to implement this. You can check out the code for pinch in Component.java as it should be a good base for this:
public void pointerDragged(int[] x, int[] y) {
if (x.length > 1) {
double currentDis = distance(x, y);
// prevent division by 0
if (pinchDistance <= 0) {
pinchDistance = currentDis;
}
double scale = currentDis / pinchDistance;
if (pinch((float)scale)) {
return;
}
}
pointerDragged(x[0], y[0]);
}
private double distance(int[] x, int[] y) {
int disx = x[0] - x[1];
int disy = y[0] - y[1];
return Math.sqrt(disx * disx + disy * disy);
}
Adding the entry is simple, just place a blank component in the place and grow its preferred size until it reaches the desired size.

Snapping a SurfaceListBox

I'm looking to create a scrolling surfacelistbox which automatically snaps into a position after a drag is finished so that the center item on the screen is centered itself in the viewport.
I've gotten the center item, but now as usual the way that WPF deals with sizes, screen positions, and offsets has me perplexed.
At the moment I've chosen to subscribe to the SurfaceScrollViewer's ManipulationCompleted event, as that seems to consistently fire after I've finished a scroll gesture (whereas the ScrollChanged event tends to fire early).
void ManipCompleted(object sender, ManipulationCompletedEventArgs e)
{
FocusTaker.Focus(); //reset focus to a dummy element
List<FrameworkElement> visibleElements = new List<FrameworkElement>();
for (int i = 0; i < List.Items.Count; i++)
{
SurfaceListBoxItem item = List.ItemContainerGenerator.ContainerFromIndex(i) as SurfaceListBoxItem;
if (ViewportHelper.IsInViewport(item) && (List.Items[i] as string != "Dummy"))
{
FrameworkElement el = item as FrameworkElement;
visibleElements.Add(el);
}
}
int centerItemIdx = visibleElements.Count / 2;
FrameworkElement centerItem = visibleElements[centerItemIdx];
double center = ss.ViewportWidth / 2;
//ss is the SurfaceScrollViewer
Point itemPosition = centerItem.TransformToAncestor(ss).Transform(new Point(0, 0));
double desiredOffset = ss.HorizontalOffset + (center - itemPosition.X);
ss.ScrollToHorizontalOffset(desiredOffset);
centerItem.Focus(); //this also doesn't seem to work, but whatever.
}
The list snaps, but where it snaps seems to be somewhat chaotic. I have a line down the center of the screen, and sometimes it looks right down the middle of the item, but other times it's off to the side or even between items. Can't quite nail it down, but it seems that the first and fourth quartile of the list work well, but the second and third are progressively more off toward the center.
Just looking for some help on how to use positioning in WPF. All of the relativity and the difference between percentage-based coordinates and 'screen-unit' coordinates has me somewhat confused at this point.
After a lot of trial and error I ended up with this:
void ManipCompleted(object sender, ManipulationCompletedEventArgs e)
{
FocusTaker.Focus(); //reset focus
List<FrameworkElement> visibleElements = new List<FrameworkElement>();
for (int i = 0; i < List.Items.Count; i++)
{
SurfaceListBoxItem item = List.ItemContainerGenerator.ContainerFromIndex(i) as SurfaceListBoxItem;
if (ViewportHelper.IsInViewport(item))
{
FrameworkElement el = item as FrameworkElement;
visibleElements.Add(el);
}
}
Window window = Window.GetWindow(this);
double center = ss.ViewportWidth / 2;
double closestCenterOffset = double.MaxValue;
FrameworkElement centerItem = visibleElements[0];
foreach (FrameworkElement el in visibleElements)
{
double centerOffset = Math.Abs(el.TransformToAncestor(window).Transform(new Point(0, 0)).X + (el.ActualWidth / 2) - center);
if (centerOffset < closestCenterOffset)
{
closestCenterOffset = centerOffset;
centerItem = el;
}
}
Point itemPosition = centerItem.TransformToAncestor(window).Transform(new Point(0, 0));
double desiredOffset = ss.HorizontalOffset - (center - itemPosition.X) + (centerItem.ActualWidth / 2);
ss.ScrollToHorizontalOffset(desiredOffset);
centerItem.Focus();
}
This block of code effectively determines which visible list element is overlapping the center line of the list and snaps that element to the exact center position. The snapping is a little abrupt, so I'll have to look into some kind of animation, but otherwise I'm fairly happy with it! I'll probably use something from here for animations: http://blogs.msdn.com/b/delay/archive/2009/08/04/scrolling-so-smooth-like-the-butter-on-a-muffin-how-to-animate-the-horizontal-verticaloffset-properties-of-a-scrollviewer.aspx
Edit: Well that didn't take long. I expanded the ScrollViewerOffsetMediator to include HorizontalOffset and then simply created the animation as suggested in the above post. Works like a charm. Hope this helps someone eventually.
Edit2: Here's the full code for SnapList:
SnapList.xaml
SnapList.xaml.cs
Note that I got pretty lazy as this project went on an hard-coded some of it. Some discretion will be needed to determine what you do and don't want from this code. Still, I think this should work pretty well as a starting point for anyone who wants this functionality.
The code has also changed from what I pasted above; I found that using Windows.GetWindow gave bad results when the list was housed in a control that could move. I made it so you can assign a control for your movement to be relative to (recommended that be the control just above your list in the hierarchy). I think a few other things changed as well; I've added a lot of customization options including being able to define a custom focal point for the list.

Custom panel layout doesn't work as expected when animating (WPF)

I've got a custom (and getting complex) TabControl. It's a gathering of many sources, plus my own wanted features. In it is a custom Panel to show the headers of the TabControl. Its features are to compress the size of the TabItems until they reached their minimum, and then activates scrolling features (in the Panel, again). There is also another custom panel to hold a single button, that renders on the right of the TabItems (it's a "new tab" button).
It all works great, until I try to animate the scrolling.
Here are some relevant snippets :
In the CustomTabPanel (C#, overriding Panel and implementing IScrollInfo):
private readonly TranslateTransform _translateTransform = new TranslateTransform();
public void LineLeft()
{
FirstVisibleIndex++;
var offset = HorizontalOffset + _childRects[0].Width;
if (offset < 0 || _viewPort.Width >= _extent.Width)
offset = 0;
else
{
if (offset + _viewPort.Width > _extent.Width)
offset = _extent.Width - _viewPort.Width;
}
_offset.X = offset;
if (_scrollOwner != null)
_scrollOwner.InvalidateScrollInfo();
//Animate the new offset
var aScrollAnimation = new DoubleAnimation(_translateTransform.X, -offset,
new Duration(this.AnimationTimeSpan), FillBehavior.HoldEnd) { AccelerationRatio = 0.5, DecelerationRatio = 0.5 };
aScrollAnimation.Completed += ScrollAnimationCompleted;
_translateTransform.BeginAnimation(TranslateTransform.XProperty, aScrollAnimation , HandoffBehavior.SnapshotAndReplace);
//End of animation
// These lines are the only ones needed if we remove the animation
//_translateTransform.X = -offset;
//InvalidateMeasure();
}
void ScrollAnimationCompleted(object sender, EventArgs e)
{
InvalidateMeasure();
}
the _translateTransform is initialized in the constructor :
base.RenderTransform = _translateTransform;
Again, everything is fine if I remove the animation part and just replace it with the commented out lines at the end.
I must also point out that the problem is NOT with the animation itself. That part works out well. The problem is about when I remove some tab items : all the layout then screws up. The TranslateTransformation seems to hold on some wrong value, or something.
Thanks in advance.
Well. As it's often the case, I kept working on the thing, and... answered myself.
Could still be useful for other people, so here was the catch. In the line :
var aScrollAnimation = new DoubleAnimation(_translateTransform.X, -offset, new Duration(this.AnimationTimeSpan), FillBehavior.HoldEnd)
{ AccelerationRatio = 0.5, DecelerationRatio = 0.5 };
the FillBehavior should have been FillBehavior.Stop.
As easy as that!

Selecting an object on a WPF Canvas?

I have a WPF Canvas with some Ellipse objects on it (displayed as circles). Each circle is from a collection class instance which is actually a custom hole pattern class. Each pattern has a certain number of circles, and each circle then gets added to the canvas using an iteration over the collection using the code below.
So, the canvas is populated with a bunch of circles and each circle belongs to a certain pattern instance. You can see a screenshot here: http://twitpic.com/1f2ci/full
Now I want to add the ability to click on a circle on the canvas, and be able to determine the collection it belongs to, so that I can then do some more work on the selected pattern to which that circle belongs.
public void DrawHoles()
{
// Iterate over each HolePattern in the HolePatterns collection...
foreach (HolePattern HolePattern in HolePatterns)
{
// Now iterate over each Hole in the HoleList of the current HolePattern...
// This code adds the HoleEntity, HoleDecorator, and HoleLabel to the canvas
foreach (Hole Hole in HolePattern.HoleList)
{
Hole.CanvasX = SketchX0 + (Hole.AbsX * _ZoomScale);
Hole.CanvasY = SketchY0 - (Hole.AbsY * _ZoomScale);
canvas1.Children.Add(Hole.HoleEntity);
}
}
}
All FrameworkElements have a Tag property which is of type object that can be used to hold arbitrary information. You could assign the HolePattern to the Tag property and easily use that later to get the associated collection.
i.e.:
...
Hole.HoleEntity.Tag = HolePattern as object;
canvas1.Children.Add(Hole.HoleEntity);
later on in the click event:
event(object sender,....)
{
Ellipse e = sender as Ellipse;
HolePattern hp = e.Tag as HolePattern;
...
}
So you probably already read my reply where I said I had it working. And it does work perfectly, (except that it requires great precision with the mouse), but I want to ask this: is it really smart to add an event handler to EVERY ellipse that gets added to a canvas? Now I don't know what kind of memory bog that could be, or maybe it is a piece of cake for WPF and Windows to handle.
In a practical case, I guess there would be not more that 30-50 holes even on a screen that had multiple patterns, but still; FIFTY event handlers? It just seems scary. And actually, each "Hole" is visually represented by two concentric circles and a text label (see the screenshow here: http://twitpic.com/1f2ci/full ), and I know the user would expect to be able to click on any one of those elements to select a hole. That means an event handler on 3 elements for every hole. Now we could be talking about 100 or more event handlers.
It seems like there should be a solution where you could have just one event handler on the Canvas and read the element reference under the mouse, then work off of that to get the .Tag property of that elment, and so on.
I thought I'd post my final and more refined solution in case it helps anyone else.
void canvas1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
int ClickMargin = 2;// Adjust here as desired. Span is in both directions of selected point.
var ClickMarginPointList = new Collection<Point>();
Point ClickedPoint = e.GetPosition(canvas1);
Point ClickMarginPoint=new Point();
for (int x = -1 * ClickMargin; x <= ClickMargin; x++)
{
for (int y = -1 * ClickMargin; y <= ClickMargin; y++)
{
ClickMarginPoint.X = ClickedPoint.X + x;
ClickMarginPoint.Y = ClickedPoint.Y + y;
ClickMarginPointList.Add(ClickMarginPoint);
}
}
foreach (Point p in ClickMarginPointList)
{
HitTestResult SelectedCanvasItem = System.Windows.Media.VisualTreeHelper.HitTest(canvas1, p);
if (SelectedCanvasItem.VisualHit.GetType().BaseType == typeof(Shape))
{
var SelectedShapeTag = SelectedCanvasItem.VisualHit.GetValue(Shape.TagProperty);
if (SelectedShapeTag!=null && SelectedShapeTag.GetType().BaseType == typeof(Hole))
{
Hole SelectedHole = (Hole)SelectedShapeTag;
SetActivePattern(SelectedHole.ParentPattern);
SelectedHole.ParentPattern.CurrentHole = SelectedHole;
return; //Get out, we're done.
}
}
}
}

Resources