Bug 178117 - better support for truncated text in org.eclipse.draw2d.text.TextFlow
Summary: better support for truncated text in org.eclipse.draw2d.text.TextFlow
Status: RESOLVED FIXED
Alias: None
Product: GEF
Classification: Tools
Component: GEF-Legacy Draw2d (show other bugs)
Version: unspecified   Edit
Hardware: Other All
: P3 normal (vote)
Target Milestone: 3.4.0 (Ganymede) M2   Edit
Assignee: Cherie Revells CLA
QA Contact:
URL:
Whiteboard:
Keywords: contributed
Depends on:
Blocks:
 
Reported: 2007-03-19 14:38 EDT by Carsten Pfeiffer CLA
Modified: 2014-05-22 04:06 EDT (History)
3 users (show)

See Also:


Attachments
Cherie's fix (42.46 KB, patch)
2007-07-25 09:50 EDT, Cherie Revells CLA
no flags Details | Diff
Correct a small error in FlowPage (42.52 KB, patch)
2007-07-25 10:31 EDT, Cherie Revells CLA
no flags Details | Diff
Patch to expose API. (5.52 KB, patch)
2007-09-04 16:02 EDT, Cherie Revells CLA
no flags Details | Diff
final patch (1.07 KB, patch)
2007-09-19 11:20 EDT, Cherie Revells CLA
ahunter.eclipse: iplog+
Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Carsten Pfeiffer CLA 2007-03-19 14:38:23 EDT
Build ID: M20070212-1330

More information:
TextFlow visualizes a truncated TextFragmentBox by appending "..." to it. This happens on a line-by-line basis -- whenever a line does not fit entirely into the bounding box, the ellipsis is drawn.

It does not visualize truncated text in the vertical direction, though. E.g. if the last line of text is clipped by the bounding box, TextFlow happily paints the upper lines as is, without a hint that the last line is omitted.

I propose adding the "..." also if one or more lines are left out.

If you agree with me that this would be useful, I would attach a patch to TextFlow.paintFigure(), that basically checks if the "next" line would be visible before painting the "current" line. If it wouldn't be visible, then the "..."  will be drawn for the current line.
Comment 1 Randy Hudson CLA 2007-03-19 15:57:53 EDT
The Block's FlowContext would have to indicate how much vertical space it is allowed to have. Once you know that, TextFlow could decide to just stop generating fragments and truncate the last one before it exceeds the available height.

Determining if a fragment would be visible to the user is too expensive and has a vague definition.
Comment 2 Carsten Pfeiffer CLA 2007-03-19 18:30:22 EDT
(In reply to comment #1)
> Determining if a fragment would be visible to the user is too expensive and has
> a vague definition.

In my case it was sufficient to use the clipping area of the Graphics object, but I agree that giving that information to the FlowContext would be better.
Comment 3 Cherie Revells CLA 2007-07-25 09:50:17 EDT
Created attachment 74560 [details]
Cherie's fix

I have attached a patch with my fix.  The patch includes duplicate Draw2D examples to test this.  I don't plan on committing these, but will create one example to test this.

How it works:
After the FlowPage finishes the layout() it checks if the size of its blockbox is greater than the recommended size it has been given and if so sets the point at which to truncate (leaving enough space for the truncation decoration).  When each TextFlow paints its fragments, it does not paint a line of text if the text will not completely fix above the truncation point.  When the FlowPage paints, it paints the truncation decoration (an arrow for now) if required.

FlowFigure
• Added a shouldTruncateVertically () method that clients can set to get the truncation behavior.
• Added a new method getTruncationPoint() which returns the point where this figure is to be truncated.  This method returns null if there is to be no truncation.  The truncation point is set in the postValidate() method after the FlowPage has layed out.
• Paints a little triangle if it is truncated (i.e. the preferred size is bigger than the actual size).  This triangle will most likely change. I need to ask the icon creation people for some help.

TextFlow
• Modified the paint method so that any fragments that extend into the area beyond the truncation point will not be painted.

FlowPage and BlockFlowLayout were also modified so that a layout will occur if the height of the figure changes.  Previously, a layout only occurred if the width changed.  Although I have decided to go with the approach of not painting text below the truncation point instead of not laying out figures below the truncation point, it is possible that a client could have a scenario where the layout is affected which is why I think the change in height should trigger a re-layout.

I had a hard time understanding what the pageWidth and recommendedWidth are (especially since the method called getPageWidth() returns the recommendedWidth.  The attached patch has changed these to be pageSize and recommendedSize already, but I would also like to rename them.  
• I would like to rename pageSize to preferredSize.  It is only represented internally and represents the preferred full-figure size.
• I would like to rename the method getPageSize() (which gets the recommendedSize) to be called getRecommendedSize().
Comment 4 Cherie Revells CLA 2007-07-25 09:51:25 EDT
Randy, can you review this when you have time?
Comment 5 Cherie Revells CLA 2007-07-25 10:31:11 EDT
Created attachment 74568 [details]
Correct a small error in FlowPage

I noticed that when I paste text in the cell editor in a truncated label on the logic diagram that the truncation decoration doesn't show up afterwards.  I need to trigger the postValidate() call somehow after the text editor appears since it using the same FlowPage (untruncated) in the text cell editor. I'll have to think about this.
Comment 6 Cherie Revells CLA 2007-08-14 09:41:27 EDT
Hi Randy.  There are probably still a couple bugs with this one, but I would like to get your opinion on the general approach before spending more time on it.  Thanks!
Comment 7 Randy Hudson CLA 2007-08-27 12:17:07 EDT
Sorry for the delay. Here are some comments...

(I haven't applied the patch yet because I haven't moved my projects to point to the new CVS root)

It seems like FlowPage is going to paint a triangle at the bottom if its block is larger than its client area. This sounds like a easy approach that communicates that some information is not visible. It seems like this decoration would even work without all of the other changes. Also, it could be done in a subclass without changing FlowPage.

The only behavior that I see as a result of the remaining changes is that text in a partially visible line doesn't paint. I think this step could be avoided. You could let the text be painted and then paint over it with the background color before painting the decoration (kind of like the palette's scrollbar buttons). Or you could extend TextFlow so that it clips the graphics before calling super.paint(g).

IMO, the overall behavior seems too specific to belong in the base text package and doesn't handle some text constructs, like Inline and TextFlow figures with borders, blocks which paint, or blocks with relative coordinate systems. Handling these cases wouldn't be worth the effort when a domain-specific solution like painting a simple decoration on a [custom] FlowPage exists.
Comment 8 Cherie Revells CLA 2007-09-04 16:02:40 EDT
Created attachment 77662 [details]
Patch to expose API.

Thanks for your feedback, Randy.  It seems to be rather difficult to create a generic solution for this, so I guess I will add this behavior in GMF to our particular label figure which contains one TextFlow.  In this case, I would rather extend the TextFlow paintFigure() method to avoid having the lines half painted.  In order to do this, I need access to some methods that are called in the TextFlow paintFigure() method.  I have attached a patch that shows methods I would like to make API.
Comment 9 Randy Hudson CLA 2007-09-05 09:57:20 EDT
The way TextFlow paints each substring currently is:
paintText(g, draw,
	frag.getX(),
	frag.getBaseline() - getAscent(),
	frag.getBidiLevel());

getAscent() could be replaced with frag.getAscent()
also:
bottom = frag.getBaseline() + frag.getDescent()

These existing methods are already API. Is it necessary to determine truncation using the line root instead of the text fragments?

Making getLargestSubstringConfinedTo public seems safe.
Comment 10 Cherie Revells CLA 2007-09-05 11:23:56 EDT
I would like to stop painting the text when I hit a text fragment that won't be completely shown and I would like to change the text in the previous fragment to add "...".  In order to do this, I need to override the paintFigure() method which means essentially copying the existing code there which accesses the private methods because I can't call super.paintFigure() method if I want to truncate the text before painting.

I was also using getLineRoot().getVisibleBottom() to see if the text will be cut off.
Comment 11 Carsten Pfeiffer CLA 2007-09-05 11:32:38 EDT
(In reply to comment #10)
FWIW, we're in exactly the same situation, with a TextFlow subclass and a copy of paintFigure(), calling all those private methods through reflection. An improvement here would be very welcome.
Comment 12 Randy Hudson CLA 2007-09-05 16:36:44 EDT
> I was also using getLineRoot().getVisibleBottom() to see if the text will be
> cut off.

Unless you have a border, it should be the case that:
frag.getLineRoot().getVisibleBottom() == frag.getBaseline() + frag.getDescent();
Comment 13 Randy Hudson CLA 2007-09-05 17:02:01 EDT
> (In reply to comment #10)
> FWIW, we're in exactly the same situation, with a TextFlow subclass and a copy
> of paintFigure(), calling all those private methods through reflection. An
> improvement here would be very welcome.

Can you describe what you're trying to do in the subclass?
Comment 14 Carsten Pfeiffer CLA 2007-09-05 17:18:24 EDT
(In reply to comment #13)
> Can you describe what you're trying to do in the subclass?

We're adding an ellipsis to the last visible line of a vertically truncated multiline textflow. We also try to merge as much of the first invisible line into the last visible line before adding the ellipsis.
Comment 15 Cherie Revells CLA 2007-09-06 09:49:30 EDT
The code I am adding to the subclass is not really the problem, it is the fact that I have to copy the code from TextFlow.paintFigure().  Here is my subclass code.  The section in surrounded in ///////// is the part I added.

    protected void paintFigure(Graphics g) {
        TextFragmentBox frag;
        g.getClip(Rectangle.SINGLETON);
        int yStart = Rectangle.SINGLETON.y;
        int yEnd = Rectangle.SINGLETON.bottom();
        int maxHeight = getParent().getClientArea().bottom();

        for (int i = 0; i < getFragments().size(); i++) {
            frag = (TextFragmentBox) getFragments().get(i);
            if (frag.offset == -1)
                continue;
            // Loop until first visible fragment
            if (yStart > frag.getLineRoot().getVisibleBottom() + 1)// The + 1
                                                                    // is for
                                                                    // disabled
                                                                    // text
                continue;
            // Break loop at first non-visible fragment
            if (yEnd < frag.getLineRoot().getVisibleTop())
                break;

            String draw = getBidiSubstring(frag, i);

            /////////////////////////////////////////////
            // If the next fragment will not be completely visible, then
            // truncate this fragment.
            boolean truncate = frag.isTruncated();
            if (i + 1 < getFragments().size()
                && maxHeight < ((TextFragmentBox) getFragments().get(i + 1))
                    .getLineRoot().getVisibleBottom()){

                draw = truncateText(draw);
                truncate = true;

                // increment the counter so no further fragments will be processed
                i = getFragments().size();
            }

            if (truncate)
                draw += ELLIPSIS;
            /////////////////////////////////////////////

            if (!isEnabled()) {
                Color cachedfgColor = g.getForegroundColor();
                g.setForegroundColor(ColorConstants.buttonLightest);
                paintText(g, draw, frag.getX() + 1, frag.getBaseline()
                    - getAscent() + 1, frag.getBidiLevel());
                g.setForegroundColor(ColorConstants.buttonDarker);
                paintText(g, draw, frag.getX(), frag.getBaseline()
                    - getAscent(), frag.getBidiLevel());
                g.setForegroundColor(cachedfgColor);
            } else {
                paintText(g, draw, frag.getX(), frag.getBaseline()
                    - getAscent(), frag.getBidiLevel());
            }
        }

I tried getting rid of the calls to frag.getLineRoot().getVisibleBottom() and frag.getLineRoot().getVisibleTop() and using the fragment's baseline, ascent, and descent and that worked fine.  So, now all I need to expose is:
- change getBidiSubstring(TextFragmentBox box, int index) to protected
- change paintText(Graphics g, String draw, int x, int y, int bidiLevel) to protected
- change static variable ELLIPSIS to public (I want to use this elsewhere as well).
Comment 16 Cherie Revells CLA 2007-09-19 11:20:45 EDT
Created attachment 78760 [details]
final patch

Updated patch code reviewed by Anthony.
Comment 17 Anthony Hunter CLA 2007-09-19 14:13:22 EDT
Committed to HEAD. Part of the fix was commited as part of Bug 194278