Hex8Spinner.java
     1: //========================================================================================
     2: //  Hex8Spinner.java
     3: //    en:Eight character hexadecimal spinner
     4: //    ja:8桁16進数スピナー
     5: //  Copyright (C) 2003-2024 Makoto Kamada
     6: //
     7: //  This file is part of the XEiJ (X68000 Emulator in Java).
     8: //  You can use, modify and redistribute the XEiJ if the conditions are met.
     9: //  Read the XEiJ License for more details.
    10: //  https://stdkmd.net/xeij/
    11: //========================================================================================
    12: 
    13: //----------------------------------------------------------------------------------------
    14: //  操作できるビットのマスクを指定する
    15: //  選択できる値のヒントを指定する
    16: //  ↑キーを押すか▲ボタンをクリックすると直前の小さい方の値が選択される
    17: //  ↓キーを押すか▼ボタンをクリックすると直後の大きい方の値が選択される
    18: //----------------------------------------------------------------------------------------
    19: 
    20: package xeij;
    21: 
    22: 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
    23: import java.lang.*;  //Boolean,Character,Class,Comparable,Double,Exception,Float,IllegalArgumentException,Integer,Long,Math,Number,Object,Runnable,SecurityException,String,StringBuilder,System
    24: import java.util.*;  //ArrayList,Arrays,Calendar,GregorianCalendar,HashMap,Map,Map.Entry,Timer,TimerTask,TreeMap
    25: 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
    26: import javax.swing.border.*;  //Border,CompoundBorder,EmptyBorder,EtchedBorder,LineBorder,TitledBorder
    27: import javax.swing.event.*;  //CaretListener,ChangeEvent,ChangeListener,DocumentEvent,DocumentListener,ListSelectionListener
    28: import javax.swing.text.*;  //AbstractDocument,BadLocationException,DefaultCaret,Document,DocumentFilter,JTextComponent,ParagraphView,Style,StyleConstants,StyleContext,StyledDocument
    29: 
    30: public class Hex8Spinner extends JSpinner implements ChangeListener, DocumentListener {
    31: 
    32:   public Hex8SpinnerModel model;  //モデル
    33:   public JTextField editor;  //エディタ
    34: 
    35:   //コンストラクタ
    36:   @SuppressWarnings ("this-escape") public Hex8Spinner (int newValue, int newMask, boolean newReverse) {
    37:     model = new Hex8SpinnerModel (newValue, newMask, newReverse);
    38:     editor = new JTextField (XEiJ.fmtHex8 (newValue & newMask), 8);
    39:     editor.setHorizontalAlignment (JTextField.RIGHT);
    40:     //editor.setFont (LnF.lnfMonospacedFont);
    41:     AbstractDocument document = (AbstractDocument) editor.getDocument ();
    42:     document.setDocumentFilter (new Hex8DocumentFilter (newMask));
    43:     setBorder (new LineBorder (new Color (LnF.lnfRGB[10]), 1));  //[this-escape]
    44:     ComponentFactory.setFixedSize (this, 32 + (LnF.lnfFontSize * 2 / 3) * 8, LnF.lnfFontSize + 4);
    45:     setModel (model);
    46:     setEditor (editor);
    47:     model.addChangeListener (this);
    48:     document.addDocumentListener (this);
    49:   }
    50: 
    51:   //モデルのチェンジリスナー
    52:   //  モデルに値が設定されたときに呼び出される
    53:   //  モデルの値をエディタに設定する
    54:   //  エディタのドキュメントリスナーの中ではドキュメントがロックされていて書き換えることができない
    55:   //  もともと書き換える必要がないので何もしなくてよいのだが、IllegalStateExceptionが出るのは困る
    56:   //  ロックされているかどうか直接調べる方法が思い付かなかったのでIllegalStateExceptionを無視することにした
    57:   //  他のチェンジリスナーがモデルから値を読み出す分には問題ない
    58:   @Override public void stateChanged (ChangeEvent ce) {
    59:     try {
    60:       editor.setText (XEiJ.fmtHex8 (model.getIntValue ()));
    61:     } catch (IllegalStateException ise) {
    62:     }
    63:   }
    64: 
    65:   //エディタのドキュメントリスナー
    66:   //  エディタの値が書き換えられたときに呼び出される
    67:   //  エディタの値をモデルに設定する
    68:   @Override public void changedUpdate (DocumentEvent de) {
    69:   }
    70:   @Override public void insertUpdate (DocumentEvent de) {
    71:     model.setIntValue ((int) Long.parseLong (editor.getText (), 16));  //0x80000000~0xffffffffは直接intにできない
    72:   }
    73:   @Override public void removeUpdate (DocumentEvent de) {
    74:   }
    75: 
    76:   //値の操作
    77:   public int getIntValue () {
    78:     return model.getIntValue ();
    79:   }
    80:   public void setIntValue (int newValue) {
    81:     model.setIntValue (newValue);
    82:   }
    83: 
    84:   //ヒントの操作
    85:   public void setHintArray (int[] array, int count) {
    86:     model.setHintArray (array, count);
    87:   }
    88:   public int getHintIndex () {
    89:     return model.getHintIndex ();
    90:   }
    91:   public void setHintIndex (int index) {
    92:     model.setHintIndex (index);
    93:   }
    94: 
    95: 
    96: 
    97:   //========================================================================================
    98:   //$$XFL Hex8DocumentFilter
    99:   //  8桁16進数ドキュメントフィルタ
   100:   //  常に上書き
   101:   //  削除はできない
   102:   //  a~fをA~Fに変換する
   103:   //  MaskFormatterだと偶数のみのような制約が作れないので自前のフィルタを作った
   104:   public static class Hex8DocumentFilter extends DocumentFilter {
   105:     public int mask;
   106:     public char[] buffer;
   107:     public Hex8DocumentFilter (int newMask) {
   108:       mask = newMask;
   109:       buffer = new char[8];
   110:     }
   111:     @Override public void insertString (DocumentFilter.FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException {
   112:        replace (fb, offset, 0, string, attr);
   113:      }
   114:     @Override public void remove (DocumentFilter.FilterBypass fb, int offset, int length) throws BadLocationException {
   115:      }
   116:     @Override public void replace (DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
   117:        length = text.length ();
   118:        int start = offset;
   119:        for (int i = 0; offset < 8 && i < length; i++) {
   120:          int x = Character.digit (text.charAt (i), 16);
   121:          if (x >= 0) {
   122:            buffer[offset] = XEiJ.fmtHexc (mask >>> (7 - offset) * 4 & x);
   123:            offset++;
   124:          }
   125:        }
   126:        fb.replace (start, offset - start, String.valueOf (buffer, start, offset - start), attrs);
   127:      }
   128:   }  //class Hex8DocumentFilter
   129: 
   130: 
   131: 
   132:   //========================================================================================
   133:   //$$XSM Hex8SpinnerModel
   134:   //  8桁16進数スピナーモデル
   135:   public static class Hex8SpinnerModel extends AbstractSpinnerModel {
   136: 
   137:     public int value;  //現在の値
   138:     public int mask;  //マスク
   139:     public boolean reverse;  //逆回転
   140:     public int[] hintArray;  //ヒントの配列
   141:     public int hintCount;  //ヒントの数。負数=ヒントが設定されていない
   142:     public int hintIndex;  //ヒントのインデックス。負数=現在の値がヒントの配列にない
   143: 
   144:     //コンストラクタ
   145:     public Hex8SpinnerModel (int newValue, int newMask, boolean newReverse) {
   146:       value = newValue & newMask;
   147:       mask = newMask;
   148:       reverse = newReverse;
   149:       hintArray = new int[0];
   150:       hintCount = 0;
   151:       hintIndex = -1;
   152:     }
   153: 
   154:     //value = getNextValue ()
   155:     //  直前の小さい方の値を返す
   156:     //  ↑キーを押すか▲ボタンをクリックしたときに呼び出される
   157:     //  ノーマル
   158:     //    現在の値がヒントの配列にないとき
   159:     //      マスクが0のビットを避けてインクリメントした値を返す
   160:     //    現在の値がヒントの配列にあるとき
   161:     //      現在の値がヒントの配列の末尾のとき
   162:     //        現在の値を返す
   163:     //      現在の値がヒントの配列の末尾でないとき
   164:     //        ヒントの配列にある直後の値を返す
   165:     //  リバース
   166:     //    現在の値がヒントの配列にないとき
   167:     //      マスクが0のビットを避けてデクリメントした値を返す
   168:     //    現在の値がヒントの配列にあるとき
   169:     //      現在の値がヒントの配列の先頭のとき
   170:     //        現在の値を返す
   171:     //      現在の値がヒントの配列の先頭でないとき
   172:     //        ヒントの配列にある直前の値を返す
   173:     @Override public Object getNextValue () {
   174:       if (reverse) {
   175:         return (hintCount < 0 || hintIndex < 0 ? (value & mask) - 1 & mask :  //マスクが0のビットを避けてデクリメントした値
   176:                 hintIndex == 0 ? value :  //現在の値
   177:                 hintArray[hintIndex - 1]);  //ヒントの配列にある直前の値
   178:       } else {
   179:         return (hintCount < 0 || hintIndex < 0 ? (value | ~mask) + 1 & mask :  //マスクが0のビットを避けてインクリメントした値
   180:                 hintIndex == hintCount - 1 ? value :  //現在の値
   181:                 hintArray[hintIndex + 1]);  //ヒントの配列にある直後の値
   182:       }
   183:     }
   184:     //value = getPreviousValue ()
   185:     //  直後の大きい方の値を返す
   186:     //  ↓キーを押すか▼ボタンをクリックしたときに呼び出される
   187:     //  ノーマル
   188:     //    現在の値がヒントの配列にないとき
   189:     //      マスクが0のビットを避けてデクリメントした値を返す
   190:     //    現在の値がヒントの配列にあるとき
   191:     //      現在の値がヒントの配列の先頭のとき
   192:     //        現在の値を返す
   193:     //      現在の値がヒントの配列の先頭でないとき
   194:     //        ヒントの配列にある直前の値を返す
   195:     //  リバース
   196:     //    現在の値がヒントの配列にないとき
   197:     //      マスクが0のビットを避けてインクリメントした値を返す
   198:     //    現在の値がヒントの配列にあるとき
   199:     //      現在の値がヒントの配列の末尾のとき
   200:     //        現在の値を返す
   201:     //      現在の値がヒントの配列の末尾でないとき
   202:     //        ヒントの配列にある直後の値を返す
   203:     @Override public Object getPreviousValue () {
   204:       if (reverse) {
   205:         return (hintCount < 0 || hintIndex < 0 ? (value | ~mask) + 1 & mask :  //マスクが0のビットを避けてインクリメントした値
   206:                 hintIndex == hintCount - 1 ? value :  //現在の値
   207:                 hintArray[hintIndex + 1]);  //ヒントの配列にある直後の値
   208:       } else {
   209:         return (hintCount < 0 || hintIndex < 0 ? (value & mask) - 1 & mask :  //マスクが0のビットを避けてデクリメントした値
   210:                 hintIndex == 0 ? value :  //現在の値
   211:                 hintArray[hintIndex - 1]);  //ヒントの配列にある直前の値
   212:       }
   213:     }
   214:     //value = getValue ()
   215:     //  現在の値を文字列で返す
   216:     @Override public Object getValue () {
   217:       return XEiJ.fmtHex8 (getIntValue ());
   218:     }
   219:     //setValue (newValue)
   220:     //  現在の値を文字列で設定する
   221:     @Override public void setValue (Object newValue) {
   222:       setIntValue (newValue instanceof Integer ? ((Integer) newValue).intValue () :
   223:                    newValue instanceof String ? (int) Long.parseLong ((String) newValue, 16) : 0);  //0x80000000~0xffffffffは直接intにできない
   224:     }
   225: 
   226:     //value = getIntValue ()
   227:     //  現在の値を整数で返す
   228:     public int getIntValue () {
   229:       return value;
   230:     }
   231:     //setIntValue (newValue)
   232:     //  現在の値を整数で設定する
   233:     //  値はマスクされる
   234:     //  現在の値がヒントの配列にあるときはヒントのインデックスも設定する
   235:     //  現在の値が変化したときはチェンジリスナーを呼び出す
   236:     public void setIntValue (int newValue) {
   237:       newValue &= mask;
   238:       if (value != newValue) {  //値が変化する
   239:         value = newValue;
   240:         hintIndex = Arrays.binarySearch (hintArray, 0, hintCount, value);  //ヒントのインデックス。現在の値がヒントの配列にないときは負数
   241:         fireStateChanged ();
   242:       }
   243:     }
   244: 
   245:     //setHintArray (array, count)
   246:     //  ヒントの配列を設定する
   247:     //  現在の値は操作しない
   248:     //  配列はコピーされるので、設定後に元の配列に加えられた変更は反映されない
   249:     //  配列はマスク済みで昇順にソートされていなければならない
   250:     //  配列に同じ値が複数あってはならない
   251:     public void setHintArray (int[] array, int count) {
   252:       if (hintArray == null || hintArray.length < count) {  //ヒントの配列が確保されていないか現在の配列に収まらないとき
   253:         hintArray = new int[count];  //ヒントの配列を作り直す
   254:       }
   255:       System.arraycopy (array, 0, hintArray, 0, count);  //ヒントの配列をコピーする
   256:       hintCount = count;
   257:       hintIndex = Arrays.binarySearch (hintArray, 0, hintCount, value);  //ヒントのインデックス。現在の値がヒントの配列にないときは負数
   258:     }
   259: 
   260:     //index = getHintIndex ()
   261:     //  ヒントのインデックスを返す
   262:     public int getHintIndex () {
   263:       return hintIndex;
   264:     }
   265:     //setHingIndex (index)
   266:     //  ヒントのインデックスを設定する
   267:     public void setHintIndex (int index) {
   268:       if (0 <= index && index < hintCount &&
   269:           hintIndex != index) {
   270:         hintIndex = index;
   271:         int newValue = hintArray[index];
   272:         if (value != newValue) {  //値が変化する
   273:           value = newValue;
   274:           fireStateChanged ();
   275:         }
   276:       }
   277:     }
   278: 
   279:   }  //class Hex8SpinnerModel
   280: 
   281: 
   282: 
   283: }  //class Hex8Spinner
   284: 
   285: 
   286: