Wanted: TextEditor::scrollCaretTo(int x, int y)


#1

I need to be able to scroll a TextEditor so that the caret gets vertically centered.

The TextEditor has the methods necessary to accomplish this, but they are private.

I need it for navigating search result lists etc, so that when I walk up and down a ListBox, I immediately get to see the occurrence in the source, placed so I can see its surroundings. But I think the ability to adjust the scrolling would be useful to any app that does text searching.

(To illustrate it, you might want to open juce_amalgamated.h with Visual Studio and search for a common word such as const, and take note of how the scrolling behaves. I’m not saying this behaviour should be cloned by TextEditor but I think the primitives needed to implement something similar should be protected or public, not private.)

Perhaps TextEditor::scrollCaretTo could be defined to accept float parameters, with the same meaning as in the StretchableLayoutManager, so thatscrollCaretTo(-1, -0.5);would scroll as far to the left as possible while centering vertically.

Jules, if you suggest an interface, I could write and test and you could jucify or rewrite if you think it’s worth it.


#2

Ok - maybe a good method would take two proportional positions that are the preferred position of the caret?


#3

Here is a shot - introducing 4 methods.

In the public section of class TextEditor:

[code] /** Attempts to scroll the caret to a given position, in pixels relative to the top
left corner of the visible area.

    The position will be adjusted to ensure the caret is visible.
*/
void scrollCaretToPosition (const int x, const int y) throw();

/** Attempts to scroll the caret to a given position.
    The position is given as a fraction of the visible area.

    Both x and y should be between 0.0 and 1.0, inclusive.

    The default values will center the caret vertically, which is useful
    for text searching.
*/
void scrollCaretToProportionalPosition (const float xProportion = 1.0, const float yProportion = 0.5) throw();

/** Get the graphical position of the caret.

    When useScreenCoordinates = false, the rectangle is relative to the TextEditor.
    When true, desktop coordinates are used.

    Use this with search/replace dialogs to prevent them from hiding the caret.

    The width of the rectangle is always 0. The height depends on the
    current font.
*/
const Rectangle getCaretRectangle (const bool useDesktopCoordinates = true) throw();

/** Get the caret position as a fraction of the text display area.

    The x and y coordinates will be between 0.0 and 1.0, inclusive.
*/
const Point getCaretProportionalPosition () throw();

[/code]

And the implementation:

[code]void TextEditor::scrollCaretToPosition (const int x, const int y) throw()
{
cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn’t set this value)

getCharPosition (caretPosition,
                 cursorX, cursorY,
                 cursorHeight);

int vpX = roundFloatToInt (cursorX) - x;
int vpY = roundFloatToInt (cursorY) - y;

if (x < jmax (1, proportionOfWidth (0.05f)))
{
    vpX += x - proportionOfWidth (0.2f);
}
else if (x > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10)))
{
    vpX += x + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth();
}

vpX = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), vpX);
vpY = jlimit (0, jmax (0, textHolder->getHeight() - viewport->getMaximumVisibleHeight()), vpY);

if (! isMultiLine())
{
    vpY = (getHeight() - textHolder->getHeight() - topIndent) / -2;
}
else
{
    const int curH = roundFloatToInt (cursorHeight);

    if (y < 0)
    {
        vpY = jmax (0, y + vpY);
    }
    else if (y > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - curH))
    {
        vpY += y + 2 + curH + topIndent - viewport->getMaximumVisibleHeight();
    }
}

viewport->setViewPosition (vpX, vpY);

}

void TextEditor::scrollCaretToProportionalPosition (const float xRatio, const float yRatio) throw()
{
int width = viewport->getMaximumVisibleWidth();
int height = viewport->getMaximumVisibleHeight() - roundFloatToInt(currentFont.getHeight());
int x = jlimit(0, width, roundFloatToInt(width * xRatio));
int y = jlimit(0, width, roundFloatToInt(height * yRatio));
scrollCaretToPosition (x, y);
}

const Rectangle TextEditor::getCaretRectangle (const bool useScreenCoordinates) throw()
{
cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn’t set this value)

getCharPosition (caretPosition,
                 cursorX, cursorY,
                 cursorHeight);

int x = roundFloatToInt(cursorX) - viewport->getX();
int y = roundFloatToInt(cursorY) - viewport->getY();
int height = roundFloatToInt(cursorHeight);
if (useScreenCoordinates)
{
    x += getScreenX();
    y += getScreenY();
}
return Rectangle(x, y, 0, height);

}

const Point TextEditor::getCaretProportionalPosition () throw()
{
const Rectangle rect = getCaretRectangle(false);
int width = jmax(1, viewport->getMaximumVisibleWidth());
int height = jmax(1, viewport->getMaximumVisibleHeight() - rect.getHeight());
float x = jlimit(0.0f, 1.0f, rect.getX() * 1.0f / width);
float y = jlimit(0.0f, 1.0f, rect.getY() * 1.0f / height);
return Point (x, y);
}[/code]
The first one - TextEditor::scrollCaretToPosition - is the heavy one, I adapted the code from TextEditor::scrollToMakeSureCursorIsVisible but I don’t fully understand the correction steps, though it works as far as I have tested it. The 2nd is the convenience wrapper I actually use; a single parameterless call centers the caret vertically.

The 3rd and 4th aren’t necessary for what I’m doing but I felt it was natural to add “getter” methods, should someone want finer control.

(The style of returning a Rectangle and a Point is, admittedly, biased by my jucePILS binding generator which doesn’t support out parameters as-is. It won’t spoil my day if you change it or decide these methods aren’t wanted.)

BTW TextEditor::scrollToMakeSureCursorIsVisible uses “Cursor” while other methods use “Caret” - I used “Caret” because it doesn’t get confused with the mouse cursor.

I hope you’ll consider them for inclusion, or, if they don’t feel right, suggest what should be changed or perhaps re-classify the private methods and fields I used as protected, so I can implement the caret scrolling in my own subclass without having to modify the juce source.


#4

Ok, thanks - there’s a few things that don’t feel right there - like the “use screen co-ordinates” flag would be cluttering up the class too much for my liking. And returning a Point object which is actually proportional would seem wrong semantically - if there was a ProportionalPoint object, it’d be fair enough, but Points are always used to define an absolute position.

But I’ll have a look through and check something in soon - thanks!


#5

Just spotted a typo in what I wrote:

void TextEditor::scrollCaretToProportionalPosition (const float xRatio, const float yRatio) throw() { int width = viewport->getMaximumVisibleWidth(); int height = viewport->getMaximumVisibleHeight() - roundFloatToInt(currentFont.getHeight()); int x = jlimit(0, width, roundFloatToInt(width * xRatio)); int y = jlimit(0, width, roundFloatToInt(height * yRatio)); /* oops height */ scrollCaretToPosition (x, y); }