RestorableFrame.java
     1: //========================================================================================
     2: //  RestorableFrame.java
     3: //    en:Restorable frame -- It is a frame that you can easily save and restore the position, size and state.
     4: //    ja:リストアラブルフレーム -- 位置とサイズと状態の保存と復元が簡単にできるフレームです。
     5: //  Copyright (C) 2003-2023 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: //    初めて表示されたときインスタンスの位置とサイズと状態を復元する
    21: //      サイズは表示された後に復元しなければならない
    22: //    2回目以降はインスタンスの位置とサイズと状態を記憶する
    23: //  クラスメソッドでキーに対応するインスタンスの位置とサイズと状態を取り出して保存する
    24: //  インスタンスメソッドは変更しない
    25: //    JFrameをRestorableFrameに置き換えるとき変数宣言の型を変更しなくて済む
    26: //  最大化しているときは位置とサイズを保存しない
    27: //    最大化している状態で位置とサイズを保存してしまうと復元した後に元の大きさに戻せなくなる
    28: //    最大化した状態を復元するときは最大化する前の位置とサイズを復元してから最大化させる
    29: //  水平方向だけ最大化または垂直方向だけ最大化
    30: //    Windows7の場合、通常のウインドウはウインドウの枠を左ダブルクリックするとウインドウを水平方向だけ最大化または垂直方向だけ最大化できる
    31: //    しかし、JFrameには水平方向だけ最大化または垂直方向だけ最大化する機能がないらしい
    32: //    枠を左ダブルクリックしてもウインドウのサイズが変わらない
    33: //    Frame.getExtendedState()はMAXIMIZED_HORIZまたはFrame.MAXIMIZED_VERTを返さない
    34: //    Frame.setExtendedState()にMAXIMIZED_HORIZまたはFrame.MAXIMIZED_VERTを与えてもウインドウのサイズが変わらない
    35: //  Frame.getExtendedState()では全画面表示を判別できない
    36: //    GraphicsEnvironment.getLocalGraphicsEnvironment ().getDefaultScreenDevice ().getFullScreenWindow () == frameで判別する
    37: //----------------------------------------------------------------------------------------
    38: 
    39: package xeij;
    40: 
    41: 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
    42: 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
    43: import java.awt.image.*;  //BufferedImage,DataBuffer,DataBufferByte,DataBufferInt,IndexColorModel
    44: import java.lang.*;  //Boolean,Character,Class,Comparable,Double,Exception,Float,IllegalArgumentException,Integer,Long,Math,Number,Object,Runnable,SecurityException,String,StringBuilder,System
    45: import java.util.*;  //ArrayList,Arrays,Calendar,GregorianCalendar,HashMap,Map,Map.Entry,Timer,TimerTask,TreeMap
    46: 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
    47: 
    48: public class RestorableFrame extends JFrame {
    49: 
    50:   //クラス変数
    51:   protected static HashMap<String,RestorableFrame> rfmKeyToFrame = new HashMap<String,RestorableFrame> ();
    52:   protected static HashMap<String,Rectangle> rfmKeyToBounds = new HashMap<String,Rectangle> ();
    53:   protected static HashMap<String,Integer> rfmKeyToState = new HashMap<String,Integer> ();
    54:   protected static HashMap<String,Boolean> rfmKeyToOpened = new HashMap<String,Boolean> ();  //ウインドウが開いているかどうか。これはまとめて保存しやすくするための情報。そもそもインスタンスを作らなければ開けないのだから実際に開く処理はインスタンスを作る側で行う
    55: 
    56:   //クラスメソッド
    57: 
    58:   //bounds = rfmGetBounds (key)
    59:   //  インスタンスの位置とサイズを読み出す
    60:   public static Rectangle rfmGetBounds (String key) {
    61:     return rfmKeyToBounds.containsKey (key) ? rfmKeyToBounds.get (key) : new Rectangle ();
    62:   }  //rfmGetBounds(String)
    63: 
    64:   //rfmSetBounds (key, bounds)
    65:   //  インスタンスの位置とサイズを指定する
    66:   public static void rfmSetBounds (String key, Rectangle bounds) {
    67:     rfmKeyToBounds.put (key, bounds);  //新しい位置とサイズ
    68:     if (rfmKeyToFrame.containsKey (key)) {  //インスタンスが既にあるとき
    69:       RestorableFrame frame = rfmKeyToFrame.get (key);  //インスタンス
    70:       if (frame.isShowing ()) {  //表示されている
    71:         frame.setLocation (bounds.x, bounds.y);  //位置を復元する
    72:         if (frame.rfmResizable) {
    73:           if (bounds.width > 0 && bounds.height > 0) {  //サイズが保存されているとき
    74:             frame.setSize (bounds.width, bounds.height);  //サイズを復元する
    75:           }
    76:         }
    77:       }
    78:     }
    79:   }  //rfmSetBounds(String,Rectangle)
    80: 
    81:   //state = rfmGetState (key)
    82:   //  インスタンスの状態を読み出す
    83:   public static int rfmGetState (String key) {
    84:     return rfmKeyToState.containsKey (key) ? rfmKeyToState.get (key) : Frame.NORMAL;
    85:   }  //rfmGetState(String)
    86: 
    87:   //rfmSetState (key, state)
    88:   //  インスタンスの状態を指定する
    89:   public static void rfmSetState (String key, int state) {
    90:     rfmKeyToState.put (key, state);  //新しい状態
    91:     if (rfmKeyToFrame.containsKey (key)) {  //インスタンスが既にあるとき
    92:       RestorableFrame frame = rfmKeyToFrame.get (key);  //インスタンス
    93:       if (frame.isShowing ()) {  //表示されている
    94:         frame.setExtendedState (state);  //状態を復元する
    95:       }
    96:     }
    97:   }  //rfmSetState(String,int)
    98: 
    99:   //opened = rfmGetOpened (key)
   100:   //  ウインドウが開いているかどうかを読み出す
   101:   public static boolean rfmGetOpened (String key) {
   102:     return rfmKeyToOpened.containsKey (key) && rfmKeyToOpened.get (key);
   103:   }  //rfmGetOpened(String)
   104: 
   105:   //rfmSetOpened (key, opened)
   106:   //  ウインドウが開いているかどうかを指定する
   107:   public static void rfmSetOpened (String key, boolean opened) {
   108:     rfmKeyToOpened.put (key, opened);
   109:     //フレームは操作しない
   110:   }  //rfmSetOpened(String,boolean)
   111: 
   112:   //image = rfmCapture (key)
   113:   //  ウインドウをキャプチャする
   114:   public static BufferedImage rfmCapture (String key) {
   115:     if (rfmGetOpened (key)) {  //存在して開いている
   116:       rfmKeyToFrame.get (key).rfmUpdate ();  //レクタングルを更新する
   117:       Rectangle rect = rfmGetBounds (key);
   118:       if (!rect.isEmpty ()) {  //空ではない
   119:         try {
   120:           BufferedImage image = new Robot().createScreenCapture (rect);
   121:           return image;
   122:         } catch (Exception e) {
   123:         }
   124:       }
   125:     }
   126:     return null;
   127:   }  //rfmCapture(String)
   128: 
   129:   //インスタンス変数
   130:   private String rfmKey;  //キー
   131:   private boolean rfmRestored;  //false=まだ復元されていない,true=復元済み
   132:   private boolean rfmResizable;  //false=リサイズ不可
   133: 
   134:   //コンストラクタ
   135:   public RestorableFrame (String key, String title) {
   136:     this (key, title, true);
   137:   }
   138:   //  rfmKeyToFrame.put (key, this);
   139:   @SuppressWarnings ("this-escape") public RestorableFrame (String key, String title, boolean resizable) {
   140:     super (title, GraphicsEnvironment.getLocalGraphicsEnvironment ().getDefaultScreenDevice ().getDefaultConfiguration ());
   141:     rfmKey = key;  //キー
   142:     rfmRestored = false;  //まだ復元されていない
   143:     rfmResizable = resizable;
   144:     if (rfmKeyToFrame.containsKey (key)) {  //同じキーを持つインスタンスが既にあるとき
   145:       throw new IllegalArgumentException ("RestorableFrame: Key " + key + " is already used.");
   146:     }
   147:     rfmKeyToFrame.put (key, this);  //インスタンス
   148:     if (rfmKeyToBounds.containsKey (key)) {  //位置とサイズが既に指定されているとき
   149:       rfmRestoreBounds (rfmKeyToBounds.get (key));  //位置とサイズを復元する
   150:     } else {  //位置とサイズがまだ指定されていないとき
   151:       rfmKeyToBounds.put (key, new Rectangle ());  //ダミーの位置とサイズ
   152:     }
   153:     if (rfmKeyToState.containsKey (key)) {  //状態が既に指定されているとき
   154:       setExtendedState (rfmKeyToState.get (key));  //状態を復元する
   155:     } else {  //状態がまだ指定されていないとき
   156:       rfmKeyToState.put (key, Frame.NORMAL);  //ダミーの状態
   157:     }
   158:     //コンポーネントリスナー
   159:     addComponentListener (new ComponentAdapter () {
   160:       @Override public void componentMoved (ComponentEvent ce) {
   161:         rfmUpdate ();  //位置とサイズと状態を復元または記憶する
   162:       }
   163:       @Override public void componentResized (ComponentEvent ce) {
   164:         rfmUpdate ();  //位置とサイズと状態を復元または記憶する
   165:       }
   166:     });
   167:     //ウインドリスナー
   168:     addWindowListener (new WindowAdapter () {
   169:       @Override public void windowClosing (WindowEvent we) {
   170:         //HIDE_ON_CLOSEのときclosedは呼び出されないがclosingは呼び出される
   171:         rfmKeyToOpened.put (rfmKey, false);
   172:       }
   173:       @Override public void windowOpened (WindowEvent we) {
   174:         rfmKeyToOpened.put (rfmKey, true);
   175:       }
   176:     });
   177:     //ウインドウステートリスナー
   178:     addWindowStateListener (new WindowStateListener () {
   179:       @Override public void windowStateChanged (WindowEvent we) {
   180:         rfmUpdate ();  //位置とサイズと状態を復元または記憶する
   181:       }
   182:     });
   183:   }  //new RestorableFrame(String,String,GraphicsConfiguration)
   184: 
   185:   //rfmUpdate ()
   186:   //  位置とサイズと状態を復元または記憶する
   187:   private void rfmUpdate () {
   188:     if (!isShowing ()) {  //表示されていない
   189:       return;
   190:     }
   191:     for (GraphicsDevice gd : GraphicsEnvironment.getLocalGraphicsEnvironment ().getScreenDevices ()) {
   192:       if (gd.getFullScreenWindow () == this) {  //全画面表示
   193:         return;
   194:       }
   195:     }
   196:     if (!rfmRestored) {  //まだ復元されていない
   197:       rfmRestored = true;  //復元済み
   198:       rfmRestoreBounds (rfmKeyToBounds.get (rfmKey));  //位置とサイズを復元する
   199:       setExtendedState (rfmKeyToState.get (rfmKey));  //状態を復元する
   200:     } else {  //復元済み
   201:       Point p = getLocationOnScreen ();  //位置
   202:       Dimension d = getSize ();  //サイズ
   203:       int state = getExtendedState ();  //状態
   204:       //位置とサイズを記憶する
   205:       Rectangle bounds = rfmKeyToBounds.get (rfmKey);  //位置とサイズ
   206:       if ((state & (Frame.ICONIFIED | Frame.MAXIMIZED_HORIZ)) == 0) {  //アイコン化または水平方向に最大化されていない
   207:         //水平方向の要素を記憶する
   208:         bounds.x = p.x;
   209:         bounds.width = d.width;
   210:       }
   211:       if ((state & (Frame.ICONIFIED | Frame.MAXIMIZED_VERT)) == 0) {  //アイコン化または垂直方向に最大化されていない
   212:         //垂直方向の要素を記憶する
   213:         bounds.y = p.y;
   214:         bounds.height = d.height;
   215:       }
   216:       //状態を記憶する
   217:       rfmKeyToState.put (rfmKey, state);  //状態
   218:     }  //if まだ復元されていない/復元済み
   219:   }  //rfmUpdate()
   220: 
   221:   //rfmRestoreBounds (bounds)
   222:   //  位置とサイズを復元する
   223:   //  保存後にマルチスクリーン環境が変化して画面外に復元されると困るので、
   224:   //  ウインドウの上端の64x16ピクセルを含む画面がなければデフォルト画面の左上に復元する
   225:   private void rfmRestoreBounds (Rectangle bounds) {
   226:     Rectangle location = bounds;  //復元する位置
   227:   test:
   228:     {
   229:       Rectangle testBounds = new Rectangle (bounds.x, bounds.y, bounds.width, 16);
   230:       for (GraphicsDevice gd : GraphicsEnvironment.getLocalGraphicsEnvironment ().getScreenDevices ()) {
   231:         for (GraphicsConfiguration gc : gd.getConfigurations ()) {
   232:           Rectangle intersectionBounds = testBounds.intersection (gc.getBounds ());
   233:           if (intersectionBounds.width >= 64 && intersectionBounds.height >= 16) {
   234:             //ウインドウの上端の64x16ピクセルを含む画面が見つかった
   235:             break test;
   236:           }
   237:         }  //for gc
   238:       }  //for gd
   239:       //ウインドウの上端の64x16ピクセルを含む画面が見つからなかった
   240:       location = GraphicsEnvironment.getLocalGraphicsEnvironment ().getDefaultScreenDevice ().getDefaultConfiguration ().getBounds ();
   241:     }
   242:     setLocation (location.x, location.y);  //位置を復元する
   243:     if (rfmResizable) {
   244:       if (bounds.width > 0 && bounds.height > 0) {  //サイズが保存されているとき
   245:         setSize (bounds.width, bounds.height);  //サイズを復元する
   246:       }
   247:     }
   248:   }  //rfmRestoreBounds(Rectangle)
   249: 
   250: }  //class RestorableFrame
   251: 
   252: 
   253: