xeij/ScrollTextArea.java
//========================================================================================
//  ScrollTextArea.java
//    en:Text area with scroll bars -- It is a modified JScrollPage that has a JTextArea as the view.
//    ja:スクロールバー付きテキストエリア -- JTextAreaをビューに持つJScrollPaneです。
//  Copyright (C) 2003-2025 Makoto Kamada
//
//  This file is part of the XEiJ (X68000 Emulator in Java).
//  You can use, modify and redistribute the XEiJ if the conditions are met.
//  Read the XEiJ License for more details.
//  https://stdkmd.net/xeij/
//========================================================================================

//----------------------------------------------------------------------------------------
//  追加機能
//    背景にグリッドを表示できる
//    ハイライトエリアを表示できる
//    ハイライトカーソルを表示できる
//    アンダーラインカーソルを表示できる
//----------------------------------------------------------------------------------------

package xeij;

import java.awt.*;  //BasicStroke,BorderLayout,BoxLayout,Color,Component,Container,Cursor,Desktop,Dimension,Font,Frame,Graphics,Graphics2D,GraphicsDevice,GraphicsEnvironment,GridLayout,Image,Insets,Paint,Point,Rectangle,RenderingHints,Robot,Shape,Stroke,TexturePaint,Toolkit
import java.awt.event.*;  //ActionEvent,ActionListener,ComponentAdapter,ComponentEvent,ComponentListener,FocusAdapter,FocusEvent,FocusListener,InputEvent,KeyAdapter,KeyEvent,KeyListener,MouseAdapter,MouseEvent,MouseListener,MouseMotionAdapter,MouseWheelEvent,WindowAdapter,WindowEvent,WindowListener,WindowStateListener
import java.awt.geom.*;  //AffineTransform,GeneralPath,Point2D,Rectangle2D
import java.awt.image.*;  //BufferedImage,DataBuffer,DataBufferByte,DataBufferInt,IndexColorModel
import javax.swing.*;  //AbstractSpinnerModel,Box,ButtonGroup,DefaultListModel,ImageIcon,JApplet,JButton,JCheckBox,JCheckBoxMenuItem,JDialog,JFileChooser,JFrame,JLabel,JList,JMenu,JMenuBar,JMenuItem,JPanel,JRadioButton,JScrollPane,JSpinner,JTextArea,JTextField,JTextPane,JViewport,ScrollPaneConstants,SpinnerListModel,SpinnerNumberModel,SwingConstants,SwingUtilities,UIManager,UIDefaults,UnsupportedLookAndFeelException
import javax.swing.event.*;  //CaretListener,ChangeEvent,ChangeListener,DocumentEvent,DocumentListener,ListSelectionListener
import javax.swing.text.*;  //AbstractDocument,BadLocationException,DefaultCaret,Document,DocumentFilter,JTextComponent,ParagraphView,Style,StyleConstants,StyleContext,StyledDocument

public class ScrollTextArea extends JScrollPane {

  private boolean opaqueOn;  //true=背景を表示する
  private boolean gridOn;  //true=グリッドを表示する
  private boolean highlightCursorOn;  //true=ハイライトカーソルを表示する
  private boolean underlineCursorOn;  //true=アンダーラインカーソルを表示する

  private Color foregroundColor;  //文字の色
  private Color backgroundColor;  //背景の色
  private Color rowGridColor;  //行グリッドの色
  private Color columnGridColor;  //列グリッドの色
  private Color tabColumnGridColor;  //タブ列グリッドの色
  private Color highlightAreaColor;  //ハイライトエリアの色
  private Color highlightCursorColor;  //ハイライトカーソルの色
  private Color underlineCursorColor;  //アンダーラインカーソルの色

  private Paint gridPaint;  //グリッドのテクスチャペイント
  private int highlightAreaStart;  //ハイライトエリアの開始位置
  private int highlightAreaEnd;  //ハイライトエリアの終了位置。-1=ハイライトエリアなし
  private Stroke underlineCursorStroke;  //アンダーラインカーソルのストローク

  private JTextArea textArea;

  private int fontWidth;
  private int fontHeight;
  private int marginTop;
  private int marginLeft;

  private DefaultCaret caret;

  //コンストラクタ
  //  super.setOpaque (false)で警告this-escapeが出るので@SuppressWarnings ("this-escape")を追加
  @SuppressWarnings ("this-escape") public ScrollTextArea () {
    super ();

    opaqueOn = true;
    gridOn = true;
    highlightCursorOn = false;
    underlineCursorOn = false;

    foregroundColor      = new Color (LnF.lnfRGB[14]);
    backgroundColor      = new Color (LnF.lnfRGB[0]);
    rowGridColor         = new Color (LnF.lnfRGB[2]);
    columnGridColor      = new Color (LnF.lnfRGB[1]);
    tabColumnGridColor   = new Color (LnF.lnfRGB[2]);
    highlightAreaColor   = new Color (LnF.lnfRGB[3]);
    highlightCursorColor = new Color (LnF.lnfRGB[6]);
    underlineCursorColor = new Color (LnF.lnfRGB[10]);

    gridPaint = backgroundColor;
    highlightAreaStart = highlightAreaEnd = -1;
    underlineCursorStroke = new BasicStroke (1.0F,
                                             BasicStroke.CAP_SQUARE,
                                             BasicStroke.JOIN_MITER,
                                             10.0F,
                                             new float[] { 0.0F, 2.0F },
                                             0.0F);

    textArea = new JTextArea () {
      //描画
      @Override public void paintComponent (Graphics g) {
        Graphics2D g2 = (Graphics2D) g;
        int width = getWidth ();
        int height = getHeight ();
        //背景
        if (opaqueOn) {
          g2.setPaint (gridOn ? gridPaint : backgroundColor);
          g2.fillRect (0, 0, width, height);
        }
        paintAfterBackground (this, g2);
        //ハイライトエリア
        if (0 <= highlightAreaEnd) {
          try {
            //Rectangle startRect = modelToView (highlightAreaStart).getBounds ();  //modelToView2Dは9から
            Rectangle startRect = modelToView2D (highlightAreaStart).getBounds ();  //modelToView2Dは9から
            //Rectangle endRect = modelToView (highlightAreaEnd).getBounds ();  //modelToView2Dは9から
            Rectangle endRect = modelToView2D (highlightAreaEnd).getBounds ();  //modelToView2Dは9から
            Insets margin = getMargin ();
            g2.setPaint (highlightAreaColor);
            g2.fillRect (margin.left, startRect.y,
                         width - margin.left - margin.right, endRect.y - startRect.y + endRect.height);
          } catch (BadLocationException ble) {
          }
        }
        //ラインカーソル
        if (highlightCursorOn || underlineCursorOn) {
          try {
            //Rectangle caretRect = modelToView (getCaretPosition ()).getBounds ();  //modelToView2Dは9から
            Rectangle caretRect = modelToView2D (getCaretPosition ()).getBounds ();  //modelToView2Dは9から
            Insets margin = getMargin ();
            if (highlightCursorOn) {
              g2.setPaint (highlightCursorColor);
              g2.fillRect (margin.left, caretRect.y,
                           width - margin.left - margin.right, caretRect.height);
            }
            if (underlineCursorOn) {
              g2.setPaint (underlineCursorColor);
              g2.setStroke (underlineCursorStroke);
              g2.drawLine (margin.left, caretRect.y + caretRect.height - 1,
                           width - margin.right - 1, caretRect.y + caretRect.height - 1);
            }
          } catch (BadLocationException ble) {
          }
        }
        //テキスト
        super.paintComponent (g);
        paintAfterText (this, g2);
      }
    };
    textArea.setOpaque (false);  //背景色を塗らせない
    super.setOpaque (false);

    textArea.setForeground (foregroundColor);
    textArea.setSelectionColor (new Color (LnF.lnfRGB[7]));  //選択領域の背景の色
    //textArea.setSelectedTextColor (new Color (LnF.lnfRGB[14]));  //選択領域のテキストの色

    Font font = textArea.getFont ();
    fontWidth = font.getSize () + 1 >> 1;
    fontHeight = textArea.getFontMetrics (font).getHeight ();
    Insets margin = textArea.getMargin ();
    marginTop = margin.top;
    marginLeft = margin.left;

    createGrid ();

    caret = new DefaultCaret () {
      @Override protected void damage (Rectangle r) {
        if (r != null) {
          if (highlightCursorOn || underlineCursorOn) {
            x = 0;
            y = r.y;
            width = textArea.getWidth ();
            height = r.height;
            textArea.repaint ();
          } else {
            super.damage (r);
          }
        }
      }
    };
    caret.setBlinkRate (500);
    textArea.setCaret (caret);

    getViewport ().setView (textArea);
  }

  //paintAfterBackground (textArea, g2)
  //  背景を描画した後に呼び出される
  public void paintAfterBackground (JTextArea textArea, Graphics2D g2) {
  }

  //paintAfterText (textArea, g2)
  //  テキストを描画した後に呼び出される
  public void paintAfterText (JTextArea textArea, Graphics2D g2) {
  }

  //グリッドを作る
  //  フォントの右下にグリッドを表示する
  //  フォント(6,13),マージン(3,3,3,3)のとき
  //  ..t.....c.....c.....c.....c.....c.....c.....c...
  //  ................................................
  //  r.r.r.r.r.r.r.r.r.r.r.r.r.r.r.r.r.r.r.r.r.r.r.r.
  //  ..t.....c.....c.....c.....c.....c.....c.....c...
  //  ................................................
  //  ..t.....c.....c.....c.....c.....c.....c.....c...
  //  ................................................
  //  ..t.....c.....c.....c.....c.....c.....c.....c...
  //  ................................................
  //  ..t.....c.....c.....c.....c.....c.....c.....c...
  //  ................................................
  //  ..t.....c.....c.....c.....c.....c.....c.....c...
  //  ................................................
  private void createGrid () {
    int backgroundRGB = backgroundColor.getRGB ();
    int rowGridRGB = rowGridColor.getRGB ();
    int columnGridRGB = columnGridColor.getRGB ();
    int tabColumnGridRGB = tabColumnGridColor.getRGB ();
    int imageWidth = fontWidth * 8;
    BufferedImage image = new BufferedImage (imageWidth, fontHeight, BufferedImage.TYPE_INT_RGB);
    int[] bitmap = ((DataBufferInt) image.getRaster ().getDataBuffer ()).getData ();
    for (int y = 0; y < fontHeight; y++) {
      for (int x = 0; x < imageWidth; x++) {
        bitmap[(marginLeft + x) % imageWidth + imageWidth * ((marginTop + y) % fontHeight)] =
          (((y ^ fontHeight) & 1) == 0 ? backgroundRGB :
           y == fontHeight - 1 ? ((x % fontWidth ^ fontWidth) & 1) != 0 ? rowGridRGB : backgroundRGB :
           x == imageWidth - 1 ? tabColumnGridRGB : x % fontWidth == fontWidth - 1 ? columnGridRGB : backgroundRGB);
      }
    }
    gridPaint = new TexturePaint (image, new Rectangle (0, 0, imageWidth, fontHeight));
  }

  //サイズ
  @Override public void setMaximumSize (Dimension size) {
    super.setMaximumSize (size);
    getViewport ().setMaximumSize (size);
  }
  @Override public void setMinimumSize (Dimension size) {
    super.setMinimumSize (size);
    getViewport ().setMinimumSize (size);
  }
  @Override public void setPreferredSize (Dimension size) {
    super.setPreferredSize (size);
    getViewport ().setPreferredSize (size);
  }

  //マージン
  public Insets getMargin () {
    return textArea.getMargin ();
  }
  public void setMargin (Insets m) {
    textArea.setMargin (m);
    int t = m.top;
    int l = m.left;
    if (marginTop != t || marginLeft != l) {
      marginTop = t;
      marginLeft = l;
      createGrid ();
    }
  }

  //背景
  @Override public boolean isOpaque () {
    return opaqueOn;
  }
  @Override public void setOpaque (boolean opaque) {
    opaqueOn = opaque;
  }

  //色
  @Override public Color getForeground () {
    return foregroundColor;
  }
  @Override public void setForeground (Color color) {
    foregroundColor = color;
    if (textArea != null) {  //スーパークラスのコンストラクタからの呼び出しではない
      textArea.setForeground (color);
    }
  }
  @Override public Color getBackground () {
    return backgroundColor;
  }
  @Override public void setBackground (Color color) {
    backgroundColor = color;
    if (textArea != null) {  //スーパークラスのコンストラクタからの呼び出しではない
      createGrid ();
    }
  }

  //フォント
  @Override public Font getFont () {
    if (textArea != null) {  //スーパークラスのコンストラクタからの呼び出しではない
      return textArea.getFont ();
    }
    return super.getFont ();
  }
  @Override public void setFont (Font font) {
    if (textArea != null) {  //スーパークラスのコンストラクタからの呼び出しではない
      textArea.setFont (font);
      int w = font.getSize () + 1 >> 1;
      int h = textArea.getFontMetrics (font).getHeight ();
      if (fontWidth != w || fontHeight != h) {
        fontWidth = w;
        fontHeight = h;
        createGrid ();
      }
    }
  }

  //イベント
  public void addCaretListener (CaretListener listener) {
    textArea.addCaretListener (listener);
  }
  public void removeCaretListener (CaretListener listener) {
    textArea.removeCaretListener (listener);
  }
  public void addDocumentListener (DocumentListener listener) {
    textArea.getDocument ().addDocumentListener (listener);
  }
  public void removeDocumentListener (DocumentListener listener) {
    textArea.getDocument ().removeDocumentListener (listener);
  }
  @Override public void addKeyListener (KeyListener listener) {
    textArea.addKeyListener (listener);
  }
  @Override public void removeKeyListener (KeyListener listener) {
    textArea.removeKeyListener (listener);
  }
  @Override public void addMouseListener (MouseListener listener) {
    textArea.addMouseListener (listener);
  }
  @Override public void removeMouseListener (MouseListener listener) {
    textArea.removeMouseListener (listener);
  }
  @Override public void addFocusListener (FocusListener listener) {
    textArea.addFocusListener (listener);
  }
  @Override public void removeFocusListener (FocusListener listener) {
    textArea.removeFocusListener (listener);
  }

  //折り返し
  public boolean getLineWrap () {
    return textArea.getLineWrap ();
  }
  public void setLineWrap (boolean wrap) {
    textArea.setLineWrap (wrap);
  }

  //キャレット
  public Color getCaretColor () {
    return textArea.getCaretColor ();
  }
  public void setCaretColor (Color color) {
    textArea.setCaretColor (color);
  }
  public int getCaretPosition () {
    return textArea.getCaretPosition ();
  }
  public void setCaretPosition (int pos) {
    textArea.setCaretPosition (pos);
  }
  public boolean isCaretVisible () {
    return caret.isVisible ();
  }
  public void setCaretVisible (boolean visible) {
    caret.setVisible (visible);
  }

  //テキスト
  public JTextArea getTextArea () {
    return textArea;
  }
  public String getText () {
    return textArea.getText ();
  }
  public void setText (String text) {
    highlightAreaEnd = -1;
    textArea.setText (text);
  }

  //編集
  public void append (String text) {
    highlightAreaEnd = -1;
    textArea.append (text);
  }
  public void insert (String text, int pos) {
    highlightAreaEnd = -1;
    textArea.insert (text, pos);
  }
  public void replaceRange (String text, int start, int end) {
    highlightAreaEnd = -1;
    textArea.replaceRange (text, start, end);
  }

  //選択
  public void selectAll () {
    textArea.selectAll ();
  }
  public String getSelectedText () {
    return textArea.getSelectedText ();
  }
  public int getSelectionStart () {
    return textArea.getSelectionStart ();
  }
  public int getSelectionEnd () {
    return textArea.getSelectionEnd ();
  }

  //モード
  public boolean isEditable () {
    return textArea.isEditable ();
  }
  public void setEditable (boolean editable) {
    textArea.setEditable (editable);
  }

  //グリッド
  public boolean isGridOn () {
    return gridOn;
  }
  public void setGridOn (boolean on) {
    gridOn = on;
  }
  public Color getRowGridColor () {
    return rowGridColor;
  }
  public void setRowGridColor (Color color) {
    rowGridColor = color;
    createGrid ();
  }
  public Color getColumnGridColor () {
    return columnGridColor;
  }
  public void setColumnGridColor (Color color) {
    columnGridColor = color;
    createGrid ();
  }
  public Color getTabColumnGridColor () {
    return tabColumnGridColor;
  }
  public void setTabColumnGridColor (Color color) {
    tabColumnGridColor = color;
    createGrid ();
  }

  //ハイライトエリア
  public void setHighlightArea (int start, int end) {
    highlightAreaStart = start;
    highlightAreaEnd = end;
  }

  //ハイライトカーソル
  public boolean isHighlightCursorOn () {
    return highlightCursorOn;
  }
  public void setHighlightCursorOn (boolean on) {
    highlightCursorOn = on;
  }
  public Color getHighlightCursorColor () {
    return highlightCursorColor;
  }
  public void setHighlightCursorColor (Color color) {
    highlightCursorColor = color;
  }

  //アンダーラインカーソル
  public boolean isUnderlineCursorOn () {
    return underlineCursorOn;
  }
  public void setUnderlineCursorOn (boolean on) {
    underlineCursorOn = on;
  }
  public Color getUnderlineCursorColor () {
    return underlineCursorColor;
  }
  public void setUnderlineCursorColor (Color color) {
    underlineCursorColor = color;
  }

}  //class ScrollTextArea