OSDN Git Service

ズームパネルのマウス表示判定のスケール対応
[charactermanaj/CharacterManaJ.git] / src / main / java / charactermanaj / ui / PreviewPanel.java
1 package charactermanaj.ui;
2
3 import static java.lang.Math.*;
4
5 import java.awt.BorderLayout;
6 import java.awt.Color;
7 import java.awt.Component;
8 import java.awt.Cursor;
9 import java.awt.Dimension;
10 import java.awt.FontMetrics;
11 import java.awt.Graphics;
12 import java.awt.Graphics2D;
13 import java.awt.GridBagConstraints;
14 import java.awt.GridBagLayout;
15 import java.awt.Insets;
16 import java.awt.Point;
17 import java.awt.Rectangle;
18 import java.awt.RenderingHints;
19 import java.awt.Toolkit;
20 import java.awt.event.ActionEvent;
21 import java.awt.event.ActionListener;
22 import java.awt.event.MouseAdapter;
23 import java.awt.event.MouseEvent;
24 import java.awt.event.MouseMotionAdapter;
25 import java.awt.event.MouseMotionListener;
26 import java.awt.event.MouseWheelEvent;
27 import java.awt.event.MouseWheelListener;
28 import java.awt.geom.Rectangle2D;
29 import java.awt.image.BufferedImage;
30 import java.beans.PropertyChangeEvent;
31 import java.beans.PropertyChangeListener;
32 import java.util.ArrayList;
33 import java.util.EventObject;
34 import java.util.LinkedList;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Properties;
38 import java.util.TreeSet;
39 import java.util.concurrent.Semaphore;
40 import java.util.logging.Level;
41 import java.util.logging.Logger;
42
43 import javax.swing.AbstractButton;
44 import javax.swing.BorderFactory;
45 import javax.swing.Box;
46 import javax.swing.Icon;
47 import javax.swing.JButton;
48 import javax.swing.JCheckBox;
49 import javax.swing.JComboBox;
50 import javax.swing.JLabel;
51 import javax.swing.JLayeredPane;
52 import javax.swing.JPanel;
53 import javax.swing.JScrollBar;
54 import javax.swing.JScrollPane;
55 import javax.swing.JSlider;
56 import javax.swing.JTextField;
57 import javax.swing.JToolBar;
58 import javax.swing.JViewport;
59 import javax.swing.OverlayLayout;
60 import javax.swing.SwingUtilities;
61 import javax.swing.Timer;
62 import javax.swing.UIManager;
63 import javax.swing.border.Border;
64 import javax.swing.event.ChangeEvent;
65 import javax.swing.event.ChangeListener;
66 import javax.swing.plaf.basic.BasicComboBoxEditor;
67
68 import charactermanaj.Main;
69 import charactermanaj.graphics.filters.BackgroundColorFilter;
70 import charactermanaj.graphics.filters.BackgroundColorFilter.BackgroundColorMode;
71 import charactermanaj.model.AppConfig;
72 import charactermanaj.ui.util.ScaleSupport;
73 import charactermanaj.ui.util.ScrollPaneDragScrollSupport;
74 import charactermanaj.util.LocalizedResourcePropertyLoader;
75 import charactermanaj.util.UIHelper;
76
77 /**
78  * プレビューパネル
79  *
80  * @author seraphy
81  */
82 public class PreviewPanel extends JPanel {
83
84         private static final long serialVersionUID = 1L;
85
86         protected static final String STRINGS_RESOURCE = "languages/previewpanel";
87
88
89         /**
90          * プレビューパネルの上部ツールバーの通知を受けるリスナ
91          *
92          * @author seraphy
93          */
94         public interface PreviewPanelListener {
95
96                 /**
97                  * 保存
98                  *
99                  * @param e
100                  */
101                 void savePicture(PreviewPanelEvent e);
102
103                 /**
104                  * コピー
105                  *
106                  * @param e
107                  */
108                 void copyPicture(PreviewPanelEvent e);
109
110                 /**
111                  * 背景色変更
112                  *
113                  * @param e
114                  */
115                 void changeBackgroundColor(PreviewPanelEvent e);
116
117                 /**
118                  * 情報
119                  *
120                  * @param e
121                  */
122                 void showInformation(PreviewPanelEvent e);
123
124                 /**
125                  * お気に入りに追加
126                  *
127                  * @param e
128                  */
129                 void addFavorite(PreviewPanelEvent e);
130
131                 /**
132                  * 左右反転
133                  *
134                  * @param e
135                  */
136                 void flipHorizontal(PreviewPanelEvent e);
137         }
138
139         /**
140          * ロード中を示すインジケータ
141          */
142         private final String indicatorText;
143
144         /**
145          * ロード中であるか判定するタイマー
146          */
147         private final Timer timer;
148
149         /**
150          * インジケータを表示するまでのディレイ
151          */
152         private long indicatorDelay;
153
154         @Override
155         public void addNotify() {
156                 super.addNotify();
157                 if (!timer.isRunning()) {
158                         timer.start();
159                 }
160         }
161
162         @Override
163         public void removeNotify() {
164                 if (timer.isRunning()) {
165                         timer.stop();
166                 }
167                 super.removeNotify();
168         }
169
170         public static class PreviewPanelEvent extends EventObject {
171
172                 private static final long serialVersionUID = 1L;
173
174                 private int modifiers;
175
176                 public PreviewPanelEvent(Object src, ActionEvent e) {
177                         this(src, (e == null) ? 0 : e.getModifiers());
178                 }
179
180                 public PreviewPanelEvent(Object src, int modifiers) {
181                         super(src);
182                         this.modifiers = modifiers;
183                 }
184
185                 public int getModifiers() {
186                         return modifiers;
187                 }
188
189                 public boolean isShiftKeyPressed() {
190                         return (modifiers & ActionEvent.SHIFT_MASK) != 0;
191                 }
192         }
193
194         private final Object lock = new Object();
195
196         private long loadingTicket;
197
198         private long loadedTicket;
199
200         private long firstWaitingTimestamp;
201
202         private boolean indicatorShown;
203
204         private String title;
205
206         private JLabel lblTitle;
207
208         private JLayeredPane layeredPane;
209
210         private CheckInfoLayerPanel checkInfoLayerPanel;
211
212         private PreviewImagePanel previewImgPanel;
213
214         private JScrollPane previewImgScrollPane;
215
216         private ScrollPaneDragScrollSupport scrollSupport;
217
218         private PreviewControlPanel previewControlPanel;
219
220         private double latestToggleZoom = 2.;
221
222         private LinkedList<PreviewPanelListener> listeners = new LinkedList<PreviewPanelListener>();
223
224
225         public PreviewPanel(ScaleSupport scaleSupport) {
226                 setLayout(new BorderLayout());
227
228                 final AppConfig appConfig = AppConfig.getInstance();
229                 final Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
230                         .getLocalizedProperties(STRINGS_RESOURCE);
231
232                 // 画像をロード中であることを示すインジケータの確認サイクル.
233                 timer = new Timer(100, new ActionListener() {
234                                 public void actionPerformed(ActionEvent e) {
235                                         onTimer();
236                                 }
237                         });
238
239                 indicatorText = strings.getProperty("indicatorText");
240                 indicatorDelay = appConfig.getPreviewIndicatorDelay();
241
242                 UIHelper uiUtl = UIHelper.getInstance();
243                 JButton saveBtn = uiUtl.createIconButton("icons/save.png");
244                 JButton copyBtn = uiUtl.createIconButton("icons/copy.png");
245                 JButton colorBtn = uiUtl.createIconButton("icons/color.png");
246                 JButton informationBtn = uiUtl.createIconButton("icons/information.png");
247                 JButton favoriteBtn = uiUtl.createIconButton("icons/favorite.png");
248                 JButton flipHolizontalBtn = uiUtl.createIconButton("icons/flip.png");
249
250                 saveBtn.addActionListener(new ActionListener() {
251                         public void actionPerformed(ActionEvent e) {
252                                 savePicture(new PreviewPanelEvent(PreviewPanel.this, e));
253                         }
254                 });
255                 copyBtn.addActionListener(new ActionListener() {
256                         public void actionPerformed(ActionEvent e) {
257                                 copyPicture(new PreviewPanelEvent(PreviewPanel.this, e));
258                         }
259                 });
260                 colorBtn.addActionListener(new ActionListener() {
261                         public void actionPerformed(ActionEvent e) {
262                                 changeBackgroundColor(new PreviewPanelEvent(PreviewPanel.this, e));
263                         }
264                 });
265                 informationBtn.addActionListener(new ActionListener() {
266                         public void actionPerformed(ActionEvent e) {
267                                 showInformation(new PreviewPanelEvent(PreviewPanel.this, e));
268                         }
269                 });
270                 favoriteBtn.addActionListener(new ActionListener() {
271                         public void actionPerformed(ActionEvent e) {
272                                 addFavorite(new PreviewPanelEvent(PreviewPanel.this, e));
273                         }
274                 });
275                 flipHolizontalBtn.addActionListener(new ActionListener() {
276                         public void actionPerformed(ActionEvent e) {
277                                 flipHolizontal(new PreviewPanelEvent(PreviewPanel.this, e));
278                         }
279                 });
280
281                 saveBtn.setToolTipText(strings.getProperty("tooltip.save"));
282                 copyBtn.setToolTipText(strings.getProperty("tooltip.copy"));
283                 colorBtn.setToolTipText(strings.getProperty("tooltip.changeBgColor"));
284                 informationBtn.setToolTipText(strings.getProperty("tooltip.showInformation"));
285                 favoriteBtn.setToolTipText(strings.getProperty("tooltip.registerFavorites"));
286                 flipHolizontalBtn.setToolTipText(strings.getProperty("tooltip.flipHorizontal"));
287
288                 final JToolBar toolBar = new JToolBar();
289                 toolBar.setFloatable(false);
290                 toolBar.add(flipHolizontalBtn);
291                 toolBar.add(copyBtn);
292                 toolBar.add(saveBtn);
293                 toolBar.add(Box.createHorizontalStrut(8));
294                 toolBar.add(colorBtn);
295                 toolBar.add(Box.createHorizontalStrut(4));
296                 toolBar.add(favoriteBtn);
297                 toolBar.add(informationBtn);
298
299                 lblTitle = new JLabel() {
300                         private static final long serialVersionUID = 1L;
301
302                         public Dimension getPreferredSize() {
303                                 Dimension dim = super.getPreferredSize();
304                                 int maxWidth = getParent().getWidth() - toolBar.getWidth();
305                                 if (dim.width > maxWidth) {
306                                         dim.width = maxWidth;
307                                 }
308                                 return dim;
309                         };
310
311                         public Dimension getMaximumSize() {
312                                 return getPreferredSize();
313                         };
314
315                         public Dimension getMinimumSize() {
316                                 Dimension dim = getPreferredSize();
317                                 dim.width = 50;
318                                 return dim;
319                         };
320                 };
321
322                 lblTitle.setBorder(BorderFactory.createEmptyBorder(3, 10, 3, 3));
323
324                 JPanel previewPaneHeader = new JPanel();
325                 previewPaneHeader.setLayout(new BorderLayout());
326                 previewPaneHeader.add(lblTitle, BorderLayout.WEST);
327                 previewPaneHeader.add(toolBar, BorderLayout.EAST);
328
329                 previewImgPanel = new PreviewImagePanel();
330
331
332                 previewImgScrollPane = new JScrollPane(previewImgPanel);
333                 previewImgScrollPane.setAutoscrolls(false);
334                 previewImgScrollPane.setWheelScrollingEnabled(false);
335                 previewImgScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
336                 previewImgScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
337
338                 scrollSupport = new ScrollPaneDragScrollSupport(previewImgScrollPane) {
339                         @Override
340                         protected void setCursor(Cursor cursor) {
341                                 PreviewPanel.this.setCursor(cursor);
342                         }
343                 };
344
345                 add(previewPaneHeader, BorderLayout.NORTH);
346
347                 layeredPane = new JLayeredPane();
348                 layeredPane.setLayout(new OverlayLayout(layeredPane));
349
350                 layeredPane.add(previewImgScrollPane, JLayeredPane.DEFAULT_LAYER);
351
352                 checkInfoLayerPanel = new CheckInfoLayerPanel();
353                 layeredPane.add(checkInfoLayerPanel, JLayeredPane.POPUP_LAYER);
354                 checkInfoLayerPanel.setVisible(false);
355
356                 add(layeredPane, BorderLayout.CENTER);
357
358                 previewControlPanel = new PreviewControlPanel();
359                 Dimension dim = previewControlPanel.getPreferredSize();
360                 Dimension prevDim = previewImgScrollPane.getPreferredSize();
361                 dim.width = prevDim.width;
362                 previewControlPanel.setPreferredSize(dim);
363
364                 add(previewControlPanel, BorderLayout.SOUTH);
365                 previewControlPanel.setPinned(appConfig.isEnableZoomPanel());
366
367                 // 倍率が変更された場合
368                 previewControlPanel.addPropertyChangeListener("zoomFactorInt", new PropertyChangeListener() {
369                         public void propertyChange(PropertyChangeEvent evt) {
370                                 Integer newValue = (Integer) evt.getNewValue();
371                                 zoomWithCenterPosition(newValue.doubleValue() / 100., null);
372                         }
373                 });
374                 // 背景モードが切り替えられた場合
375                 previewControlPanel.addPropertyChangeListener("backgroundColorMode", new PropertyChangeListener() {
376                         public void propertyChange(PropertyChangeEvent evt) {
377                                 BackgroundColorMode bgColorMode = (BackgroundColorMode) evt.getNewValue();
378                                 previewImgPanel.setBackgroundColorMode(bgColorMode);
379                                 if (bgColorMode != BackgroundColorMode.ALPHABREND
380                                                 && appConfig.isEnableCheckInfoTooltip() ) {
381                                                         // チェック情報ツールチップの表示
382                                         checkInfoLayerPanel.setMessage(null);
383                                         checkInfoLayerPanel.setVisible(true);
384
385                                 } else {
386                                                         // チェック情報ツールチップの非表示
387                                         checkInfoLayerPanel.setVisible(false);
388                                 }
389                         }
390                 });
391
392                 previewImgScrollPane.addMouseMotionListener(new MouseMotionAdapter() {
393                         @Override
394                         public void mouseMoved(MouseEvent e) {
395                                 Rectangle rct = previewImgScrollPane.getBounds();
396                                 int y = e.getY();
397                                 UIHelper uiUtl = UIHelper.getInstance();
398                                 if (y > rct.height - (int) (appConfig.getZoomPanelActivationArea() * uiUtl.getScaleY())) {
399                                         previewControlPanel.setVisible(true);
400                                 } else {
401                                         if ( !previewControlPanel.isPinned()) {
402                                                 previewControlPanel.setVisible(false);
403                                         }
404                                 }
405                         }
406                 });
407
408                 // 標準のホイールリスナは削除する.
409                 for (final MouseWheelListener listener : previewImgScrollPane.getMouseWheelListeners()) {
410                         previewImgScrollPane.removeMouseWheelListener(listener);
411                 }
412
413                 previewImgScrollPane.addMouseWheelListener(new MouseWheelListener() {
414                         public void mouseWheelMoved(MouseWheelEvent e) {
415                                 if ((Main.isMacOSX() && e.isAltDown()) ||
416                                                 ( !Main.isMacOSX() && e.isControlDown())) {
417                                         // Mac OS XならOptionキー、それ以外はコントロールキーとともにホイールスクロールの場合
418                                         zoomByWheel(e);
419                                 } else {
420                                         // ズーム以外のホイール操作はスクロールとする.
421                                         scrollByWheel(e);
422                                 }
423                                 // 現在画像位置の情報の更新
424                                 updateCheckInfoMessage(e.getPoint());
425                         }
426                 });
427
428                 previewImgScrollPane.addMouseListener(new MouseAdapter() {
429                         @Override
430                         public void mousePressed(MouseEvent e) {
431                                 if (e.getClickCount() == 2) {
432                                         // ダブルクリック
433                                         // (正確に2回目。3回目以降はダブルクリック + シングルクリック)
434                                         toggleZoom(e.getPoint());
435                                 } else {
436                                         scrollSupport.drag(true, e.getPoint());
437                                 }
438                         }
439                         @Override
440                         public void mouseReleased(MouseEvent e) {
441                                 scrollSupport.drag(false, e.getPoint());
442                         }
443                 });
444
445                 previewImgScrollPane.addMouseMotionListener(new MouseMotionListener() {
446
447                         public void mouseMoved(MouseEvent e) {
448                                 updateCheckInfoMessage(e.getPoint());
449                         }
450
451                         public void mouseDragged(MouseEvent e) {
452                                 scrollSupport.dragging(e.getPoint());
453
454                                 // 現在画像位置の情報の更新
455                                 updateCheckInfoMessage(e.getPoint());
456                         }
457                 });
458         }
459
460         /**
461          * 倍率を切り替える.
462          */
463         protected void toggleZoom(Point mousePos) {
464                 if (previewImgPanel.isDefaultZoom()) {
465                         // 等倍であれば以前の倍率を適用する.
466                         zoomWithCenterPosition(latestToggleZoom, mousePos);
467
468                 } else {
469                         // 等倍でなければ現在の倍率を記憶して等倍にする.
470                         double currentZoomFactor = previewImgPanel.getZoomFactor();
471                         latestToggleZoom = currentZoomFactor;
472                         zoomWithCenterPosition(1., mousePos);
473                 }
474         }
475
476         /**
477          * マウス位置に対して画像情報のツールチップを表示する
478          *
479          * @param mousePosition
480          *            マウス位置
481          */
482         protected void updateCheckInfoMessage(Point mousePosition) {
483                 if ( !checkInfoLayerPanel.isVisible()) {
484                         return;
485                 }
486                 // マウス位置から画像位置を割り出す
487                 Point imgPos = null;
488                 if (mousePosition != null) {
489                         Point panelPt = SwingUtilities.convertPoint(previewImgScrollPane,
490                                         mousePosition, previewImgPanel);
491                         imgPos = previewImgPanel.getImagePosition(panelPt);
492                 }
493                 if (imgPos != null) {
494                         // 画像位置があれば、その位置の情報を取得する.
495                         int argb = previewImgPanel.getImageARGB(imgPos);
496                         int a = (argb >> 24) & 0xff;
497                         int r = (argb >> 16) & 0xff;
498                         int g = (argb >> 8) & 0xff;
499                         int b = argb & 0xff;
500                         int y = (int) (0.298912f * r + 0.586611f * g + 0.114478f * b);
501                         String text = String.format(
502                                         "(%3d,%3d)¥nA:%3d, Y:%3d¥nR:%3d, G:%3d, B:%3d", imgPos.x,
503                                         imgPos.y, a, y, r, g, b);
504                         checkInfoLayerPanel.setMessage(text);
505                         checkInfoLayerPanel.setPotision(mousePosition);
506
507                 } else {
508                         // 画像位置がなければツールチップは空にする.
509                         checkInfoLayerPanel.setMessage(null);
510                 }
511         }
512
513         /**
514          * マウス座標単位で指定したオフセット分スクロールする.
515          *
516          * @param diff_x
517          *            水平方向スクロール数
518          * @param diff_y
519          *            垂直方向スクロール数
520          */
521         protected void scroll(int diff_x, int diff_y) {
522                 scrollSupport.scroll(diff_x, diff_y);
523         }
524
525         /**
526          * マウスホイールによる水平・垂直スクロール.<br>
527          * シフトキーで水平、それ以外は垂直とする.<br>
528          *
529          * @param e
530          *            ホイールイベント
531          */
532         protected void scrollByWheel(final MouseWheelEvent e) {
533                 scrollSupport.scrollByWheel(e);
534
535                 // イベントは処理済みとする.
536                 e.consume();
537         }
538
539         /**
540          * ホイールによる拡大縮小.<br>
541          * ホイールの量は関係なく、方向だけで判定する.<br>
542          * プラットフォームごとに修飾キーの判定が異なるので、 呼び出しもとであらかじめ切り分けて呼び出すこと.<br>
543          *
544          * @param e
545          *            ホイールイベント
546          */
547         protected void zoomByWheel(final MouseWheelEvent e) {
548                 int wheelRotation = e.getWheelRotation();
549                 double currentZoom = previewImgPanel.getZoomFactor();
550                 double zoomFactor;
551                 if (wheelRotation < 0) {
552                         // ホイール上で拡大
553                         zoomFactor = currentZoom * 1.1;
554
555                 } else if (wheelRotation > 0){
556                         // ホイール下で縮小
557                         zoomFactor = currentZoom * 0.9;
558
559                 } else {
560                         return;
561                 }
562
563                 // 倍率変更する
564                 zoomWithCenterPosition(zoomFactor, e.getPoint());
565
566                 // イベント処理済み
567                 e.consume();
568         }
569
570         /**
571          * ズームスライダまたはコンボのいずれかの値を更新すると、他方からも更新通知があがるため 二重処理を防ぐためのセマフォ.<br>
572          */
573         private Semaphore zoomLock = new Semaphore(1);
574
575         /**
576          * プレビューに表示する画像の倍率を更新する.<br>
577          * 指定した座標が拡大縮小の中心点になるようにスクロールを試みる.<br>
578          * 座標がnullの場合は現在表示されている中央を中心とするようにスクロールを試みる.<br>
579          * (スクロールバーが表示されていない、もしくは十分にスクロールできない場合は必ずしも中心とはならない.)<br>
580          * コントロールパネルの表示値も更新する.<br>
581          * コントロールパネルからの更新通知をうけて再入しないように、 同時に一つしか実行されないようにしている.<br>
582          *
583          * @param zoomFactor
584          *            倍率、範囲外のものは範囲内に補正される.
585          * @param mousePos
586          *            スクロールペイン上のマウス座標、もしくはnull(nullの場合は表示中央)
587          */
588         protected void zoomWithCenterPosition(double zoomFactor, Point mousePos) {
589                 if ( !zoomLock.tryAcquire()) {
590                         return;
591                 }
592                 try {
593                         // 範囲制限.
594                         if (zoomFactor < 0.2) {
595                                 zoomFactor = 0.2;
596                         } else if (zoomFactor > 8.) {
597                                 zoomFactor = 8.;
598                         }
599
600                         JViewport vp = previewImgScrollPane.getViewport();
601
602                         Point viewCenter;
603                         if (mousePos != null) {
604                                 // スクロールペインのマウス座標を表示パネルの位置に換算する.
605                                 viewCenter = SwingUtilities.convertPoint(this, mousePos, previewImgPanel);
606
607                         } else {
608                                 // 表示パネル上の現在表示しているビューポートの中央の座標を求める
609                                 Rectangle viewRect = vp.getViewRect();
610                                 viewCenter = new Point(
611                                                 (viewRect.x + viewRect.width / 2),
612                                                 (viewRect.y + viewRect.height / 2)
613                                                 );
614                         }
615
616                         // 現在のビューサイズ(余白があれば余白も含む)
617                         Dimension viewSize = previewImgPanel.getScaledSize(true);
618
619                         // 倍率変更
620                         previewControlPanel.setZoomFactor(zoomFactor);
621                         previewImgPanel.setZoomFactor(zoomFactor);
622
623                         // 新しいのビューサイズ(余白があれば余白も含む)
624                         Dimension viewSizeAfter = previewImgPanel.getScaledSize(true);
625                         Dimension visibleSize = vp.getExtentSize();
626
627                         if (viewSize != null && viewSizeAfter != null &&
628                                 viewSizeAfter.width > 0 && viewSizeAfter.height > 0 &&
629                                 viewSizeAfter.width > visibleSize.width &&
630                                 viewSizeAfter.height > visibleSize.height) {
631                                 // 新しいビューの大きさよりも表示可能領域が小さい場合のみ
632                                 vp.setViewSize(viewSizeAfter);
633
634                                 // スクロールペインに表示されている画面サイズを求める.
635                                 // スクロールバーがある方向は、コンテンツの最大と等しいが
636                                 // スクロールバーがない場合は画面サイズのほうが大きいため、
637                                 // 倍率変更による縦横の移動比は、それぞれ異なる.
638                                 int visible_width = max(visibleSize.width, viewSize.width);
639                                 int visible_height = max(visibleSize.height, viewSize.height);
640                                 int visible_width_after = max(visibleSize.width, viewSizeAfter.width);
641                                 int visible_height_after = max(visibleSize.height, viewSizeAfter.height);
642
643                                 // 前回の倍率から今回の倍率の倍率.
644                                 // オリジナルに対する倍率ではない.
645                                 // また、画像は縦横同率であるが表示ウィンドウはスクロールバー有無により同率とは限らない.
646                                 double zoomDiffX = (double) visible_width_after / (double) visible_width;
647                                 double zoomDiffY = (double) visible_height_after / (double) visible_height;
648
649                                 // 拡大後の座標の補正
650                                 Point viewCenterAfter = new Point();
651                                 viewCenterAfter.x = (int) round(viewCenter.x * zoomDiffX);
652                                 viewCenterAfter.y = (int) round(viewCenter.y * zoomDiffY);
653
654                                 // 倍率適用前後の座標の差分
655                                 int diff_x = viewCenterAfter.x - viewCenter.x;
656                                 int diff_y = viewCenterAfter.y - viewCenter.y;
657
658                                 // スクロール
659                                 scroll(diff_x, diff_y);
660                         }
661
662                         // スクロールの単位を画像1ドットあたりの表示サイズに変更する.
663                         // (ただし1を下回らない)
664                         JScrollBar vsb = previewImgScrollPane.getVerticalScrollBar();
665                         JScrollBar hsb = previewImgScrollPane.getHorizontalScrollBar();
666                         vsb.setUnitIncrement(max(1, (int) ceil(zoomFactor)));
667                         hsb.setUnitIncrement(max(1, (int) ceil(zoomFactor)));
668
669                 } finally {
670                         zoomLock.release();
671                 }
672         }
673
674         /**
675          * 現在のビューの左上位置を返す
676          * @return
677          */
678         public Point getViewPosition() {
679                 JViewport vp = previewImgScrollPane.getViewport();
680                 return vp.getViewPosition();
681         }
682
683         /**
684          * 指定した座標が中央となるようにスクロールする。
685          * まだ画像が表示されていない場合は次に画像を設定したときに行う。
686          * @param centerPt 中央
687          */
688         public void setViewPosition(Point viewPt) {
689                 JViewport vp = previewImgScrollPane.getViewport();
690                 if (previewImgPanel.getPreviewImage() != null) {
691                         if (viewPt != null) {
692                                 vp.setViewPosition(viewPt);
693                         }
694                         requestViewPt = null;
695                 } else {
696                         requestViewPt = viewPt;
697                 }
698         }
699
700         private Point requestViewPt;
701
702         /**
703          * プレビューに表示するタイトル.<br>
704          *
705          * @param title
706          *            タイトル
707          */
708         public void setTitle(String title) {
709                 if (title == null) {
710                         title = "";
711                 }
712                 if (!title.equals(this.title)) {
713                         this.title = title;
714                         lblTitle.setText(title + (indicatorShown ? indicatorText : ""));
715                         lblTitle.setToolTipText(title);
716                 }
717         }
718
719         public String getTitle() {
720                 return this.title;
721         }
722
723         /**
724          * ロードに時間がかかっているか判定し、 インジケータを表示するためのタイマーイベントハンドラ.<br>
725          */
726         protected void onTimer() {
727                 boolean waiting;
728                 long firstRequest;
729                 synchronized (lock) {
730                         waiting = isWaiting();
731                         firstRequest = firstWaitingTimestamp;
732                 }
733                 boolean indicatorShown = (waiting && ((System.currentTimeMillis() - firstRequest) > indicatorDelay));
734                 if (this.indicatorShown != indicatorShown) {
735                         this.indicatorShown = indicatorShown;
736                         lblTitle.setText(title + (indicatorShown ? indicatorText : ""));
737                 }
738         }
739
740         /**
741          * チケットの状態が、ロード完了待ち状態であるか?<br>
742          * ロード中のチケットが、ロード完了のチケットより新しければロード中と見なす.<br>
743          *
744          * @return 完了待ちであればtrue、そうでなければfalse
745          */
746         protected boolean isWaiting() {
747                 synchronized (lock) {
748                         return loadingTicket > loadedTicket;
749                 }
750         }
751
752         /**
753          * ロード要求が出されるたびに、そのロード要求チケットを登録する.<br>
754          * チケットは要求されるたびに増加するシーケンスとする.<br>
755          *
756          * @param ticket
757          *            ロード要求チケット
758          */
759         public void setLoadingRequest(long ticket) {
760                 synchronized (lock) {
761                         if ( !isWaiting() && this.loadedTicket < ticket) {
762                                 // 現在認識しているチケットの状態がロード完了であり、
763                                 // それよりも新しいチケットが要求されたならば、
764                                 // 今回のチケットから待ち時間の計測を開始する.
765                                 this.firstWaitingTimestamp = System.currentTimeMillis();
766                         }
767                         this.loadingTicket = ticket;
768                 }
769         }
770
771         /**
772          * ロード完了するたびに呼び出される.<br>
773          *
774          * @param ticket
775          *            ロード要求チケット.
776          */
777         public void setLoadingComplete(long ticket) {
778                 synchronized (lock) {
779                         this.loadedTicket = ticket;
780                 }
781         }
782
783         /**
784          * 表示画像を設定する.<br>
785          *
786          * @param previewImg
787          *            表示画像、もしくはnull
788          */
789         public void setPreviewImage(BufferedImage previewImg) {
790                 previewImgPanel.setPreviewImage(previewImg);
791                 if (requestViewPt != null) {
792                         // 画像設定前にスクロール位置の要求があれば、再適用を試みる
793                         setViewPosition(requestViewPt);
794                 }
795         }
796
797         /**
798          * 表示されている画像を取得する.<br>
799          * 表示画像が設定されていなければnull.<br>
800          *
801          * @return 表示画像、もしくはnull
802          */
803         public BufferedImage getPreviewImage() {
804                 return previewImgPanel.getPreviewImage();
805         }
806
807         /**
808          * 表示している画面イメージそのままを取得する.
809          *
810          * @return 表示画像
811          */
812         public BufferedImage getScreenImage() {
813                 JViewport vp = previewImgScrollPane.getViewport();
814                 Dimension dim = vp.getExtentSize();
815                 BufferedImage img = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_ARGB);
816                 Graphics2D g = img.createGraphics();
817                 try {
818                         vp.paint(g);
819
820                 } finally {
821                         g.dispose();
822                 }
823                 return img;
824         }
825
826         /**
827          * 壁紙を設定する.<br>
828          *
829          * @param wallpaperImg
830          *            壁紙、null不可
831          */
832         public void setWallpaper(Wallpaper wallpaper) {
833                 previewImgPanel.setWallpaper(wallpaper);
834         }
835
836         /**
837          * 壁紙を取得する.<br>
838          * 壁紙が未設定の場合は空の壁紙インスタンスが返される.<br>
839          *
840          * @return 壁紙
841          */
842         public Wallpaper getWallpaper() {
843                 return previewImgPanel.getWallpaper();
844         }
845
846         /**
847          * 表示倍率を取得する.
848          *
849          * @return 表示倍率
850          */
851         public double getZoomFactor() {
852                 return previewControlPanel.getZoomFactor();
853         }
854
855         /**
856          * 表示倍率を設定する
857          *
858          * @param zoomFactor
859          *            表示倍率
860          */
861         public void setZoomFactor(double zoomFactor) {
862                 previewControlPanel.setZoomFactor(zoomFactor);
863         }
864
865         /**
866          * ズームパネルのピン留め制御
867          *
868          * @param visible
869          *            表示する場合はtrue
870          */
871         public void setVisibleZoomBox(boolean visible) {
872                 previewControlPanel.setPinned(visible);
873         }
874
875         /**
876          * ズームパネルがピン留めされているか?
877          *
878          * @return ピン留めされていればtrue
879          */
880         public boolean isVisibleZoomBox() {
881                 return previewControlPanel.isPinned();
882         }
883
884         public void addPreviewPanelListener(PreviewPanelListener listener) {
885                 if (listener == null) {
886                         throw new IllegalArgumentException();
887                 }
888                 listeners.add(listener);
889         }
890
891         public void removePreviewPanelListener(PreviewPanelListener listener) {
892                 listeners.remove(listener);
893         }
894
895         protected void savePicture(PreviewPanelEvent e) {
896                 for (PreviewPanelListener listener : listeners) {
897                         listener.savePicture(e);
898                 }
899         }
900
901         protected void flipHolizontal(PreviewPanelEvent e) {
902                 for (PreviewPanelListener listener : listeners) {
903                         listener.flipHorizontal(e);
904                 }
905         }
906
907         protected void copyPicture(PreviewPanelEvent e) {
908                 for (PreviewPanelListener listener : listeners) {
909                         listener.copyPicture(e);
910                 }
911         }
912
913         protected void changeBackgroundColor(PreviewPanelEvent e) {
914                 for (PreviewPanelListener listener : listeners) {
915                         listener.changeBackgroundColor(e);
916                 }
917         }
918
919         protected void showInformation(PreviewPanelEvent e) {
920                 for (PreviewPanelListener listener : listeners) {
921                         listener.showInformation(e);
922                 }
923         }
924
925         protected void addFavorite(PreviewPanelEvent e) {
926                 for (PreviewPanelListener listener : listeners) {
927                         listener.addFavorite(e);
928                 }
929         }
930 }
931
932 /**
933  * チェック情報の表示用レイヤーパネル.<br>
934  *
935  * @author seraphy
936  */
937 class CheckInfoLayerPanel extends JPanel {
938         private static final long serialVersionUID = 1L;
939
940         /**
941          * ロガー
942          */
943         private static final Logger logger = Logger.getLogger(CheckInfoLayerPanel.class.getName());
944
945         /**
946          * ボックスの余白
947          */
948         private Insets padding = new Insets(3, 3, 3, 3);
949
950         /**
951          * 表示位置プロパティ
952          */
953         private Point pos = new Point();
954
955         /**
956          * 表示メッセージプロパティ.<br>
957          * ¥nで改行となる.<br>
958          * 空文字ならば非表示.<br>
959          */
960         private String message = "";
961
962         /**
963          * 解析済みメッセージ.<br>
964          * 業に分割される.<br>
965          * 空文字は空のリストとなる.<br>
966          */
967         private String[] messageLines;
968
969         /**
970          * 解析済みフォントの高さ.<br>
971          */
972         private int fontHeight;
973
974         /**
975          * 描画済みエリア.<br>
976          * 次回描画前に消去する必要のある領域.<br>
977          * まだ一度も描画してなければnull.<br>
978          */
979         private Rectangle eraseRect;
980
981         /**
982          * 現在、描画すべきエリア.<br>
983          * なければnull.<br>
984          */
985         private Rectangle requestRect;
986
987         /**
988          * 画面に関連づけられていない状態でのテキスト表示サイズは 計算できないため、画面追加時に再計算させるための 予約フラグ.<br>
989          */
990         private boolean requestRecalcOnAdd;
991
992         /**
993          * フォントのためのデスクトップヒント.(あれば)
994          */
995         @SuppressWarnings("rawtypes")
996         private Map desktopHintsForFont;
997
998         /**
999          * 透明コンポーネントとして構築する.<br>
1000          */
1001         @SuppressWarnings("rawtypes")
1002         public CheckInfoLayerPanel() {
1003                 setOpaque(false);
1004
1005                 Toolkit tk = Toolkit.getDefaultToolkit();
1006                 desktopHintsForFont = (Map) tk.getDesktopProperty("awt.font.desktophints");
1007                 logger.log(Level.CONFIG, "awt.font.desktophints=" + desktopHintsForFont);
1008         }
1009
1010         /**
1011          * 指定エリアに情報を描画する.<br>
1012          */
1013         @Override
1014         protected void paintComponent(Graphics g0) {
1015                 Graphics2D g = (Graphics2D) g0;
1016                 super.paintComponent(g);
1017
1018                 // クリップ領域
1019                 Rectangle clip = g.getClipBounds();
1020                 // System.out.println("clip:" + clip + " /eraseRect:" + eraseRect + " /drawRect:" + requestRect);
1021
1022                 // 削除すべき領域が描画範囲に含まれているか?
1023                 // (含まれていれば、その領域は消去済みである.)
1024                 if (clip == null || (eraseRect != null && clip.contains(eraseRect))) {
1025                         eraseRect = null;
1026                 }
1027
1028                 // 表示領域の判定
1029                 if (requestRect == null || requestRect.isEmpty()
1030                                 || !(clip != null && clip.intersects(requestRect))) {
1031                         // 表示すべき領域が存在しないか、描画要求範囲にない.
1032                         return;
1033                 }
1034                 if (messageLines == null || messageLines.length == 0) {
1035                         // 表示するものがなければ何もしない.
1036                         return;
1037                 }
1038
1039                 // フォントのレンダリングヒント
1040                 if (desktopHintsForFont != null) {
1041                         g.addRenderingHints(desktopHintsForFont);
1042                 }
1043
1044                 // 箱の描画
1045                 g.setColor(new Color(255, 255, 255, 192));
1046                 g.fillRect(requestRect.x, requestRect.y, requestRect.width, requestRect.height);
1047                 g.setColor(Color.GRAY);
1048                 g.drawRect(requestRect.x, requestRect.y, requestRect.width - 1, requestRect.height - 1);
1049
1050                 // 情報の描画
1051                 g.setColor(Color.BLACK);
1052                 int oy = fontHeight;
1053                 for (String messageLine : messageLines) {
1054                         g.drawString(messageLine, requestRect.x + padding.left, requestRect.y + padding.top - 1 + oy);
1055                         oy += fontHeight;
1056                 }
1057
1058                 // 描画された領域を次回消去領域として記憶する.
1059                 if (eraseRect == null || eraseRect.isEmpty()) {
1060                         // 消去済みであれば、今回分のみを次回消去領域とする.
1061                         eraseRect = (Rectangle) requestRect.clone();
1062
1063                 } else {
1064                         // 消去済みエリアが未消去で残っている場合は
1065                         // 今回領域を結合する.
1066                         eraseRect.add(requestRect);
1067                 }
1068         }
1069
1070         /**
1071          * 画面にアタッチされた場合、描画領域の再計算が 必要であれば計算する.<br>
1072          */
1073         @Override
1074         public void addNotify() {
1075                 super.addNotify();
1076                 if (requestRecalcOnAdd) {
1077                         requestRecalcOnAdd = false;
1078                         calcRepaint();
1079                 }
1080         }
1081
1082         /**
1083          * 要求されたプロパティから、フォント高さによる表示領域を計算し、 その領域の再描画を要求する.(描画する内容がなれば、描画要求しない.)<br>
1084          * 前回表示領域があれば、消去するために、そのエリアも再描画を要求する.<br>
1085          * それ以外のエリアは描画要求しない.(描画の最適化による負荷軽減策)<br>
1086          * フォントサイズを求めるためにグラフィクスへのアクセスが必要となるが、 まだ取得できない場合は{@link #addNotify()}の呼び出し時に
1087          * 再計算するようにフラグを立てておく.<br>
1088          */
1089         protected void calcRepaint() {
1090                 Graphics2D g = (Graphics2D) getGraphics();
1091                 if (g == null) {
1092                         requestRecalcOnAdd = true;
1093                         return;
1094                 }
1095                 try {
1096                         // 前回描画領域のクリアのために呼び出す.
1097                         if (eraseRect != null && !eraseRect.isEmpty()) {
1098                                 repaint(eraseRect);
1099                         }
1100
1101                         // 空であれば新たな描画なし.
1102                         if (message.length() == 0) {
1103                                 requestRect = null;
1104                                 return;
1105                         }
1106
1107                         FontMetrics fm = g.getFontMetrics();
1108                         String[] messageLines = message.split("¥n");
1109
1110                         Rectangle2D rct = null;
1111                         for (String messageLine : messageLines) {
1112                                 Rectangle2D tmp = fm.getStringBounds(messageLine, g);
1113                                 if (rct != null) {
1114                                         rct.add(tmp);
1115
1116                                 } else {
1117                                         rct = tmp;
1118                                 }
1119                         }
1120
1121                         int fw = (int) rct.getWidth();
1122                         int fh = (int) rct.getHeight();
1123
1124                         int w = fw + padding.left + padding.right;
1125                         int h = fh * messageLines.length + padding.top + padding.bottom;
1126
1127                         // 指定した位置の右上あたりにする
1128                         int x = pos.x + 16;
1129                         int y = pos.y - h;
1130
1131                         // サイズ
1132                         int client_w = getWidth();
1133                         int client_h = getHeight();
1134
1135                         if (x + w > client_w) {
1136                                 // 画面右の場合はカーソルの左に移動
1137                                 x = pos.x - w - 10;
1138                         }
1139                         if (y < 0) {
1140                                 // 画面上の場合はカーソルの下に移動
1141                                 y = pos.y + 10;
1142                         }
1143                         if (y + h > client_h) {
1144                                 y -= (y + h - client_h);
1145                         }
1146
1147                         // 結果の格納
1148                         this.requestRect = new Rectangle(x, y, w, h);
1149                         this.messageLines = messageLines;
1150                         this.fontHeight = fh;
1151
1152                         // 再描画の要求
1153                         Rectangle paintRect = (Rectangle) requestRect.clone();
1154                         repaint(paintRect);
1155
1156                 } finally {
1157                         g.dispose();
1158                 }
1159         }
1160
1161         public void setPotision(Point requestPt) {
1162                 if (requestPt == null) {
1163                         throw new IllegalArgumentException();
1164                 }
1165                 if ( !requestPt.equals(pos)) {
1166                         Point oldpos = pos;
1167                         pos = (Point) requestPt.clone();
1168                         calcRepaint();
1169                         firePropertyChange("position", oldpos, pos);
1170                 }
1171         }
1172
1173         public Point getPosition() {
1174                 return (Point) pos.clone();
1175         }
1176
1177         public void setMessage(String message) {
1178                 if (message == null) {
1179                         message = "";
1180                 }
1181                 message = message.replace("¥r¥n", "¥n");
1182                 if ( !message.equals(this.message)) {
1183                         String oldmes = this.message;
1184                         this.message = message;
1185                         calcRepaint();
1186                         firePropertyChange("message", oldmes, message);
1187                 }
1188         }
1189
1190         public String getMessage() {
1191                 return message;
1192         }
1193 }
1194
1195 /**
1196  * 画像表示パネル
1197  *
1198  * @author seraphy
1199  */
1200 class PreviewImagePanel extends JPanel {
1201         private static final long serialVersionUID = 1L;
1202
1203         /**
1204          * 背景モード.<br>
1205          */
1206         private BackgroundColorMode bgColorMode;
1207
1208         /**
1209          * 壁紙.<br>
1210          */
1211         private Wallpaper wallpaper;
1212
1213         /**
1214          * 壁紙変更イベントのリスナ
1215          */
1216         private PropertyChangeListener wallpaperListener;
1217
1218         /**
1219          * 透過オリジナル画像.<br>
1220          */
1221         private BufferedImage previewImg;
1222
1223         /**
1224          * 表示用画像(背景モードによる調整あり).<br>
1225          * 事前に拡大縮小を適用済みの場合は、{@link #scaledZoomFactor}に 適用している倍率が設定される.<br>
1226          * 表示用に改めてイメージを生成する必要がない場合は、 透過オリジナルと同じインスタンスとなりえる.<br>
1227          */
1228         private BufferedImage previewImgForDraw;
1229
1230         /**
1231          * 表示用画像がスケール済みである場合、そのスケールが設定される.<br>
1232          * スケール済みでない場合はnullとなる.<br>
1233          */
1234         private Double scaledZoomFactor;
1235
1236
1237         /**
1238          * 倍率
1239          */
1240         private double zoomFactor = 1.;
1241
1242         /**
1243          * 許容誤差
1244          */
1245         private static final double TOLERANT = 0.001;
1246
1247
1248         /**
1249          * コンストラクタ
1250          */
1251         public PreviewImagePanel() {
1252                 super();
1253
1254                 // 通常モード
1255                 bgColorMode = BackgroundColorMode.ALPHABREND;
1256
1257                 // 壁紙変更通知リスナ
1258                 wallpaperListener = new PropertyChangeListener() {
1259                         public void propertyChange(PropertyChangeEvent evt) {
1260                                 onChangeWallpaper();
1261                         }
1262                 };
1263
1264                 // 壁紙
1265                 wallpaper = new Wallpaper();
1266                 wallpaper.addPropertyChangeListener(wallpaperListener);
1267         }
1268
1269         /**
1270          * 画像を表示する.
1271          */
1272         @Override
1273         protected void paintComponent(Graphics g0) {
1274                 Graphics2D g = (Graphics2D) g0;
1275                 super.paintComponent(g);
1276
1277                 if (previewImgForDraw == null) {
1278                         return;
1279                 }
1280
1281                 // 倍率を適用した画像を画面の中央に配置できるように計算する.
1282                 // (画像が倍率適用済みであれば1倍とする)
1283                 Rectangle imgRct = adjustImageRectangle();
1284
1285                 // 表示用画像がスケール済みでない場合はレンダリングオプションを適用する.
1286                 if (scaledZoomFactor == null) {
1287                         Object renderingOption = getRenderingOption();
1288                         g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, renderingOption);
1289                 }
1290
1291                 // 背景処理
1292                 if (bgColorMode == BackgroundColorMode.ALPHABREND) {
1293                         // 表示の最大範囲 (可視領域外も含む)
1294                         int w = getWidth();
1295                         int h = getHeight();
1296                         wallpaper.drawWallpaper(g, w, h);
1297                 }
1298
1299                 // レンダリング
1300                 g.drawImage(previewImgForDraw,
1301                                 imgRct.x, imgRct.y,
1302                                 imgRct.x + imgRct.width, imgRct.y + imgRct.height,
1303                                 0, 0,
1304                                 previewImgForDraw.getWidth(), previewImgForDraw.getHeight(),
1305                                 null);
1306
1307                 // 通常モード以外のグリッド描画に該当するモードはグリッドを前景に描く
1308                 AppConfig appConfig = AppConfig.getInstance();
1309                 int drawGridMask = appConfig.getDrawGridMask();
1310                 if ((drawGridMask & bgColorMode.mask()) != 0) {
1311                         Color oldc = g.getColor();
1312                         try {
1313                                 g.setColor(appConfig.getPreviewGridColor());
1314                                 drawGrid(g, imgRct.x, imgRct.y, appConfig.getPreviewGridSize());
1315
1316                         } finally {
1317                                 g.setColor(oldc);
1318                         }
1319                 }
1320         }
1321
1322
1323         /**
1324          * グリッドを描画する.<br>
1325          * 開始位置の-1単位位置から画像サイズの+1単位までがグリッド範囲となる。
1326          *
1327          * @param g
1328          * @param offset_x
1329          *            開始位置
1330          * @param offset_y
1331          *            開始位置
1332          * @param unit
1333          *            グリッド単位(pixel)
1334          */
1335         protected void drawGrid(Graphics2D g, int offset_x, int offset_y, int unit) {
1336                 Rectangle clip = g.getClipBounds();
1337
1338                 int src_w = previewImg.getWidth();
1339                 int src_h = previewImg.getHeight();
1340                 int my = src_h / unit;
1341                 int mx = src_w / unit;
1342
1343                 int st_x = offset_x + (int)(-1 * unit * zoomFactor);
1344                 int en_x = offset_x + (int)((mx + 1) * unit * zoomFactor);
1345                 int w = en_x - st_x + 1;
1346
1347                 for (int y = -1; y <= my + 1; y++) {
1348                         int y1 = y * unit;
1349                         Rectangle rct = new Rectangle(
1350                                         st_x, offset_y + (int)(y1 * zoomFactor),
1351                                         w, 1);
1352                         if (clip == null || clip.intersects(rct)) {
1353                                 g.drawLine(rct.x, rct.y, rct.x + rct.width, rct.y);
1354                         }
1355                 }
1356
1357                 int st_y = offset_y + (int)(-1 * unit * zoomFactor);
1358                 int en_y = offset_y + (int)((my + 1) * unit * zoomFactor);
1359                 int h = en_y - st_y + 1;
1360
1361                 for (int x = -1; x <= mx + 1; x++) {
1362                         int x1 = x * unit;
1363                         Rectangle rct = new Rectangle(
1364                                         offset_x + (int)(x1 * zoomFactor), st_y,
1365                                         1, h);
1366                         g.drawLine(rct.x, rct.y, rct.x, rct.y + rct.height);
1367                 }
1368         }
1369
1370         /**
1371          * 現在の倍率に応じたレンダリングオプションを取得する.<br>
1372          *
1373          * @return レンダリングオプション
1374          */
1375         protected Object getRenderingOption() {
1376                 AppConfig appConfig = AppConfig.getInstance();
1377                 double rendringOptimizeThreshold;
1378                 if (bgColorMode == BackgroundColorMode.ALPHABREND) {
1379                         rendringOptimizeThreshold = appConfig.getRenderingOptimizeThresholdForNormal();
1380                 } else {
1381                         rendringOptimizeThreshold = appConfig.getRenderingOptimizeThresholdForCheck();
1382                 }
1383                 Object renderingHint;
1384                 if (zoomFactor < rendringOptimizeThreshold) {
1385                         // 補正を適用する最大倍率以内である場合
1386                         if (zoomFactor <= 1. || !appConfig.isEnableInterpolationBicubic()) {
1387                                 // 縮小する場合、もしくはバイキュービックをサポートしない場合
1388                                 renderingHint = RenderingHints.VALUE_INTERPOLATION_BILINEAR;
1389                         } else {
1390                                 // 拡大する場合でバイキュービックをサポートしている場合
1391                                 renderingHint = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
1392                         }
1393
1394                 } else {
1395                         // 補正を適用する最大倍率を超えている場合は補正なし.
1396                         renderingHint = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
1397                 }
1398                 return renderingHint;
1399         }
1400
1401         /**
1402          * 倍率と、画面のサイズと、表示するオリジナルの画像サイズをもとに、 倍率を適用した画像サイズを、画面に収まる位置に補正して返す.<br>
1403          * 返される矩形の幅と高さ(width, height)は拡大後の画像サイズに等しい.<br>
1404          * 拡大後の画像が画面よりも小さければセンタリングするように矩形の開始位置(x, y)がオフセットされる.<br>
1405          * そうでなければ矩形の開始位置(x, y)は0となる.<br>
1406          * 画像が設定されていなければ幅と高さがゼロの矩形が返される.<br>
1407          *
1408          * @return 画像を表示するオフセットと大きさ、もしくは空の矩形
1409          */
1410         public Rectangle adjustImageRectangle() {
1411                 if (previewImg == null) {
1412                         return new Rectangle(0, 0, 0, 0); // 幅・高さともにゼロ
1413                 }
1414                 int client_w = getWidth();
1415                 int client_h = getHeight();
1416
1417                 int src_w = previewImg.getWidth();
1418                 int src_h = previewImg.getHeight();
1419
1420                 int w = (int) round(src_w * zoomFactor);
1421                 int h = (int) round(src_h * zoomFactor);
1422
1423                 int offset_x = 0;
1424                 if (w < client_w) {
1425                         offset_x = (client_w - w) / 2;
1426                 }
1427                 int offset_y = 0;
1428                 if (h < client_h) {
1429                         offset_y = (client_h - h) / 2;
1430                 }
1431
1432                 return new Rectangle(offset_x, offset_y, w, h);
1433         }
1434
1435         /**
1436          * パネルのマウス座標から、実寸の画像のピクセル位置を返す.<br>
1437          * 画像が表示されていないか、範囲外であればnullを返す.<br>
1438          *
1439          * @param pt
1440          *            パネルの座標
1441          * @return 画像の位置、もしくはnull
1442          */
1443         public Point getImagePosition(Point pt) {
1444                 if (pt == null || previewImg == null) {
1445                         // プレビュー画像が設定されていなければnull
1446                         return null;
1447                 }
1448
1449                 Rectangle imgRct = adjustImageRectangle();
1450
1451                 if ( !imgRct.contains(pt.x, pt.y)) {
1452                         // 範囲外であればnull
1453                         return null;
1454                 }
1455
1456                 // オフセットを除去する.
1457                 Point ret = (Point) pt.clone();
1458                 ret.x -= imgRct.x;
1459                 ret.y -= imgRct.y;
1460
1461                 // 倍率を解除する.
1462                 ret.x = (int) floor(ret.x / zoomFactor);
1463                 ret.y = (int) floor(ret.y / zoomFactor);
1464
1465                 return ret;
1466         }
1467
1468         /**
1469          * 画像の位置から画面の位置を割り出す.<br>
1470          *
1471          * @param pt
1472          *            画像の位置
1473          * @return 画面の位置
1474          */
1475         public Point getMousePosition(Point pt) {
1476                 if (pt == null || previewImg == null) {
1477                         // プレビュー画像が設定されていなければnull
1478                         return null;
1479                 }
1480
1481                 Rectangle imgRct = adjustImageRectangle();
1482
1483                 // 表示倍率を加える
1484                 Point ret = (Point) pt.clone();
1485                 ret.x = (int) ceil(ret.x * zoomFactor);
1486                 ret.y = (int) ceil(ret.y * zoomFactor);
1487
1488                 // オフセットを加える
1489                 ret.x += imgRct.x;
1490                 ret.y += imgRct.y;
1491
1492                 return ret;
1493         }
1494
1495         /**
1496          * 指定した位置のRGB値を取得する.<br>
1497          * 範囲外の場合は0が返される.<br>
1498          *
1499          * @param pt
1500          *            イメージの位置
1501          * @return イメージのARGB値 (ビット順序は、A:24, R:16, G:8, B:0)
1502          */
1503         public int getImageARGB(Point pt) {
1504                 if (pt == null) {
1505                         throw new IllegalArgumentException();
1506                 }
1507                 try {
1508                         return previewImg.getRGB(pt.x, pt.y);
1509
1510                 } catch (RuntimeException ex) {
1511                         return 0; // 範囲外
1512                 }
1513         }
1514
1515         /**
1516          * 倍率を適用した画像パネルのサイズを計算し適用する.<br>
1517          * モードにより余白が加えられる.<br>
1518          */
1519         protected void recalcScaledSize() {
1520                 Dimension scaledSize = getScaledSize(true);
1521                 if (scaledSize != null) {
1522                         setPreferredSize(scaledSize);
1523                         revalidate();
1524                 }
1525         }
1526
1527         /**
1528          * 元画像の倍率適用後のサイズを返す.<br>
1529          * 元画像が設定されていなければnull.<br>
1530          * needOffsetがfalseであれば表示モードに関わらず、画像の拡大・縮小後の純粋なサイズ、
1531          * trueであれば余白が必要な表示モードの場合の余白が付与された場合のサイズが返される.<br>
1532          *
1533          * @param needOffset
1534          *            余白を加味したサイズが必要な場合はtrue
1535          * @return 倍率適用後のサイズ、もしくはnull
1536          */
1537         protected Dimension getScaledSize(boolean needOffset) {
1538                 if (previewImg == null) {
1539                         return null;
1540                 }
1541                 int src_w = previewImg.getWidth();
1542                 int src_h = previewImg.getHeight();
1543
1544                 int w = (int) round(src_w * zoomFactor);
1545                 int h = (int) round(src_h * zoomFactor);
1546
1547                 Dimension scaledSize = new Dimension(w, h);
1548
1549                 if (bgColorMode != BackgroundColorMode.ALPHABREND) {
1550                         // 通常モード以外は画像よりも少し大きめにすることで
1551                         // キャンバスに余白をつける
1552                         AppConfig appConfig = AppConfig.getInstance();
1553                         int unfilledSpace = appConfig.getPreviewUnfilledSpaceForCheckMode();
1554                         scaledSize.width += max(0, unfilledSpace * 2);
1555                         scaledSize.height += max(0, unfilledSpace * 2);
1556                 }
1557
1558                 return scaledSize;
1559         }
1560
1561         /**
1562          * プレビュー画像を設定する.
1563          *
1564          * @param previewImg
1565          */
1566         public void setPreviewImage(BufferedImage previewImg) {
1567                 BufferedImage oldimg = this.previewImg;
1568                 this.previewImg = previewImg;
1569
1570                 recalcScaledSize();
1571                 makeDrawImage(true);
1572                 repaint();
1573
1574                 firePropertyChange("previewImage", oldimg, previewImg);
1575         }
1576
1577         public BufferedImage getPreviewImage() {
1578                 return previewImg;
1579         }
1580
1581         /**
1582          * 壁紙を設定する.
1583          *
1584          * @param wallpaper
1585          */
1586         public void setWallpaper(Wallpaper wallpaper) {
1587                 if (wallpaper == null) {
1588                         throw new IllegalArgumentException();
1589                 }
1590                 if ( !this.wallpaper.equals(wallpaper)) {
1591                         Wallpaper wallpaperOld = this.wallpaper;
1592                         if (wallpaperOld != null) {
1593                                 wallpaperOld.removePropertyChangeListener(wallpaperListener);
1594                         }
1595                         this.wallpaper = wallpaper;
1596                         if (this.wallpaper != null) {
1597                                 this.wallpaper.addPropertyChangeListener(wallpaperListener);
1598                         }
1599                         firePropertyChange("wallpaper", wallpaperOld, this.wallpaper);
1600                         onChangeWallpaper();
1601                 }
1602         }
1603
1604         public Wallpaper getWallpaper() {
1605                 return wallpaper;
1606         }
1607
1608         protected void onChangeWallpaper() {
1609                 repaint();
1610         }
1611
1612         /**
1613          * 背景モード調整済みの表示用画像を作成する.
1614          *
1615          * @param changeImage
1616          *            画像の変更あり
1617          */
1618         protected void makeDrawImage(boolean changeImage) {
1619                 if (previewImg == null) {
1620                         // 画像が設定されていなければ空
1621                         this.previewImgForDraw = null;
1622                         scaledZoomFactor = null;
1623                         return;
1624                 }
1625
1626                 BufferedImage img;
1627                 if (changeImage || scaledZoomFactor != null) {
1628                         // 画像が変更されているか、スケール済みであれば
1629                         // 背景モードの再適用が必要.
1630                         if (bgColorMode == BackgroundColorMode.ALPHABREND) {
1631                                 // アルファブレンド通常モードは背景用にあえて作成する必要はない.
1632                                 img = previewImg;
1633
1634                         } else {
1635                                 // アルファブレンド通常モード以外は背景に作成する
1636                                 Color bgColor = wallpaper.getBackgroundColor();
1637                                 BackgroundColorFilter bgColorFilter = new BackgroundColorFilter(bgColorMode, bgColor);
1638                                 img = bgColorFilter.filter(previewImg, null);
1639                         }
1640
1641                 } else {
1642                         // 画像が変更されておらず、スケール済みでもなければ
1643                         // すでに作成済みの画像が使用できる.
1644                         img = previewImgForDraw;
1645                 }
1646
1647                 // レンダリングオプション
1648                 Object renderingOption = getRenderingOption();
1649
1650                 // バイキュービックでなければ、事前の拡大縮小は行わずに、表示時に行う.
1651                 if ( !renderingOption.equals(RenderingHints.VALUE_INTERPOLATION_BICUBIC)) {
1652                         previewImgForDraw = img;
1653                         scaledZoomFactor = null;
1654                         return;
1655                 }
1656
1657                 // バイキュービックの場合、倍率を適用したサイズに予め加工しておく.
1658                 Dimension scaledSize = getScaledSize(false);
1659                 BufferedImage offscreen = new BufferedImage(
1660                                 scaledSize.width, scaledSize.height, BufferedImage.TYPE_INT_ARGB);
1661                 Graphics2D g = offscreen.createGraphics();
1662                 try {
1663                         g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
1664                                         RenderingHints.VALUE_INTERPOLATION_BICUBIC);
1665
1666                         g.drawImage(img,
1667                                         0, 0, scaledSize.width, scaledSize.height,
1668                                         0, 0, img.getWidth(), img.getHeight(),
1669                                         null);
1670
1671                 } finally {
1672                         g.dispose();
1673                 }
1674                 previewImgForDraw = offscreen;
1675                 scaledZoomFactor = Double.valueOf(zoomFactor);
1676         }
1677
1678         public void setBackgroundColorMode(BackgroundColorMode bgColorMode) {
1679                 if (bgColorMode == null) {
1680                         throw new IllegalArgumentException();
1681                 }
1682                 if (this.bgColorMode != bgColorMode) {
1683                         BackgroundColorMode oldcm = bgColorMode;
1684                         this.bgColorMode = bgColorMode;
1685
1686                         makeDrawImage(true);
1687                         recalcScaledSize();
1688                         repaint();
1689
1690                         firePropertyChange("backgroundColorMode", oldcm, bgColorMode);
1691                 }
1692         }
1693
1694         public BackgroundColorMode setBackgroundColorMode() {
1695                 return bgColorMode;
1696         }
1697
1698         public void setZoomFactor(double zoomFactor) {
1699                 if (abs(zoomFactor - this.zoomFactor) > TOLERANT) {
1700                         // 0.001未満の差異は誤差とみなして反映しない.
1701                         double oldzoom = this.zoomFactor;
1702                         this.zoomFactor = zoomFactor;
1703
1704                         recalcScaledSize();
1705                         makeDrawImage(false);
1706                         repaint();
1707
1708                         firePropertyChange("zoomFactor", oldzoom, zoomFactor);
1709                 }
1710         }
1711
1712         public double getZoomFactor() {
1713                 return zoomFactor;
1714         }
1715
1716         /**
1717          * 倍率が100%であるか?
1718          *
1719          * @return 100%であればtrue
1720          */
1721         public boolean isDefaultZoom() {
1722                 return zoomFactor - 1 < TOLERANT;
1723         }
1724 }
1725
1726 /**
1727  * 倍率・背景モードを操作するための下部パネル用
1728  *
1729  * @author seraphy
1730  */
1731 class PreviewControlPanel extends JPanel {
1732         private static final long serialVersionUID = 1L;
1733
1734         private static final Logger logger = Logger.getLogger(PreviewControlPanel.class.getName());
1735
1736         protected static final String STRINGS_RESOURCE = "languages/previewpanel";
1737
1738         /**
1739          * ピン留めチェックボックス
1740          */
1741         private JCheckBox chkPinning;
1742
1743         /**
1744          * アルファ確認チェックボックス
1745          */
1746         private JCheckBox chkNoAlpha;
1747
1748         /**
1749          * グレースケール確認チェックボックス
1750          */
1751         private JCheckBox chkGrayscale;
1752
1753         /**
1754          * 倍率用スライダ
1755          */
1756         private JSlider zoomSlider;
1757
1758         /**
1759          * 倍率入力用コンボボックス
1760          */
1761         private JComboBox zoomCombo;
1762
1763
1764         /**
1765          * スライダの最小値
1766          */
1767         private static final int MIN_INDEX = -170;
1768
1769         /**
1770          * スライダの最大値
1771          */
1772         private static final int MAX_INDEX = 219;
1773
1774         /**
1775          * 最小倍率
1776          */
1777         private double minimumZoomFactor;
1778
1779         /**
1780          * 最大倍率
1781          */
1782         private double maximumZoomFactor;
1783
1784
1785         /**
1786          * 現在の倍率(100倍済み)
1787          */
1788         private int currentZoomFactorInt;
1789
1790         /**
1791          * 現在の背景色モード
1792          */
1793         private BackgroundColorMode backgroundColorMode;
1794
1795
1796         /**
1797          * 任意の底Aをもつ対数 logA(N)を計算して返す.
1798          *
1799          * @param a
1800          *            底
1801          * @param x
1802          *            引数
1803          * @return logA(N)
1804          */
1805         private static double logN(double a, double x) {
1806                 return log(x) / log(a);
1807         }
1808
1809         /**
1810          * 倍率(等倍を1とする)に対するスライダのインデックス値を返す.<br>
1811          * スライダは10ステップごとに前のステップの10%づつ増減する.(複利式)<br>
1812          *
1813          * @param zoomFactor
1814          *            倍率(1を等倍とする)
1815          * @return インデックス
1816          */
1817         private static int zoomFactorToIndex(double zoomFactor) {
1818                 return (int) round(logN((1. + 0.1), zoomFactor) * 10);
1819         }
1820
1821         /**
1822          * スライダのインデックス値から倍率(等倍を1とする)を返す.<br>
1823          * 10ステップごとに10%づつ増減する.(複利式)<br>
1824          *
1825          * @param index
1826          *            インデックス
1827          * @return 倍率(1を等倍とする)
1828          */
1829         private static double zoomFactorFromIndex(int index) {
1830                 return pow(1. + 0.1, index / 10.);
1831         }
1832
1833
1834         /**
1835          * コンストラクタ
1836          */
1837         public PreviewControlPanel() {
1838                 final Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
1839                                 .getLocalizedProperties(STRINGS_RESOURCE);
1840
1841                 final UIHelper uiHelper = UIHelper.getInstance();
1842
1843                 // ピンアイコン
1844                 Icon pinIcon = uiHelper.createTwoStateIcon(
1845                                 "icons/pin-icon1.png", "icons/pin-icon2.png");
1846
1847                 // ピンチェックボックス
1848                 chkPinning = new JCheckBox(pinIcon);
1849                 chkPinning.setToolTipText(strings.getProperty("tooltip.zoompanel.pinning"));
1850
1851                 // 円ボタン型チェックボックス用アイコンの実装
1852                 final Icon stateIcon = new Icon() {
1853                         public int getIconHeight() {
1854                                 return (int)(12 * uiHelper.getScaleX());
1855                         }
1856                         public int getIconWidth() {
1857                                 return (int)(6 * uiHelper.getScaleY());
1858                         };
1859                         public void paintIcon(Component c, Graphics g, int x, int y) {
1860                                 boolean sw = false;
1861                                 if (c instanceof AbstractButton) {
1862                                         AbstractButton btn = (AbstractButton) c;
1863                                         sw = btn.isSelected();
1864                                 }
1865
1866                                 int w = getIconWidth();
1867                                 int h = getIconHeight();
1868
1869                                 int s = min(w, h);
1870
1871                                 int ox = 0;
1872                                 int oy = 0;
1873                                 if (w > s) {
1874                                         ox = (w - s) / 2;
1875                                 }
1876                                 if (h > s) {
1877                                         oy = (h - s) / 2;
1878                                 }
1879
1880                                 if (sw) {
1881                                         AppConfig appConfig = AppConfig.getInstance();
1882                                         Color fillColor = appConfig.getSelectedItemBgColor();
1883                                         g.setColor(fillColor);
1884                                         g.fillOval(x + ox, y + oy, s, w);
1885                                 }
1886                                 g.setColor(Color.GRAY);
1887                                 g.drawOval(x + ox, y + oy, s, s);
1888                         }
1889                 };
1890
1891                 // アルファ確認とグレースケール確認用のチェックボックス
1892                 chkNoAlpha = new JCheckBox(stateIcon);
1893                 chkGrayscale = new JCheckBox(stateIcon);
1894
1895                 chkNoAlpha.setToolTipText(strings.getProperty("tooltip.zoompanel.checkalpha"));
1896                 chkGrayscale.setToolTipText(strings.getProperty("tooltip.zoompanel.checkgrayscale"));
1897
1898                 backgroundColorMode = BackgroundColorMode.ALPHABREND;
1899
1900                 final ChangeListener chkAlphaGrayChangeListener = new ChangeListener() {
1901                         public void stateChanged(ChangeEvent e) {
1902                                 onChangeCheckAlphaGray();
1903                         }
1904                 };
1905                 chkNoAlpha.addChangeListener(chkAlphaGrayChangeListener);
1906                 chkGrayscale.addChangeListener(chkAlphaGrayChangeListener);
1907
1908                 // 倍率スライダ
1909                 zoomSlider = new JSlider(JSlider.HORIZONTAL, MIN_INDEX, MAX_INDEX, 0);
1910                 zoomSlider.setToolTipText(strings.getProperty("tooltip.zoompanel.zoomfactor_slider"));
1911
1912                 // 倍率コンボ
1913                 zoomCombo = new JComboBox();
1914                 zoomCombo.setToolTipText(strings.getProperty("tooltip.zoompanel.zoomfactor_combo"));
1915
1916                 // 倍率の既定リストの設定と、最大・最小値の算定
1917                 minimumZoomFactor = zoomFactorFromIndex(zoomSlider.getMinimum());
1918                 maximumZoomFactor = zoomFactorFromIndex(zoomSlider.getMaximum());
1919
1920                 int minZoomRange = (int) round(minimumZoomFactor * 100.);
1921                 int maxZoomRange = (int) round(maximumZoomFactor * 100.);
1922
1923                 List<Integer> predefinedZoomRanges = getPredefinedZoomRanges();
1924                 for (int zoomRange : predefinedZoomRanges) {
1925                         if (zoomRange < minZoomRange) {
1926                                 minZoomRange = zoomRange;
1927                         }
1928                         if (zoomRange > maxZoomRange) {
1929                                 maxZoomRange = zoomRange;
1930                         }
1931                         zoomCombo.addItem(Integer.toString(zoomRange));
1932                 }
1933                 final int[] zoomRanges = {minZoomRange, maxZoomRange};
1934
1935                 currentZoomFactorInt = 100;
1936                 zoomCombo.setSelectedItem(Integer.toString(currentZoomFactorInt));
1937                 zoomCombo.setEditable(true);
1938                 if ( !Main.isMacOSX()) {
1939                         // Windows環境だとデフォルトで9桁分のテキストフィールドが作成され
1940                         // それがレイアウトの推奨サイズとして実際に使われてしまうため、
1941                         // 明示的に3桁にとどめておくようにオーバーライドする.
1942                         // Mac OS Xならば問題ない.
1943                         zoomCombo.setEditor(new BasicComboBoxEditor() {
1944                                 {
1945                                         editor = new JTextField(3) {
1946                                                 private static final long serialVersionUID = 1L;
1947                                                 @Override
1948                                                 public void setBorder(Border border) {
1949                                                         // 何もしない.
1950                                                 }
1951                                                 public void setText(String s) {
1952                                                         if (getText().equals(s)) {
1953                                                                 return;
1954                                                         }
1955                                                         super.setText(s);
1956                                                 }
1957                                         };
1958                                 }
1959                         });
1960                 }
1961
1962                 // スライダを変更することによりコンボボックスを変更する、
1963                 // もしくはコンボボックスを変更することでスライダを変更するが、
1964                 // 互いに通知を呼び合うことになるため他方を無視するためのセマフォ
1965                 final Semaphore changeLock = new Semaphore(1);
1966
1967                 zoomCombo.addActionListener(new ActionListener() {
1968                         public void actionPerformed(ActionEvent e) {
1969                                 boolean adjusted = false;
1970                                 String value = (String) zoomCombo.getSelectedItem();
1971                                 int zoomFactorInt;
1972                                 try {
1973                                         zoomFactorInt = Integer.parseInt(value);
1974                                         if (zoomFactorInt < zoomRanges[0]) {
1975                                                 zoomFactorInt = zoomRanges[0];
1976                                                 adjusted = true;
1977
1978                                         } else if (zoomFactorInt > zoomRanges[1]) {
1979                                                 zoomFactorInt = zoomRanges[1];
1980                                                 adjusted = true;
1981                                         }
1982
1983                                 } catch (RuntimeException ex) {
1984                                         zoomFactorInt = 100;
1985                                         adjusted = true;
1986                                 }
1987                                 if (adjusted) {
1988                                         zoomCombo.setSelectedItem(Integer.toString(zoomFactorInt));
1989                                         Toolkit tk = Toolkit.getDefaultToolkit();
1990                                         tk.beep();
1991                                 }
1992                                 if (changeLock.tryAcquire()) {
1993                                         try {
1994                                                 zoomSlider.setValue(zoomFactorToIndex(zoomFactorInt / 100.));
1995
1996                                         } finally {
1997                                                 changeLock.release();
1998                                         }
1999                                 }
2000                                 fireZoomFactorChange(zoomFactorInt);
2001                         }
2002                 });
2003
2004                 zoomSlider.addChangeListener(new ChangeListener() {
2005                         public void stateChanged(ChangeEvent e) {
2006                                 int index = zoomSlider.getValue();
2007                                 double zoomFactor = zoomFactorFromIndex(index);
2008                                 int zoomFactorInt = (int) round(zoomFactor * 100);
2009
2010                                 if (changeLock.tryAcquire()) {
2011                                         try {
2012                                                 zoomCombo.setSelectedItem(Integer.toString(zoomFactorInt));
2013
2014                                         } finally {
2015                                                 changeLock.release();
2016                                         }
2017                                         fireZoomFactorChange(zoomFactorInt);
2018                                 }
2019                         }
2020                 });
2021
2022                 // パーツの配備
2023
2024                 GridBagLayout gbl = new GridBagLayout();
2025                 setLayout(gbl);
2026
2027                 GridBagConstraints gbc = new GridBagConstraints();
2028                 gbc.gridx = 0;
2029                 gbc.gridy = 0;
2030                 gbc.ipadx = 0;
2031                 gbc.ipady = 0;
2032                 gbc.gridheight = 1;
2033                 gbc.gridwidth = 1;
2034                 gbc.fill = GridBagConstraints.NONE;
2035                 gbc.anchor = GridBagConstraints.CENTER;
2036                 gbc.insets = new Insets(0, 0, 0, 5);
2037                 gbc.weightx = 0.;
2038                 gbc.weighty = 0.;
2039
2040                 add(chkPinning, gbc);
2041
2042                 gbc.gridx = 1;
2043                 gbc.weightx = 0.;
2044                 gbc.insets = new Insets(0, 0, 0, 0);
2045                 add(chkGrayscale, gbc);
2046
2047                 gbc.gridx = 2;
2048                 gbc.weightx = 0.;
2049                 gbc.insets = new Insets(0, 0, 0, 5);
2050                 add(chkNoAlpha, gbc);
2051
2052                 gbc.gridx = 3;
2053                 gbc.weightx = 1.;
2054                 gbc.fill = GridBagConstraints.HORIZONTAL;
2055                 add(zoomSlider, gbc);
2056
2057                 gbc.gridx = 4;
2058                 gbc.weightx = 0.;
2059                 gbc.insets = new Insets(3, 0, 3, 0);
2060                 gbc.fill = GridBagConstraints.VERTICAL;
2061                 add(zoomCombo, gbc);
2062
2063                 Integer scrollbarWidth = (Integer) UIManager.get("ScrollBar.width");
2064                 logger.log(Level.CONFIG, "ScrollBar.width=" + scrollbarWidth);
2065                 if (scrollbarWidth == null) {
2066                         scrollbarWidth = Integer.parseInt(
2067                                         strings.getProperty("uiconstraint.scrollbar.width"));
2068                 }
2069
2070                 gbc.gridx = 5;
2071                 gbc.weightx = 0.;
2072                 gbc.anchor = GridBagConstraints.WEST;
2073                 gbc.insets = new Insets(0, 0, 0, scrollbarWidth);
2074                 add(new JLabel("%"), gbc);
2075         }
2076
2077         /**
2078          * アプリケーション設定より事前定義済みの倍率候補を取得する
2079          *
2080          * @return 倍率候補のリスト(順序あり)
2081          */
2082         protected List<Integer> getPredefinedZoomRanges() {
2083                 AppConfig appConfig = AppConfig.getInstance();
2084                 String strs = appConfig.getPredefinedZoomRanges();
2085                 TreeSet<Integer> ranges = new TreeSet<Integer>();
2086                 for (String str : strs.split(",")) {
2087                         str = str.trim();
2088                         if (str.length() > 0) {
2089                                 try {
2090                                         int zoomFactor = Integer.parseInt(str);
2091                                         ranges.add(Integer.valueOf(zoomFactor));
2092
2093                                 } catch (RuntimeException ex) {
2094                                         // 無視する.
2095                                 }
2096                         }
2097                 }
2098                 ranges.add(Integer.valueOf(100)); // 等倍は常に設定する.
2099                 return new ArrayList<Integer>(ranges);
2100         }
2101
2102         /**
2103          * 倍率が変更されたことを通知する.
2104          */
2105         protected void fireZoomFactorChange(int newZoomFactor) {
2106                 if (currentZoomFactorInt != newZoomFactor) {
2107                         int oldValue = currentZoomFactorInt;
2108                         currentZoomFactorInt = newZoomFactor;
2109                         firePropertyChange("zoomFactorInt", oldValue, newZoomFactor);
2110                 }
2111         }
2112
2113         private Semaphore changeChkLock = new Semaphore(1);
2114
2115         protected void onChangeCheckAlphaGray() {
2116                 changeChkLock.tryAcquire();
2117                 try {
2118                         BackgroundColorMode backgroundColorMode = BackgroundColorMode.valueOf(
2119                                         chkNoAlpha.isSelected(),
2120                                         chkGrayscale.isSelected()
2121                                         );
2122                         setBackgroundColorMode(backgroundColorMode);
2123
2124                 } finally {
2125                         changeChkLock.release();
2126                 }
2127         }
2128
2129         public BackgroundColorMode getBackgroundColorMode() {
2130                 return this.backgroundColorMode;
2131         }
2132
2133         public void setBackgroundColorMode(BackgroundColorMode backgroundColorMode) {
2134                 if (backgroundColorMode == null) {
2135                         throw new IllegalArgumentException();
2136                 }
2137
2138                 BackgroundColorMode oldcm = this.backgroundColorMode;
2139                 if (oldcm != backgroundColorMode) {
2140                         this.backgroundColorMode = backgroundColorMode;
2141                         changeChkLock.tryAcquire();
2142                         try {
2143                                 chkNoAlpha.setSelected(backgroundColorMode.isNoAlphaChannel());
2144                                 chkGrayscale.setSelected(backgroundColorMode.isGrayscale());
2145
2146                         } finally {
2147                                 changeChkLock.release();
2148                         }
2149                         firePropertyChange("backgroundColorMode", oldcm, backgroundColorMode);
2150                 }
2151         }
2152
2153         public boolean isPinned() {
2154                 return chkPinning.isSelected();
2155         }
2156
2157         public void setPinned(boolean pinned) {
2158                 chkPinning.setSelected(pinned);
2159                 if (isDisplayable()) {
2160                         setVisible(pinned);
2161                 }
2162         }
2163
2164         public double getZoomFactor() {
2165                 return (double) currentZoomFactorInt / 100.;
2166         }
2167
2168         public void setZoomFactor(double zoomFactor) {
2169                 if (zoomFactor < minimumZoomFactor) {
2170                         zoomFactor = minimumZoomFactor;
2171                 }
2172                 if (zoomFactor > maximumZoomFactor) {
2173                         zoomFactor = maximumZoomFactor;
2174                 }
2175                 int zoomFactorInt = (int) round(zoomFactor * 100.);
2176                 if (zoomFactorInt != currentZoomFactorInt) {
2177                         zoomCombo.setSelectedItem(Integer.toString(zoomFactorInt));
2178                 }
2179         }
2180 }