Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
JavaFX9でTextAreaに行番号がついたTextAreaを作ってみた
package nodamushi.jfx;
import javafx.scene.control.TextArea;
import javafx.scene.control.skin.TextInputControlSkin;
import static javafx.collections.FXCollections.observableArrayList;
import static javafx.collections.FXCollections.unmodifiableObservableList;
import java.util.ArrayList;
import java.util.List;
import javafx.collections.ObservableList;
import javafx.geometry.Rectangle2D;
/**
*
* @author nodamushi
*/
public class MyTextArea extends TextArea{
public static class Line{
int begin;
int end;
int line;
public int getBegin(){return begin;}
public int getEnd(){return end;}
public int getLine(){return line;}
public boolean contains(int index){return begin <= index && index < end;}
private boolean update(int begin,int end,int line){
if( begin == this.begin && end == this.end && line == this.line){
return false;
}
this.begin = begin;
this.end = end;
this.line = line;
return true;
}
}
public static class YRange{
public final double start;
public final double end;
public YRange(double s,double e){start = s;end = e;}
public double getStart(){return start;}
public double getEnd(){return end;}
}
@Override
protected MyTextAreaSkin createDefaultSkin() {
return new MyTextAreaSkin(this);
}
private ObservableList<Line> linesData ,unmodify;
public ObservableList<Line> getLines(){
if(unmodify == null){
linesData = observableArrayList();
unmodify = unmodifiableObservableList(linesData);
textProperty().addListener(o->updateText());
updateText();
}
return unmodify;
}
public YRange getLineYRange(Line line){
TextInputControlSkin<?> skin = (TextInputControlSkin<?>)getSkin();
Rectangle2D r1 = skin.getCharacterBounds(line.begin);
Rectangle2D r2 = skin.getCharacterBounds(line.end);
double begin = Math.min(r1.getMinY(),r2.getMinY());
double end = Math.max(r1.getMaxY(),r2.getMaxY());
return new YRange(begin,end);
}
public YRange getLineYRange(int line){
List<Line> lines = getLines();
if(lines.size() < line){
throw new IndexOutOfBoundsException(line);
}
Line l = lines.get(line);
return getLineYRange(l);
}
private void updateText(){
String text = getText();
List<Line> cloneData = this.cloneData = new ArrayList<>(linesData);
int line = 0;
int begin = 0;
boolean update=false;
int len = text.length();
int i=0;
for(;i<len;i++){
switch(text.charAt(i)){
case '\r':
if(i+1 < len && text.charAt(i+1)=='\n'){
i++;
}
case '\n':
update|=updateLine(begin, i, line);
line++;
begin = i+1;
break;
}
}
if(i!=begin){
update|=updateLine(begin, i, line);
}
this.cloneData = null;
if(line+1 < cloneData.size()){
update = true;
cloneData.subList(line+1, cloneData.size()).clear();
}
if(linesData.isEmpty()){
Line l =new Line();
l.begin = 0;l.end = 0;l.line = 0;
linesData.add(l);
}
if(update){
linesData.setAll(cloneData);
}
}
private List<Line> cloneData;
private boolean updateLine(int begin,int end,int line){
if(cloneData.size() < line+1){
Line l =new Line();
l.begin = begin;l.end = end;l.line = line;
cloneData.add(l);
return true;
}else{
return cloneData.get(line).update(begin, end, line);
}
}
}
package nodamushi.jfx;
import java.util.List;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.skin.TextAreaSkin;
import javafx.scene.text.Text;
/**
*
* @author nodamushi
*/
public class MyTextAreaSkin extends TextAreaSkin{
private final MyTextArea textarea;
private final Group lineNumbers;
private final Text dummy;
public MyTextAreaSkin(MyTextArea area){
super(area);
textarea = area;
lineNumbers = new Group();
lineNumbers.setManaged(false);
dummy = new Text();
getChildren().addAll(lineNumbers);
textarea.setWrapText(true);
textarea.scrollTopProperty().addListener(o->textarea.requestLayout());
}
private double prefLineNumbersWidth(double h){
int n =textarea.getLines().size();
StringBuilder sb = new StringBuilder();
do{
sb.append('9');
n/=10;
}while(n != 0);
dummy.setText(sb.toString());
ObservableList<Node> l =lineNumbers.getChildren();
l.add(dummy);
Bounds b = dummy.getBoundsInLocal();
l.remove(l.size()-1);
return b.getWidth();
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
double lnw = prefLineNumbersWidth(h);
double taw = w - lnw - 6;
super.layoutChildren(lnw+6+x, y, taw, h);
double zeroY = -getCharacterBounds(0).getMinY();
int insertPoint = getInsertionPoint(0,zeroY);
int lastInsertPoint = getInsertionPoint(0,zeroY + h);
if(lastInsertPoint < 0){
lastInsertPoint = textarea.getLength();
}
List<MyTextArea.Line> lines = textarea.getLines();
ObservableList<Node> nums = lineNumbers.getChildren();
int index =0,nlen = nums.size();
double minY = -100000000;
for(int l=0,end = lines.size();l<end;l++){
MyTextArea.Line line = lines.get(l);
if(line.end <insertPoint)continue;
if(line.begin > lastInsertPoint)break;
MyTextArea.YRange ly = textarea.getLineYRange(l);
if(ly.end < 0)continue;
if(ly.start > y + h || minY > y+h){
break;
}
Text text;
if(index < nlen){
text = (Text)nums.get(index);
}else{
text = new Text();
text.setManaged(false);
nums.add(text);
}
index++;
text.setFont(textarea.getFont());
text.setText(Integer.toString(l+1));
double nextY = ly.start+3;
double yy = Math.max(nextY, minY);
text.relocate(0, yy);
minY = Math.max(text.getBoundsInParent().getMaxY() ,ly.end);
}
if(index < nlen){
nums.remove(index, nlen);
}
}
}
package nodamushi.jfx;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
/**
*
* @author nodamushi
*/
public class TextAreaApplication extends Application{
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
MyTextArea area = new MyTextArea();
area.setWrapText(true);
area.setPrefSize(500, 500);
Scene scene = new Scene(area);
stage.setScene(scene);
stage.show();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment