2

There are thousand articles how to use LineBreakMeasurer to draw multi-line text but there is none about drawing multi-line text taking into account also \n(when you want to force a new line at a specific position in text and not only when the right - or left - margin ends).

The secret seems to lie in BreakIterator, but I couldn't find an implementation which handles \n.

adrian.tarau
  • 3,124
  • 2
  • 26
  • 29

5 Answers5

4

Instead of LineBreakMeasurer's (LBM's) nextLayout(float) method, use the overloaded LBM.nextLayout(float, int, boolean) method. This allows you to limit the text that LBM will include in the returned TextLayout. In your case, you'll instruct it not to go beyond the next newline.

This code snippet should give you the idea. First use LBM.nextOffset to "peek" which character index would be the end of the next layout. Then iterate over your string content up to that offset to see if you find any newline characters. If you do, then use that found limit as the second argument to nextLayout(float, int, boolean) which will tell LBM not to exceed the newline:

int next = lineMeasurer.nextOffset(formatWidth);
int limit = next;
if (limit < totalLength) {
   for (int i = lineMeasurer.getPosition(); i < next; ++i) {
      char c = string.charAt(i);
      if (c == '\n') {
         limit = i;
         break;
      }
   }
}

TextLayout layout = lineMeasurer.nextLayout(formatWidth, limit, false);

References

http://java.sun.com/developer/onlineTraining/Media/2DText/style.html#layout http://java.sun.com/developer/onlineTraining/Media/2DText/Code/LineBreakSample.java

aaron
  • 713
  • 9
  • 9
3

I find that this code works well for the newline issue. I used atdixon as a template to get this.

while (measurer.getPosition() < paragraph.getEndIndex()) {
   next = measurer.nextOffset(wrappingWidth);
   limit = next;
   charat = tested.indexOf('\n',measurer.getPosition()+1);
   if(next > (charat - measurer.getPosition()) && charat != -1){
      limit = charat - measurer.getPosition();
   }
   layout = measurer.nextLayout(wrappingWidth, measurer.getPosition()+limit, false);
   // Do the rest of your layout and pen work.
}
Brett
  • 51
  • 3
2

Tokenize the text first, then just apply the LineBreakMeasureCode to each token.

camickr
  • 321,443
  • 19
  • 166
  • 288
1

Aaron's code doesn't always work right so here's some tweaked code that is working for me:

int next = measurer.nextOffset(width);
int limit = next;
if (limit <= text.length()) {
  for (int i = measurer.getPosition(); i < next; ++i) {
    char c = text.charAt(i);
    if (c == '\n') {
      limit = i + 1;
      break;
    }
  }
}
TextLayout textLayout = measurer.nextLayout(width, limit, false);

If you need text from an AttributedString you can just do this beforehand

AttributedCharacterIterator iterator = attributedString.getIterator();
StringBuilder stringBuilder = new StringBuilder(iterator.getEndIndex());
while (iterator.getIndex() < iterator.getEndIndex()) {
  stringBuilder.append(iterator.current());
  iterator.next();
}
String text = stringBuilder.toString();
Nathan Brown
  • 1,193
  • 1
  • 11
  • 19
0

Even though the topic is very old, I’ve had this issue myself and had to solve it. After considerable amount of investigation, I’ve come up with solution that would work in single class that wraps ’JTextArea’.

The code is in Kotlin, as that is what I’m using. Hopefully it’ll still be useful.

package [your package name]

import java.awt.Font
import java.awt.FontMetrics
import java.awt.Insets
import java.awt.font.LineBreakMeasurer
import java.awt.font.TextAttribute
import java.text.AttributedString
import java.text.BreakIterator
import javax.swing.JTextArea

class TextAreaLineCounter(
    private val textArea: JTextArea
) {

    private val font: Font
        get() = textArea.font
    private val fontMetrics: FontMetrics
        get() = textArea.getFontMetrics(font)
    private val insets: Insets
        get() = textArea.insets
    private val formatWidth: Float
        get() = (textArea.width - insets.left - insets.right).toFloat()

    fun countLines(): Int {
        return countLinesInParagraphs(
            textRaw = textArea.text,
            font = font,
            fontMetrics = fontMetrics,
            formatWidth = formatWidth
        )
    }

    private fun countLinesInParagraphs(
        textRaw: String,
        font: Font,
        fontMetrics: FontMetrics,
        formatWidth: Float
    ): Int {
        val paragraphs: List<String> = textRaw.split("\n")
        val lineCount = paragraphs.fold(0) { acc: Int, sentence: String ->
            val newCount = acc + countLinesInSentence(sentence, font, fontMetrics, formatWidth)
            newCount
        }
        return lineCount
    }

    private fun countLinesInSentence(
        textRaw: String,
        font: Font,
        fontMetrics: FontMetrics,
        formatWidth: Float
    ): Int {
        val text = AttributedString(textRaw)
        text.addAttribute(TextAttribute.FONT, font)
        val frc = fontMetrics.fontRenderContext
        val charIt = text.iterator
        val lineMeasurer = LineBreakMeasurer(
            charIt,
            BreakIterator.getWordInstance(),
            frc
        )
        lineMeasurer.position = charIt.beginIndex
        var noLines = 0
        while (lineMeasurer.position < charIt.endIndex) {
            lineMeasurer.nextLayout(formatWidth)
            noLines++
        }
        return noLines
    }
}

Also, may be useful as well, a GUI application that lets you test out the line counter.

package [your package name]

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.awt.*
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.JFrame
import javax.swing.JPanel
import javax.swing.JTextArea
import javax.swing.SwingUtilities

class MainJTextArea(
    private val l: Logger
): JPanel(GridBagLayout()) {

    init {
        val inputStr = "Lorem ipsum dolor sit amet, consectetur adipisicing\n elit, sed do eiusmo," +
                " Lorem ipsum \ndolor sit amet, consectetur adipisicing elit, sed do eiusmo," +
                " Lorem ipsum dolor sit amet, \nconsectetur adipisicing elit, sed do eiusmo," +
                " Lorem ipsum dolor sit amet, \nconsectetur adipisicing elit, sed do eiusmo"

        val textArea = drawTextArea(
            text = inputStr,
            fontSize = 12.0
        )
        val textAreaLineCounter = TextAreaLineCounter(textArea)

        // Add Components to this panel.
        val c = GridBagConstraints().apply {
            gridwidth = GridBagConstraints.REMAINDER
            fill = GridBagConstraints.BOTH
            weightx = 1.0
            weighty = 1.0
        }
        add(textArea, c)
        addComponentListener(object : ComponentAdapter() {
            override fun componentResized(e: ComponentEvent?) {
                super.componentResized(e)
                l.debug("Line count: ${textAreaLineCounter.countLines()}")
            }
        })
    }

    private fun drawTextArea(
        text: String,
        fontSize: Double = 12.0
    ): JTextArea {
        val textArea = JTextArea(text)
        textArea.size = Dimension(width, height)
        textArea.foreground = Color.BLACK
        textArea.background = Color(0, 0, 0, 0)
        textArea.font = Font(null, Font.LAYOUT_LEFT_TO_RIGHT, fontSize.toInt())
        textArea.lineWrap = true
        textArea.wrapStyleWord = true
        return textArea
    }

    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val logger = LoggerFactory.getLogger(MainJTextArea::class.java)!!
            SwingUtilities.invokeLater {
                val frame = JFrame("JTextAreaLineCountDemo").apply {
                    preferredSize = Dimension(400, 360)
                    defaultCloseOperation = JFrame.EXIT_ON_CLOSE
                    add(MainJTextArea(logger))
                    pack()
                }
                frame.isVisible = true
            }
        }
    }
}

Update

After further invetigation, I’ve noticed calculator was still having problems and needed a bit of customization. So I’ve improved calculation mechanism to provide details with text breaks composed inside.

This mechanism works most of the time. I’ve noticed couple of cases, where JTextArea would wrap with empty line, which was not detected. So use the code at your own risk.

/**
 * Parses text to fit in [TextProvider.formatWidth] and wraps whenever needed
 */
class TextAreaLineCounter(
    private val textProvider: TextProvider
) {

    private val formatWidth: Float
        get() = textProvider.formatWidth

    fun parseParagraph(
        font: Font,
        fontMetrics: FontMetrics
    ): WrappedParagraph {
        return countLinesInParagraphs(
            textRaw = textProvider.text,
            font = font,
            fontMetrics = fontMetrics,
            formatWidth = formatWidth
        )
    }

    /**
     * Counts lines in [JTextArea]
     * Includes line breaks ('\n')
     */
    private fun countLinesInParagraphs(
        textRaw: String,
        font: Font,
        fontMetrics: FontMetrics,
        formatWidth: Float
    ): WrappedParagraph {
        val paragraphsAsString: List<String> = textRaw.split("\n")
        val sentences = paragraphsAsString.map { paragraph ->
            countLinesInSentence(paragraph, font, fontMetrics, formatWidth)
        }
        return WrappedParagraph(sentences = sentences)
    }

    /**
     * Counts lines in wrapped [JTextArea]
     * Does not include line breaks.
     */
    private fun countLinesInSentence(
        textRaw: String,
        font: Font,
        fontMetrics: FontMetrics,
        formatWidth: Float
    ): Sentence {
        if (textRaw.isEmpty()) {
            return Sentence(
                wraps = listOf(
                    SentenceWrap(
                        wrapIndex = -1,
                        wrapText = textRaw
                    )
                )
            )
        }
        val text = AttributedString(textRaw)
        text.addAttribute(TextAttribute.FONT, font)
        val frc = fontMetrics.fontRenderContext
        val charIt = text.iterator
        val words = mutableListOf<SentenceWrap>()
        val lineMeasurer = LineBreakMeasurer(
            charIt,
            BreakIterator.getLineInstance(),
            frc
        )
        lineMeasurer.position = charIt.beginIndex
        var posBegin = 0
        var posEnd = lineMeasurer.position
        var noLines = 0
        do {
            lineMeasurer.nextLayout(formatWidth)
            posBegin = posEnd
            posEnd = lineMeasurer.position
            words.add(
                SentenceWrap(
                    wrapIndex = noLines,
                    wrapText = textRaw.substring(posBegin, posEnd)
                )
            )
            noLines++
        } while (posEnd < charIt.endIndex)
        return Sentence(words)
    }

    /**
     * Holds wrapped [Sentence]s that break during 'wrap text' or text break symbols
     */
    data class WrappedParagraph(
        val sentences: List<Sentence>
    ) {
        fun lineCount(): Int {
            val sentenceCount = sentences.fold(0) { currentCount: Int, sentence: Sentence ->
                val newCount = currentCount + sentence.lineCount()
                newCount
            }
            return sentenceCount
        }
    }

    /**
     * Sentence contains text pieces which are broken by 'wrapText'
     */
    data class Sentence(
        val wraps: List<SentenceWrap>
    ) {
        fun lineCount(): Int = wraps.size
    }

    /**
     * Entity for holding a wrapped text snippet
     */
    data class SentenceWrap(
        val wrapIndex: Int,
        val wrapText: String
    )

    interface TextProvider {
        val text: String
        val formatWidth: Float
    }

    companion object {
        val l = LoggerFactory.getLogger(TextAreaLineCounter::class.java)!!
    }
}