I have a problem with my Jbuttons.
In my application, you can easily change the ui language and you can easily override the default translations for my buttons.
In this case, it is very unclear how long the text can be in a button, but my buttons have a fixed size (because of graphical harmony and so on).
Now my problem is, that I haven't found a solution to wrap a text with a inner button margin.
Example:
BUTTON 1:
"Hello"
-> Hello is short enough to be printed without newline.
BUTTON 2:
"Hello guys"
-> Due to the html tags, Hello guys will be automatically wrapped in two lines.
BUTTON 3:
"Hello g."
-> Hello g. fills exactly the width of the button. No word wrap by HTML!.
Now, the button 3 itself looks very crappy and overloaded.
Therefore I need a solution to automatically wrap a text which is wider or equal -4px than the button.
Additionally a text which doesn't contain a whitespace, should be wrapped either if it is too long.
A very easy solution for this problem is a utility method, I wrote.
Just call it in your *ButtonUI #paint method before you call super.paint(c,g);
e.g.:
if (c instanceof AbstractButton) {
String txt = button.getText();
button.setText(getWrappedText(g, button, txt));
}
Here is my formatter for free use (and for optimization, too ;) )
private static final String STR_NEWLINE = "<br />";
private FontRenderContext fontRenderContext = new FontRenderContext(new AffineTransform(), true, true);
private String getWrappedText(Graphics graphics, AbstractButton button, String str) {
if( str != null ) {
String text = str.replaceAll("<html><center>", "").replaceAll("</center></html>", "");
int width = button.getWidth();
Rectangle2D stringBounds = button.getFont().getStringBounds(text, fontRenderContext);
if ( !str.contains(STR_NEWLINE) && (width-5) < ((Double)stringBounds.getWidth()).intValue()) {
String newStr;
if( str.contains(" ") ) {
int lastIndex = str.lastIndexOf(" ");
newStr = str.substring(0, lastIndex)+STR_NEWLINE+str.substring(lastIndex);
} else {
int strLength = ((str.length()/3)*2);
newStr = str.substring(0, strLength)+STR_NEWLINE+str.substring(strLength);
}
return newStr;
}
}
return str;
}
Related
Summary
I have a string [tab] [ch]C[/ch] [ch]Am[/ch] \n I heard there was a secret chord[/tab]
When the TextView is big enough to hold it with no wrapping it should (and does) look like this:
C Am
I heard there was a secret chord
When the line(s) are too long to fit in the TextView, I want it to wrap like this:
C
I heard there was a
Am
secret chord
Right now it wraps like this (like you'd expect if it was just text)
C
Am
I heard there was a
secret chord
Constraints:
I use a monospace text font to keep alignment
The chords (C, F, Am, G) are clickable so if you make a custom implementation of TextView, it still has to be able to handle ClickableSpans or otherwise keep them clickable
Kotlin or Java (or XML) is fine
If it's helpful, this is for an open source project of mine, so the source is available on Github. Here's the fragment source (look for fun processTabContent(text: CharSequence) -- that's where I process the text right now. Here's the layout xml.
Input Format
My data is stored in a single string (this can't be changed -- I get it from an API). Here's how the above tab would be formatted:
[Intro]\n[tab][ch]C[/ch] [ch]Am[/ch] [ch]C[/ch] [ch]Am[/ch][/tab]\n[Verse 1][tab] [ch]C[ch] [ch]Am[/ch] I heard there was a secret chord [/tab][tab] [ch]C[/ch] [ch]Am[/ch]\nThat David played, and it pleased the Lord[/tab][tab] [ch]C[/ch] [ch]F[/ch] [ch]G[/ch]\n But you don't really care for music, do you?[/tab]
Note that the chords (notes that a guitarist would play, like C or F) are wrapped in [ch] tags. I currently have code that finds these, removes the [ch] tags, and wraps each chord in a ClickableSpan. On click, my application shows another fragment with instructions how to play the chord on a guitar. This is only important in that the answer to this question must allow these chords to be clicked like this still.
What I'm doing right now (that isn't working)
As you may have noticed by now, it's the [tab] tags that we're going to have to focus on for this question. Right now, I'm going through the string and replacing [tab] with a newline and removing all instances of [/tab]. This works fine if my TextView's text size is small enough that entire lines fit on the device screen. However, when the word wrap kicks in I start having problems.
This:
C Am
I heard there was a secret chord
Should wrap to this:
C
I heard there was a
Am
secret chord
But instead wraps like this:
C
Am
I heard there was a
secret chord
I think this solution might solve the issue. But there are some assumption,
Every lyric starts with [tab] and end with [/tab]
It is always separated with \n between chords and lyric
And I believe you need to cleanse the data before you use it. Since, it is likely possible to handle Intro, Verse easily, I will focus on lyric tab only.
Here is the sample data for single lyric
[tab] [ch]C[/ch] [ch]F[/ch] [ch]G[/ch]
\n But you don't really care for music, do you?[/tab]
Firstly, We need to remove some unwanted blocks.
val inputStr = singleLyric
.replace("[tab]", "")
.replace("[/tab]", "")
.replace("[ch]", "")
.replace("[/ch]", "")
After that, I separated the chords and lyric
val indexOfLineBreak = inputStr.indexOf("\n")
val chords = inputStr.substring(0, indexOfLineBreak)
val lyrics = inputStr.substring(indexOfLineBreak + 1, inputStr.length).trim()
After we clean the data, we can start to set the data.
text_view.text = lyrics
text_view.post {
val lineCount = text_view.lineCount
var currentLine = 0
var newStr = ""
if (lineCount <= 1) {// if it's not multi line, no need to manipulate data
newStr += chords + "\n" + lyrics
} else {
val chordsCount = chords.count()
while (currentLine < lineCount) {
//get start and end index of selected line
val lineStart = text_view.layout.getLineStart(currentLine)
val lineEnd = text_view.layout.getLineEnd(currentLine)
// add chord substring
if (lineEnd <= chordsCount) //chords string can be shorter than lyric
newStr += chords.substring(lineStart, lineEnd) + "\n"
else if (lineStart < chordsCount) //it can be no more chords data to show
newStr += chords.substring(lineStart, chordsCount) + "\n"
// add lyric substring
newStr += lyrics.substring(lineStart, lineEnd) + "\n"
currentLine++
}
}
text_view.text = newStr
}
Idea is simple. After we set the lyric data to textview, we can get line count. With the current line number, we can get starting index and ending index of the selected line. With the indexes, we can manipulate the string. Hope this can help u.
This is based off of Hein Htet Aung's answer. The general idea is that you have two lines passed in (singleLyric), but the lines might have to be processed before appending them (hence the middle while loop). For convenience, this was written with a parameter appendTo that the lyric will be appended to. It returns a finished SpannableStringBuilder with the lyric appended. It would be used like this:
ssb = SpannableStringBuilder()
for (lyric in listOfDoubleLyricLines) {
ssb = processLyricLine(lyric, ssb)
}
textView.movementMethod = LinkMovementMethod.getInstance() // without LinkMovementMethod, link can not click
textView.setText(ssb, TextView.BufferType.SPANNABLE)
Here's the processing function:
private fun processLyricLine(singleLyric: CharSequence, appendTo: SpannableStringBuilder): SpannableStringBuilder {
val indexOfLineBreak = singleLyric.indexOf("\n")
var chords: CharSequence = singleLyric.subSequence(0, indexOfLineBreak).trimEnd()
var lyrics: CharSequence = singleLyric.subSequence(indexOfLineBreak + 1, singleLyric.length).trimEnd()
var startLength = appendTo.length
var result = appendTo
// break lines ahead of time
// thanks #Andro https://stackoverflow.com/a/11498125
val availableWidth = binding.tabContent.width.toFloat() //- binding.tabContent.textSize / resources.displayMetrics.scaledDensity
while (lyrics.isNotEmpty() || chords.isNotEmpty()) {
// find good word break spot at end
val plainChords = chords.replace("[/?ch]".toRegex(), "")
val wordCharsToFit = findMultipleLineWordBreak(listOf(plainChords, lyrics), binding.tabContent.paint, availableWidth)
// make chord substring
var i = 0
while (i < min(wordCharsToFit, chords.length)) {
if (i+3 < chords.length && chords.subSequence(i .. i+3) == "[ch]"){
//we found a chord; add it.
chords = chords.removeRange(i .. i+3) // remove [ch]
val start = i
while(chords.subSequence(i .. i+4) != "[/ch]"){
// find end
i++
}
// i is now 1 past the end of the chord name
chords = chords.removeRange(i .. i+4) // remove [/ch]
result = result.append(chords.subSequence(start until i))
//make a clickable span
val chordName = chords.subSequence(start until i)
val clickableSpan = makeSpan(chordName)
result.setSpan(clickableSpan, startLength+start, startLength+i, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} else {
result = result.append(chords[i])
i++
}
}
result = result.append("\r\n")
// make lyric substring
val thisLine = lyrics.subSequence(0, min(wordCharsToFit, lyrics.length))
result = result.append(thisLine).append("\r\n")
// update for next pass through
chords = chords.subSequence(i, chords.length)
lyrics = lyrics.subSequence(thisLine.length, lyrics.length)
startLength = result.length
}
return result
}
And finally, I found the need to break my text at words rather than just at the max line length, so here's the word break finder function for that:
private fun findMultipleLineWordBreak(lines: List<CharSequence>, paint: TextPaint, availableWidth: Float): Int{
val breakingChars = "‐–〜゠= \t\r\n" // all the chars that we'll break a line at
var totalCharsToFit: Int = 0
// find max number of chars that will fit on a line
for (line in lines) {
totalCharsToFit = max(totalCharsToFit, paint.breakText(line, 0, line.length,
true, availableWidth, null))
}
var wordCharsToFit = totalCharsToFit
// go back from max until we hit a word break
var allContainWordBreakChar: Boolean
do {
allContainWordBreakChar = true
for (line in lines) {
allContainWordBreakChar = allContainWordBreakChar
&& (line.length <= wordCharsToFit || breakingChars.contains(line[wordCharsToFit]))
}
} while (!allContainWordBreakChar && --wordCharsToFit > 0)
// if we had a super long word, just break at the end of the line
if (wordCharsToFit < 1){
wordCharsToFit = totalCharsToFit
}
return wordCharsToFit
}
I have been trying to get simple rich text in JavaFX: I wish for continuous text where some characters are bold, subscript, or superscript. This is impossible in the normal Text or Label classes. I have tried WebView with no success, as although it will display such text, it does not size to its contents, and thus takes up an uncontrollably large portion of the screen.
Now I am attempting to use a TextFlow. I can successfully link together Text objects, some of which can be made bold. However, subscript and superscript are proving more difficult. Subscript can be emulated simply by reducing the font size, however superscript needs that Text object to be raised above the others. I cannot find a way of doing this: TextFlow specifically ignores translation properties of the Text objects, and I cannot override getBaselineOffset() on the Text in question, as it is final.
Am I going to have to put the Texts in an HBox? Is there really no support for this in JavaFX? What I am trying to do is not complex; it seems mind-boggling that there is no native support for subscript and superscript.
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextFlow;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class TextFlowBuilder
{
private static final String BOLD = "<b>";
private static final String UN_BOLD = "</b>";
private static final String SUPERSCRIPT = "<sup>";
private static final String UN_SUPERSCRIPT = "</sup>";
private static final String SUBSCRIPT = "<sub>";
private static final String UN_SUBSCRIPT = "</sub>";
private static final Pattern NOT_JUST_WHITESPACE = Pattern.compile("\\S");
private static final Pattern CHARACTER_CODE = Pattern.compile("&#(\\d+);");
public static TextFlow htmlToTextFlow(final String html, final int fontSize, final TextAlignment alignment)
{
final String[] split = html.split("(?<=>)|(?=<)"); //Split before and after tags, splitting it into a series of tags and tag contents.
final List<Text> texts = new LinkedList<>();
boolean b = false;
boolean sup = false;
boolean sub = false;
for (String segment : split)
{
switch (segment)
{
case BOLD:
b = true;
break;
case UN_BOLD:
b = false;
break;
case SUPERSCRIPT:
sup = true;
break;
case UN_SUPERSCRIPT:
sup = false;
break;
case SUBSCRIPT:
sub = true;
break;
case UN_SUBSCRIPT:
sub = false;
break;
default:
//Add as text if string is not a tag, and is more than just whitespace.
if (segment.length() > 0
&& NOT_JUST_WHITESPACE.matcher(segment).find()
&& !segment.startsWith("<"))
{
final Matcher m = CHARACTER_CODE.matcher(segment);
while (m.find())
{
final String specialChar = Character.toString((char)Integer.parseInt(m.group(1)));
segment = m.replaceFirst(specialChar);
}
final Text t = new Text(segment);
String style = "";
if (b)
style += "-fx-font-weight: bold; ";
if (sup)
{
style += String.format("-fx-font-size: %f.3; ", fontSize/1.75);
//Need to move text to above the rest
}
else if (sub)
{
style += String.format("-fx-font-size: %f.3; ", fontSize/1.75);
}
else
{
style += String.format("-fx-font-size: %d; ", fontSize);
}
t.setStyle(style);
texts.add(t);
}
}
}
final Text[] textsAsArray = new Text[texts.size()];
final TextFlow tf = new TextFlow(texts.toArray(textsAsArray));
tf.setTextAlignment(alignment);
return tf;
}
}
Setting the translateY property (in code, not sure about via CSS) on the text objects does indeed work - perhaps something has changed about it since the question was asked.
Here's a demonstration:
TextFlow container = new TextFlow();
Text normal = new Text("Normal");
Text sup = new Text("sup");
Text sub = new Text("sub");
sup.setTranslateY(normal.getFont().getSize() * -0.3);
sub.setTranslateY(normal.getFont().getSize() * 0.3);
container.getChildren().addAll(normal, sup, sub);
Here's what it looks like:
I am using JavaFX 11 on a Mac, in case that ends up making a difference to whether or not this actually works.
Use -fx-translate-y
You can control the text up and down
You can also use RichTextFX
that can be used to create an editable text area with varying styles and with Superscript and Subscript support
I have written simple GUI screen that actually contains a JavaFx textField where the user enters a string and that String I have to pass to TCP client.
Now the problem is user enters a string that contains SOH as the delimiter.
SOH is nothing but "\u001". But when the user enters a String that contains this delimiters then it is removed and only the simple plain text is retrieved. How can I eliminate this. This unique character is very important for me.
For example my string is as follows:
8=FIX.4.2\u001 9=9 \u001 35=A \u001 34=1\u001 49=TTDS68AP
Observer the above String where I have the \u001 character representing SOH. But when I entered this String in the TextField then the result is like:
8=FIX.4.29=9135=A34=149=TTDS68AP
How can I get the SOH character too from the Text Field?
The problem is that javaFX TextField filters invalid characters with the following function:
private static boolean isInvalidCharacter(char c, boolean newlineIllegal, boolean tabIllegal) {
if (c == 0x7F) return true;
if (c == 0xA) return newlineIllegal;
if (c == 0x9) return tabIllegal;
if (c < 0x20) return true;
return false;
}
This happens after you paste and before the String is stored in the field. Before this function is called a custom filter can be set and applied, in this filter you can change SOH to something else; it's unicode representation ␁ is a good candidate:
textField.setTextFormatter(new TextFormatter<>((t) -> {
t.setText(t.getText().replace((char)1, '\u2401'));
return t;
}));
Adding the above will change it after it's pasted and before it's stored. When you want to use the String from the textField you need to replace it back to SOH with:
String withSOH = field.getText().replace('\u2401', (char)1);
I think the problem is that you want to enter by keyboard and see the control character in the text field:
Ctrl-A (SOH) is a known shortcut, and selects the previous character.
The control character will not be displayed in a normal font.
So I came to the following solution:
String SOH_REPR = "°"; // We show SOH as this string.
someTextField.setOnKeyPressed((ke) -> {
if (ke.getCode().getName().equals("A")
&& ke.isControlDown()
&& !ke.isAltDown()
&& !ke.isMetaDown()
&& !ke.isShiftDown()) {
TextField textField = (TextField) ke.getSource();
ke.consume();
int pos = textField.getCaretPosition();
textField.insertText(pos, SOH_REPR);
}
});
someTextField.setOnKeyReleased((ke) -> {
if (ke.getCode().getName().equals("A")
&& ke.isControlDown()
&& !ke.isAltDown()
&& !ke.isMetaDown()
&& !ke.isShiftDown()) {
ke.consume();
}
});
// Convert it back.
String text = someTextField.getText().replace(SOH_REPR, "\u0001");
Pasting text with SOHs would still needed to be done for completeness sake.
Try adding the your encoding as argument.
System.setProperty("file.encoding", "yourEncoding");
I think UTF-8 should work.
I want to add text to the EditText without losing the previous text.
Ex: when typing 74, i want to add the "4" to the text box without erasing the number "7" entered before.
public void add4()
{
res = (EditText)findViewById(R.id.editText1);
if(res.getText().toString() == "0")
{
res.setText("4");
}
else
{
// add number 4 to the remaining string
}
}
You can use the append method to append to existing text.
res.append("4");
http://developer.android.com/reference/android/widget/TextView.html#append(java.lang.CharSequence)
(As a side note, don't use == to compare strings, use .equals())
Try this:
I used the .equals method on a string object to avoid the NullPointerException that may happen if the object is null or not a string.
public void add4() {
res = (EditText)findViewById(R.id.editText1);
if( "0".equals(res.getText().toString()) )
{
res.setText("4");
}
else
{
res.append("4");
}
}
res.append("4"); or you can use res.setText(res.getText() + "4");
I'm back again with a simpler question! I'd like the content of this JLabel (triedLettersLA) to update periodically throughout the application (I have that part handled).
But I'd like to ADD text to the label. NOT rewrite it entirely. For example.. If the text said "Letters Tried: ", I'd want to add "N", and "X", and then "H" on three separate occasions. So at the end, it'd look like this "Letters Tried: N X H". Here's what I have, and it's totally not working..
This is way up top,
JLabel triedLettersLA = new JLabel("Tried letters:");
public boolean used[] = new boolean[26];
And this is lower down in my code..
StringBuffer missedLetter = new StringBuffer();
for (int le = 0; le <= 25; le++) {
if (used[le]) missedLetter.append((char)(le + 'a'));
String triedLettersLA.getText(t);
triedLettersLA.setText(t + " " + missedLetter.toString());
}
The code you posted makes no sense (nor could it ever compile). Well, it would compile now, possibly.
That being said, a String in Java is immutable; you can't change it. To change the text of a JLabel you need to create a new String and call the JLabel's setText() method.
String old = triedLettersLA.getText();
String newString = old + " N"; // this creates a new String object
triedLettersLA.setText(newString);
Nonsence code:
String triedLettersLA.getText(t);
Change it to:
String t = triedLettersLA.getText();