Change line spacing in JTextArea (not JTextPane) - java

I'm trying to find a way to change the line spacing in a JTextArea component.
A little bit of searching always seems to reveal the same answer: "Use a JTextPane instead and then call setParagraphAttributes".
But I'm wondering whether it's possible to achieve this with just a JTextArea by, for example, messing with the font.
With the deriveFont(...) method, it's possible to change the tracking and the kerning of the font, i.e. the horizontal spacing between characters, but I haven't been able to find a way to change the vertical spacing (maybe ascent, descent, leading). Am I missing something there?

As camickr pointed out, JTextArea does not provide a way to change the line height directly. It simply uses the font height provided by the corresponding FontMetrics.
But, this leaves a point of attack open using the following helper-class:
public class FontMetricsWrapper extends FontMetrics {
private final FontMetrics target;
public FontMetricsWrapper(FontMetrics target) {
super(target.getFont());
this.target = target;
}
#Override
public int bytesWidth(byte[] data, int off, int len) {
return target.bytesWidth(data, off, len);
}
#Override
public int charWidth(char ch) {
return target.charWidth(ch);
}
#Override
public int charWidth(int codePoint) {
return target.charWidth(codePoint);
}
// ... tons more #Override's, all of the same form:
//
// #Override
// public baz foo(bar, ...) {
// return target.foo(bar, ...);
// }
}
Then, it becomes possible to create the JTextArea like this:
JTextArea myTextArea = new JTextArea("Some text") {
#Override
public FontMetrics getFontMetrics(Font font) {
return new FontMetricsWrapper(super.getFontMetrics(font)) {
#Override
public int getHeight() {
return 10; // Gives line height in pixels
}
};
}
};
This is definitely not the cleanest solution and is merely meant as proof of concept. For example, one issue is that getFontMetrics(...) is called quite often, and, in the given example, creates a new instance of the wrapper class each time. So, at the very least, a HashMap that caches the created FontMetricsWrapper for each given font would be in order...
But, what I was really hoping for was a way to play with the Font or maybe the associated FontRenderContext passed into the JTextArea to modify the line height. For example, is there some way to influence font's reported ascent, descent, and leading values? Seems strange that you can change pretty much any other aspect of the font's appearance, except this one...

What's wrong with light-weight coding?
Nothing, you should use the simplest component for the job. But if the simple component doesn't support a requirement it is usually because the requirement is more complex and you need a more complex component to implement the functionality. Rarely would it be as simple at setting a property of a class.
Sometimes I'm simply interested in learning something new and figuring out what's possible.
Swing text components use a View to paint the text. It is the view's responsibility to format and position the text. So each view determines when to wrap and where to position the next line.
In the case of a JTextArea it uses either a Plainview or a WrappedPlanView. For the Plainview the painting code is:
drawLine(line, g, x, y);
y += fontHeight;
where the fontHeight is determined by using the FontMetrics.getHeight() method.
So the value is basically hard coded in the View. You could always provide a custom View for your text area, but overriding a View is generally not an easy task.

Related

Set only maximum height for a panel (without using setMaximumSize) with GroupLayout

I looked around at all the other answers, but they all recommend to use GroupLayout, BoxLayout, or to wrap the panel that's using GridBagLayout with another panel that uses one of the layouts mentioned above.
I'm currently using GridBagLayout, and I'm wondering if there's a way to set the maximum height of a panel. I don't want a limit on width, only height, so setMaximumSize(Dimension) won't work.
Does GridBagLayout support this in any way? Sorry if my question doesn't contain any code, the only attempt I could possibly find is setMaximumSize(Dimension), or wrapping it in another panel (which I'm hoping to avoid)
If you want to limit only one dimension then just do not limit the other:
component.setMaximumSize( new Dimension(
Integer.MAX_VALUE,
requiredMaxHeight
) );
Well, I would say that almost always the right solution is to fix the layouting either by using a different layout manager, or by using a different hierarchy of containers (or both).
However, since it seems you won't be persuaded (I infer that from your question), I can suggest a solution to the specific question you ask (again, I would recommend to take a different path of fixing the layout, which probably is your real problem).
You can set the maximum height without affecting the maximum width, by overriding the setMaximumSize() method as follows:
#Override
public void setMaximumSize(Dimension size) {
Dimension currMaxSize = getMaximumSize();
super.setMaximumSize(currMaxSize.width, size.height);
}
Another approach can be to keep the "overridden" setting of the max height, and return it when returning the maximum height, like so:
private int overriddenMaximumHeight = -1;
public void setMaximumHeight(int height) {
overriddenMaximumHeight = height;
}
#Override
public Dimension getMaximumSize() {
Dimension size = super.getMaximumSize();
int height = (overriddenMaximumHeight >=0) ? overriddenMaximumHeight : size.height;
return new Dimension(size.width, height);
}
Again (lastly), I would recommend taking a more common approach, but if you insist ...

How to dynamically add .css to a custom Javafx LineChart Node?

So, my issue is this: I'm attempting to define a custom set of nodes for a Javafx XYChart LineChart, where each node corresponds to a point that was plotted directly from the datasets. After looking around a little bit, Jewlesea actually had a solution at one point about how to add dynamic labels to nodes on a linechart graph that gave me enough of a push in the right direction to create black symbols (they are dots at the moment, but they can be many different things). Now I have a requirement that requires me to change ONE of the nodes on the XY chart into an 'X'. this could be either through loading an image in place of the 'node', or through physically manipulating the 'shape' parameter in .css.
The problem begins when I try to add this property dynamically, since which node has the 'x' will always be changing. Here are the things I've tried, and they all end up with no results whatsoever, regardless of the property used.
private XYChart.Data datum( Double x, Double y )
{
final XYChart.Data data = new XYChart.Data(x, y);
data.setNode(
new HoveredThresholdNode(x, y));
//data.getNode().setStyle("-fx-background-image: url(\"redX.png\");");
data.getNode().styleProperty().bind(
new SimpleStringProperty("-fx-background-color: #0181e2;")
.concat("-fx-font-size: 20px;")
.concat("-fx-background-radius: 0;")
.concat("-fx-background-insets: 0;")
.concat("-fx-shape: \"M2,0 L5,4 L8,0 L10,0 L10,2 L6,5 L10,8 L10,10 L8,10 L5,6 L2,10 L0,10 L0,8 L4,5 L0,2 L0,0 Z\";")
);
data.getNode().toFront();
return data;
}
So in the above, you can see that this is adding a property through the use of the 'bind' function after the dataNode has already been created. Also note above, I tried doing it through the 'setStyle' interface at this level to give it a background image, with no success. Also, no errors are being thrown, no 'invalid css' or anything of the sort, just simply no display on the graph at all when done this way.
now, in the HoveredThresholdNode (Again a big thanks to Jewelsea for being a master of Javafx and putting this bit of code online, it's where 90% of this class came from.) I tried essentially the same thing, at a different level. (actually being IN the node creation class, as opposed to a layer above it).
class HoveredThresholdNode extends StackPane {
/**
*
* #param x the x value of our node (this gets passed around a bunch)
* #param y the y value of our node (also gets passed around a bunch)
*/
HoveredThresholdNode(Double x, Double y) {
//The preferred size of each node of the graph
//getStylesheets().add(getClass().getResource("style/XYChart.css").toExternalForm());
//getStyleClass().add("xyChart-Node");
//setOpacity(.8);
styleProperty().bind(
new SimpleStringProperty("-fx-background-color: #0181e2;")
.concat("-fx-font-size: 20px;")
.concat("-fx-background-radius: 0;")
.concat("-fx-background-insets: 0;")
.concat("-fx-shape: \"M2,0 L5,4 L8,0 L10,0 L10,2 L6,5 L10,8 L10,10 L8,10 L5,6 L2,10 L0,10 L0,8 L4,5 L0,2 L0,0 Z\";")
);
//this label is the 'tooltip' label for the graph.
final Label label = createDataThresholdLabel(x, y);
final double Myx = x;
final double Myy = y;
setOnMouseEntered(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
if (Myx == 0) {
label.setTextFill(Color.DARKGRAY);
} else if (Myx > 0) {
label.setTextFill(Color.SPRINGGREEN);
} else {
label.setTextFill(Color.FIREBRICK);
}
label.setText("Current position: " + Myx + " , " + Myy);
//setCursor(Cursor.NONE);
toFront();
}
});
setOnMouseExited(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
//getChildren().clear();
//setCursor(Cursor.CROSSHAIR);
}
});
}
Now note, I also tried the setStyle(java.lang.String) method, with all of the same type of CSS, with no success. I have NO idea why this isn't styling dynamically. It's almost as if the custom nodes are simply ignoring all new .css that I define at runtime?
Any help would be greatly appreciated, please don't be shy if you need more details or explanation on any points.
So, I did finally find a good workaround to solve my problem, although not in the way I thought it would happen. The main problem I was having, was that I was extending from stackPane to create my node, which only had a very small number of graphical display options available to it, and by switching the 'prefSize()' property, I was simply changing the size of that stackPane, and then filling in the background area of that stack pane black, giving it a very deceptive shape-look to it.
So rather than use a stack pane, whenever I reached the node that I needed to place the red 'X' on, I simply called a different Datum method that returned a datum with an ImageView Attached, like so:
private XYChart.Data CoLDatum(Double x, Double y){
final XYChart.Data data = new XYChart.Data(x, y);
ImageView myImage = new ImageView(new Image(getClass().getResource("style/redX.png").toExternalForm()));
data.setNode(myImage);
data.getNode().setOnMouseEntered(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
main_label.setText("Some Text.");
}
});
data.getNode().setOnMouseExited(new EventHandler<MouseEvent>(){
#Override public void handle(MouseEvent mouseEvent) {
main_label.setText("");
}
});
return data;
}
and since ImageView is an implementing class of Node, this worked out just fine, and allowed me to load up an image for that one single node in the graph, while still maintaining a listener to give custom text to our information label when the red 'x' was hovered over with a mouse. Sometimes, it's the simple solutions that slip right past you.
I imagine that, had I employed stackPane properties properly with the setStyle(java.lang.String) method, they would have absolutely shown up, and I was just butchering the nature of a stack pane. Interesting.
Hopefully this helps somebody else stuck with similar problems!

Creating custom swing component out of existing

So, I have this JTexrtArea which is almost perfect for my needs. The only thing wrong with it is the line spacing. I can't set it. (Why not JTextPane? Because spacing CAN be changed in JTextArea and JTextArea is way lighter thatn JTextPane, and I have a bunch of those in my program).
I have asked this question before, and this is the answer that I got from user StanislavL:
To override JTextArea's line spacing take a look at the PlainView (used to render PLainDocument).
There are following lines in the public void paint(Graphics g, Shape a) method
drawLine(line, g, x, y);
y += fontHeight;
So you can adapt the rendering fixing y offset.
In the BasicTextAreaUI method to create view. Replace it with your own implementation of the PlainView
public View create(Element elem) {
Document doc = elem.getDocument();
Object i18nFlag = doc.getProperty("i18n"/*AbstractDocument.I18NProperty*/);
if ((i18nFlag != null) && i18nFlag.equals(Boolean.TRUE)) {
// build a view that support bidi
return createI18N(elem);
} else {
JTextComponent c = getComponent();
if (c instanceof JTextArea) {
JTextArea area = (JTextArea) c;
View v;
if (area.getLineWrap()) {
v = new WrappedPlainView(elem, area.getWrapStyleWord());
} else {
v = new PlainView(elem);
}
return v;
}
}
return null;
}
I grasp the general idea of what he's telling me to do, but I don't know how to do it.
Also, I wouldn't like to override the default JTextArea "property", I'd like to have a choice - to use the default one or to use a custom one.
Only change in JTextArea code would be from
y += fontHeight,
to
y+= (fontHeight +(or -) additionalSpacing).
How do I achieve this?
Which classes do I use/copy?
Where do I put them?
How do I make them usable?
How do I get the whole thing working?
If you think this is too specific to be useful, maybe someone could write a general tutorial on how to create a custom swing component based 100% on an existing one. Then someone could easely change some values to better adjust it to it's needs.
I am simply going to copy-paste my answer from your other question.
I'd like to change the spacing inbetweem the rows of a JTextArea
My first thought was that overriding javax.swing.JTextArea#getRowHeight would be sufficient. The javadoc clearly states
Defines the meaning of the height of a row. This defaults to the height of the font.
So I was hoping that by overriding this method, you would adjust the definition and you would get more spacing between the rows. Bummer, didn't work. A quick search on the usages of that method in the JDK revealed the same. It is mainly used to calculate some sizes, but certainly not used when painting text inside the component.
By looking at the source code of the javax.swing.text.PlainView#paint method, I saw that the FontMetrics are used, and those you can easily override in the JTextArea. So second approach was to extend the JTextArea (bwah, extending Swing components but it is for a proof-of-concept)
private static class JTextAreaWithExtendedRowHeight extends JTextArea{
private JTextAreaWithExtendedRowHeight( int rows, int columns ) {
super( rows, columns );
}
#Override
public FontMetrics getFontMetrics( Font font ) {
FontMetrics fontMetrics = super.getFontMetrics( font );
return new FontMetricsWrapper( font, fontMetrics );
}
}
The FontMetricsWrapper class basically delegates everything, except the getHeight method. In that method I added 10 to the result of the delegate
#Override
public int getHeight() {
//use +10 to make the difference obvious
return delegate.getHeight() + 10;
}
And this results in more row spacing (and a caret which is way too long, but that can probably be adjusted).
A little screenshot to illustrate this (not as nice as some of the other ones, but it shows that this approach might work):
Small disclaimer: this feels like an ugly hack and might result in unexpected issues. I do hope somebody comes with a better solution.
I personally prefer the solution StanislavL is proposing, but this gives you an alternative
That's a piece of code. It's not finished. Line spacing between wrapped lines is not implemented. You can get full source of WrappedPlainView or PlainView and add your code there to achieve desired line spacing
import javax.swing.*;
import javax.swing.plaf.basic.BasicTextAreaUI;
import javax.swing.text.*;
public class LineSpacingTextArea {
public static void main(String[] args) {
JTextArea ta=new JTextArea();
JFrame fr=new JFrame("Custom line spacing in JTextArea");
fr.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ta.setText("Line 1\nLine 2\nLong text to show how line spacing works");
ta.setLineWrap(true);
ta.setWrapStyleWord(true);
ta.setUI(new CustomTextAreaUI());
fr.add(new JScrollPane(ta));
fr.setSize(100,200);
fr.setLocationRelativeTo(null);
fr.setVisible(true);
}
static class CustomTextAreaUI extends BasicTextAreaUI {
public View create(Element elem) {
Document doc = elem.getDocument();
Object i18nFlag = doc.getProperty("i18n"/*AbstractDocument.I18NProperty*/);
if ((i18nFlag != null) && i18nFlag.equals(Boolean.TRUE)) {
// build a view that support bidi
return super.create(elem);
} else {
JTextComponent c = getComponent();
if (c instanceof JTextArea) {
JTextArea area = (JTextArea) c;
View v;
if (area.getLineWrap()) {
v = new CustomWrappedPlainView(elem, area.getWrapStyleWord());
} else {
v = new PlainView(elem);
}
return v;
}
}
return null;
}
}
static class CustomWrappedPlainView extends WrappedPlainView {
public CustomWrappedPlainView(Element elem, boolean wordWrap) {
super(elem, wordWrap);
}
protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
super.layoutMajorAxis(targetSpan, axis, offsets, spans);
int ls=spans[0];
for (int i=0; i<offsets.length; i++) {
offsets[i]+=i*ls;
}
}
}
}

sorting arrows jtable column header

Does anyone know how to implement the up and down arrows of a JTable column header while sorting its rows?
I have made my own way of sorting and it is triggered by listening to mouse clicks by mouseadapter and the only things that is left is the visibility of such arrows on the header...
Is there also a way to easily implement a sortable jtable?
I finished doing all the sorting and one last thing that i can't do is show the sorting arrows..
i don't want to make a new one but i failed to find if there is an setEnableArrow or something..
any ideas about this?
You can take a look in the source code of the DefaultTableCellHeaderRenderer. In the getTableCellRendererComponent you see from where those icons are retrieved (e.g. UIManager.getIcon("Table.ascendingSortIcon")) and how this icon is set (setIcon(sortIcon);)
I suggest you don't mess up with the DefaultTableCellHeaderRenderer. The problem with this one, is that it's just that, the default. Each LaF is supposed to create a subclass of this one and do its own rendering there. My suggestion is to use a LaF that provides this functionality out of the box. I think that TinyLaf can do this but I'm not sure. You can subclass DefaultTableCellHeaderRenderer but you risk alienating the header rendering from the rest of the LaF.
So how to do it? Unicode to the rescue! Refer to the geometric shapes page and use what you like. I picked the '\u25B2' and '\u25BC' triangles. But then I had to hide the dreaded Swing icon:
UIManager.put( "Table.ascendingSortIcon", new EmptyIcon() );
UIManager.put( "Table.descendingSortIcon", new EmptyIcon() );
Be very careful with the above lines! They will replace the icons for all JTables in your application which might not be what you want. Then you should be able to see something like that:
Empty Icon can be like:
class EmptyIcon implements Icon, Serializable {
int width = 0;
int height = 0;
public void paintIcon(Component c, Graphics g, int x, int y) {}
public int getIconWidth() { return width; }
public int getIconHeight() { return height; }
}
This is the easiest way to implement sorting:
MyModel model = new MyModel();
TableRowSorter<MyModel> sorter = new TableRowSorter<MyModel> (model);
jTable1 = new javax.swing.JTable();
jTable1.setRowSorter(sorter);
jTable1.setModel(model);
What if you use JXtable instead of a Jtable?
these tables have the arrows in the header to sort them and they are easy to use...
worth trying...

Can I limit the length of text in a JTextField which can be painted, while still storing the full text?

I have a text field in my application. Despite it being a text field, users sometimes paste huge amounts of text into it. Additionally, other functions of the problem set large amounts in as well.
Sometimes there is so much text that the JVM gets an access violation in fontmanager.dll. Oracle doesn't appear to be interested in fixing the problem itself, so I would like to at least try to avoid it.
Limiting the amount of text the user inputs is apparently not acceptable (otherwise this would be the most obvious solution) but it's acceptable to allow it to be set and then disable the text field. When the text is bound back to the model, it should contain the full text again.
Since this is inherently a bug in the view, I figured that the fix should be in the view, as opposed to working around it in the model and adding the additional properties there.
My first attempt went something like this:
public class LimitedTextField extends JTextField {
static final int LIMIT = 10000;
private String fullString;
#Override
public void setText(String text) {
if (text != null && text.length() > LIMIT) {
fullString = text;
setEnabled(false);
} else {
fullString = null;
super.setText(text);
setEnabled(true);
}
}
#Override
public String getText() {
if (fullString != null) {
return fullString;
} else {
return super.getText();
}
}
}
This does pass naive unit tests, but once I wrote an additional test for BeansBinding, I found that it didn't work because BeansBinding doesn't bind to the text property but rather binds to the Document, simulating a text property. So actually getText() always returns an empty string on that test.
I am now looking at trying to make a Document implementation which will do what I want, but it sure isn't easy to do this kind of trick at the document level. I can see all the methods it has, but I can't find a good way to limit the text without also making that text unavailable when calling getText().
This is a tough one no doubt. You want to affect the painting of the field with minimal impact other than not trying to render too much text. The only suggestion I have is to look at creating your own painting strategy (see http://forums.sun.com/thread.jspa?threadID=481290) and where the text is drawn, just draw a sub-set of the characters. You could look at trying to build a new UIDelegate for drawing the text component but that might be pretty hard too. This painting overview might help steer you with regards to custom painting. This article might help you with how to apply a custom UI delegate across all textfields or just the one you are trying to correct.
Instead of using a JTextField maybe you could use a sinlge line JTextArea. Then you can insert a newline character into the Document. Then when the text is painted you will only see a single line.
You will probably need to override the getText() method as well to remove the newline character. Not sure if it helps but it may get you thinking in a different direction.
The Limited Length Document is really easy to do. Of course there are several approches, Here is the simplest (works in production in our environment):
package com.twist.ui.text.document;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.PlainDocument;
public class LimitedLengthDocument extends PlainDocument {
private static final long serialVersionUID = 1L;
private int limit;
public LimitedLengthDocument(int limit) {
super();
this.limit = limit;
}
public void insertString(int offset, String str, AttributeSet attr) throws BadLocationException {
if (str == null)
return;
// insert the string as usual.
super.insertString(offset, str, attr);
// If user tries to paste in a String that will not fit into the textfield, this approach will
// insert the text and remove the extra characters from the right.
// if resultant doc length is greater than the allowable size, truncate the document.
if( getLength() > limit )
super.remove(limit, getLength() - limit);
}
}

Categories

Resources