OSDN Git Service

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