package open.dolphin.ui;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import open.dolphin.client.KartePanel;
/**
* カルテをスワイプスクロール
*
* viewPos
* +---------------+
* (i-1)| (i) | (i+1)
* +----+--------+----------+-----
* | |<-off | | | KartePanel
* | | set->| | |
* +----+--------+----------+-----
* | |
* +---------------+ ViewPort
* i=karePageNumber
*
* @author pns
*/
public class KarteScrollPane extends MyJScrollPane {
private static final long serialVersionUID = 1L;
// 最初は viewport.getView() を一気に BufferedImage にしようとしたが,カルテが多くなると OutOfMemorryError になってしまった
// モニタの広さだけのバッファを1つ用意して,それを使い回すようにした。それでも,heap は -Xmx512m 以上にしないときつい
// ここのメモリ使い方はもう少し工夫の余地がありそう
// private static final Dimension SCREEN_SIZE = Toolkit.getDefaultToolkit().getScreenSize();
// その後,メモリ節約バージョン作成:カルテサイズ変更しなければ大幅メモリ節約だがサイズ変更すると結構大変バージョン
private BufferedImage snapImg = null;
// カルテページの先頭の画像を入れるバッファ
private BufferedImage headImg = null;
// snapImg と headImg のオフセット
private Dimension offset = new Dimension(0,0);
// カルテの各ページの先頭位置を保持する
private List<Point> positionList = null;
// 表示中のカルテページ番号
private int kartePageNumber = 0;
// スクロールの方向
private int direction;
// 現在の表示位置
private Point viewPos = new Point(-1,-1);
// 表示位置の変化を検出する
private Point prevViewPos;
// 表示している区画
private Rectangle viewRect;
// viewRect の変化を検出するための変数
private Rectangle prevViewRect = new Rectangle(0,0,0,0);
// 不透明で塗りつぶすための alpha 値
private static final AlphaComposite opaque = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f);
// 影をつけるための半透明の alpha 値
private static final AlphaComposite translucent[] = {
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.6f),
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.2f),
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.10f),
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.06f),
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.04f),
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.02f),
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.01f),
};
// mouse caputure するパネル
private JPanel mouseCapturePanel = null;
// 親フレームの Container
// mouseCapturePanel のイベントを contentPane に伝えると,イベントを pass through できる
private Container contentPane = null;
// view component に変化があったかどうか KarteDocumentViewer からセットされる
private boolean viewComponentChanged = false;
public KarteScrollPane() {
super();
// isClassicScrollBar = false;
}
// getPositionList で使う comparator
private Comparator<Point> xComparator = new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
return o1.x - o2.x;
}
};
private Comparator<Point> yComparator = new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
return o1.y - o2.y;
}
};
/**
* paintChildern で描画しておく
* 後で,MyJScrollPane の paint で,スクロールバーを書いてもらう
* @param graphics
*/
@Override
protected void paintChildren(Graphics graphics){
Graphics2D g = (Graphics2D) graphics;
super.paintChildren(g);
// null check
if (viewport.getView() == null) { return; }
// offset = viewRect の頭から次ページカルテの頭までの距離
offset.width = 0; offset.height = 0;
block:{
// カルテの各ページの viewPosition を調べる
// 表示位置によって動的に変わるので毎回新たに調べなくてはならない
positionList = getPositionList();
// 横スクロール
if (direction == ScrollBar.HORIZONTAL) {
// 現在の viewport のサイズ
viewRect = viewport.getVisibleRect();
// スクロールの必要がない場合は帰る
if (viewRect.width >= viewport.getView().getWidth()) { break block; }
// 現在の表示位置
prevViewPos = viewPos;
viewPos = viewport.getViewPosition();
// スクロールに応じて,必要なスナップショットを取る
// ページ境界検出
int currentPage = 0;
for (int i=0; i<positionList.size(); i++) {
int x = positionList.get(i).x;
if (x > viewPos.x) {
if (x < viewPos.x + viewRect.width) {
// viewport にページ境界がある場合
currentPage = i;
}
break;
}
}
// ページ境界がなければ,そのまま帰る
if (currentPage == 0) { break block; }
offset.width = positionList.get(currentPage).x - viewPos.x;
if (positionList.size() > 1
&& (kartePageNumber != currentPage || viewRectChanged() || viewComponentChanged()) ){
kartePageNumber = currentPage;
createBufferedImage();
snap();
}
// 現在の viewRect を保存
prevViewRect = viewRect;
// viewport の頭をスナップショットの頭で上書きしちゃう
if (viewRect.height < snapImg.getHeight()) {
headImg = snapImg.getSubimage(0, 0, offset.width, viewRect.height);
} else {
headImg = snapImg.getSubimage(0, 0, offset.width, snapImg.getHeight());
}
// 不透明で塗りつぶしてから
//g.setComposite(opaque);
//g.setColor(this.getBackground());
//g.fillRect(0, 0, offset.width, viewRect.height);
// スナップショットを上書き
g.drawImage(headImg, 0, 0, null);
// 素敵な shadow をつける
g.setColor(Color.BLACK);
for (int i=0; i<translucent.length; i++) {
g.setComposite(translucent[i]);
g.drawLine(offset.width-i, 0, offset.width-i, viewRect.height);
}
// 縦スクロール
// 同じコードの繰り返しでかっこわるいけどスピード優先
} else if (direction == ScrollBar.VERTICAL) {
// 現在の viewport のサイズ
viewRect = viewport.getVisibleRect();
// スクロールの必要がない場合は帰る
if (viewRect.height >= viewport.getView().getHeight()) { break block; }
// 現在の表示位置
prevViewPos = viewPos;
viewPos = viewport.getViewPosition();
// スクロールに応じて,必要なスナップショットを取る
// ページ境界検出
int currentPage = 0;
for (int i=0; i<positionList.size(); i++) {
int y = positionList.get(i).y;
// y が viewPos.y を越えたら,ページ境界が入っている可能性あり
if (y > viewPos.y) {
// viewRect 内に入っていれば,ページ境界あり
// ただし,前のページの大きさが viewRect.y より大きかったらスナップ撮らないことにする
if (y < viewPos.y + viewRect.height
&& (i != 0 && (y - positionList.get(i-1).y) < viewRect.height)) {
currentPage = i;
}
break;
}
}
// ページ境界がなければ,そのまま帰る
if (currentPage == 0) { break block; }
// ページ境界と viewRect の頭との offset
offset.height = positionList.get(currentPage).y - viewPos.y;
if (positionList.size() > 1
&& (kartePageNumber != currentPage || viewRectChanged() || viewComponentChanged()) ){
kartePageNumber = currentPage;
createBufferedImage();
snap();
}
// 現在の viewRect を保存
prevViewRect = viewRect;
// カルテの頭をスナップショットで上書き
if (viewRect.width < snapImg.getWidth()) {
headImg = snapImg.getSubimage(0, 0, viewRect.width, offset.height);
} else {
headImg = snapImg.getSubimage(0, 0, snapImg.getWidth(), offset.height);
}
// 塗りつぶしてから
//g.setComposite(opaque);
//g.setColor(this.getBackground());
//g.fillRect(0, 0, viewRect.width, offset.height);
// 上書き
g.drawImage(headImg, 0, 0, null);
// shadow をつける
g.setColor(Color.BLACK);
for (int i=0; i<translucent.length; i++) {
g.setComposite(translucent[i]);
g.drawLine(0, offset.height-i-1, viewRect.width, offset.height-i-1);
}
}
}
// break block でここまでくる
}
/**
* カルテのサイズ変更があったかどうか
* @return
*/
private boolean viewRectChanged() {
return viewRect.height != prevViewRect.height || viewRect.width != prevViewRect.width;
}
/**
* view component に変化があったかどうか
* @return
*/
private boolean viewComponentChanged() {
if (viewComponentChanged) {
viewComponentChanged = false;
return true;
}
return false;
}
/**
* view component に変更があった場合に呼ぶ
* KarteDocumentViewer からセットする
*/
public void setViewComponentChanged() {
viewComponentChanged = true;
}
/**
* Viewport 変更に応じて,BufferedImage を作り直す
*/
private void createBufferedImage() {
// snapImage バッファを作る
if (snapImg == null) {
snapImg = new BufferedImage(viewRect.width, viewRect.height, BufferedImage.TYPE_3BYTE_BGR);
} else if (viewRectChanged()) {
snapImg.flush();
snapImg = new BufferedImage(viewRect.width, viewRect.height, BufferedImage.TYPE_3BYTE_BGR);
}
// mouse capture panel を作る
if (mouseCapturePanel == null) {
createMouseCapturePanel();
} else if (viewRectChanged()) {
mouseCapturePanel.setBounds(SwingUtilities.convertRectangle(viewport, viewRect, parentLayer));
}
}
/**
* viewport.serViewPosition で使う座標をリストアップする
* @return
*/
private List<Point> getPositionList() {
List<Point> index = new ArrayList<Point>();
JPanel child = (JPanel) viewport.getView();
int checksumX = 0, checksumY = 0;
for(Component c : child.getComponents()) {
if (c instanceof KartePanel) {
Point p = SwingUtilities.convertPoint(c, 0, 0, child);
index.add(p);
checksumX += p.x;
checksumY += p.y;
}
}
if (checksumX != 0) {
// これは横スクロール
direction = ScrollBar.HORIZONTAL;
// 念のためソートしておく
Collections.sort(index, xComparator);
} else if (checksumY != 0) {
// これは縦スクロール
direction = ScrollBar.VERTICAL;
// 念のためソートしておく
Collections.sort(index, yComparator);
} else {
direction = ScrollBar.NO_ORIENTATION;
index = null;
}
return index;
}
/**
* マウスイベント捕獲のための JPanel を作る
* @return
*/
private JPanel createMouseCapturePanel() {
//
if (parentLayer == null) {
getParentLayer();
}
if (contentPane == null) {
contentPane = ((JFrame) SwingUtilities.getWindowAncestor(this)).getContentPane();
}
mouseCapturePanel = new JPanel();
parentLayer.add(mouseCapturePanel, JLayeredPane.DRAG_LAYER - 1); // MyJScrollPane より一歩下がる
mouseCapturePanel.setBounds(SwingUtilities.convertRectangle(viewport, viewRect, parentLayer));
mouseCapturePanel.setOpaque(false);
//mouseCapturePanel.setBorder(BorderFactory.createLineBorder(Color.red));
mouseCapturePanel.addMouseListener(new PassThroughMouseListener());
mouseCapturePanel.addMouseMotionListener(new PassThroughMouseListener());
mouseCapturePanel.addMouseWheelListener(new PassThroughMouseListener());
return mouseCapturePanel;
}
/**
* マウスイベント捕獲のための JPanel を破棄する
*/
private void disposeMouseCapturePanel() {
if (mouseCapturePanel != null) {
parentLayer.remove(mouseCapturePanel);
mouseCapturePanel = null;
}
}
/**
* 捕獲パネルのマウスイベントを,単純に contentPane に pass through する
* @param e
*/
private void passThroughMouseEvent(MouseEvent e) {
// 捕獲パネルでのマウス座標を,contentPane のマウス座標に変換
Point p = SwingUtilities.convertPoint((JPanel)e.getSource(), e.getPoint(), contentPane);
// contentPane のマウス場所のコンポネントを得る
Component target = contentPane.findComponentAt(p);
// そのコンポネントにイベントを送る
if (target != null) {
target.dispatchEvent(SwingUtilities.convertMouseEvent((JPanel)e.getSource(), e, target));
//if (e.getID() != MouseEvent.MOUSE_MOVED) System.out.println("target=" + target.getClass());
SwingUtilities.invokeLater(new Runnable(){
@Override
public void run() {
repaint();
}
});
}
}
/**
* 見せかけの部分を実体に変換して event dispatch する
* @param e
*/
private void translateMouseEvent(MouseEvent e) {
JComponent viewCompo = (JComponent) viewport.getView();
// 表示されている部分
// Point head = positionList.get(kartePageNumber);
Point head = getHeadPoint();
// viewCompo 上での実際のマウス位置
Point p = new Point(head.x + e.getX(), head.y + e.getY());
// viewCompo 上で,その位置の component (StampHolder) を取る
Component target = viewCompo.findComponentAt(p);
if (target != null) {
// target 上でのマウスイベントに変換
MouseEvent event = SwingUtilities.convertMouseEvent((JPanel)e.getSource(), e, target);
// 実際の位置に変換
event.translatePoint(head.x - viewPos.x, head.y - viewPos.y);
// target に対して event 発行
target.dispatchEvent(event);
// viewPosition がずれることがあるので補正
SwingUtilities.invokeLater(new Runnable(){
@Override
public void run() {
//viewport.setViewPosition(viewPos);
repaint();
}
});
}
}
/**
* 見せかけとして表示されている部分の先頭座標
* kartePagenNumber は見せかけの部分の次のページを指している
* viewRect より小さければ前のページの頭 positionsList.get(kartePageNumber-1) になるが,
* viewRect より大きいと,はみ出した分を補正しなくてはならない
* residue = はみ出した分 = 見せかけ部分のページの長さ - viewRect
* @return
*/
private Point getHeadPoint() {
if (positionList == null) {
System.out.println("KarteScrollPane: positionList is null at getHeadPoint");
return new Point(0,0);
}
Point head = new Point(positionList.get(kartePageNumber-1));
Point current = new Point(positionList.get(kartePageNumber));
Point residue = new Point(current.x - head.x - viewRect.width, current.y - head.y - viewRect.height);
if (residue.x > 0) { head.x += residue.x; }
if (residue.y > 0) { head.y += residue.y; }
return head;
}
//long l;
/**
* スナップを撮る
*/
private void snap() {
//l = System.currentTimeMillis();
int compCount = ((JComponent)viewport.getView()).getComponentCount();
if (compCount <= kartePageNumber) {
System.out.println("---- something is wrong");
System.out.println("---- kartePageNumber = " + kartePageNumber);
System.out.println("---- compCount = " + compCount);
return;
}
final JPanel p = (JPanel)((JComponent)viewport.getView()).getComponent(kartePageNumber-1);
if (! p.isValid()) {
System.out.println("invalid component = " + (kartePageNumber-1));
// viewport.setViewPosition(getHeadPoint());
// viewport.paint(snapImg.getGraphics());
// viewport.setViewPosition(viewPos);
// } else {
// p.paint(snapImg.getGraphics());
}
p.paint(snapImg.getGraphics());
//((JComponent)viewport.getView()).getComponent(kartePageNumber-1).paint(snapImg.getGraphics());
//System.out.println("viewport.paint lap = " + (System.currentTimeMillis() -l));
}
/**
* 見せかけの部分にいるかどうか
* @param e
* @return
*/
private boolean shouldTranslate(MouseEvent e) {
// 縦スクロールなら offset.width は 0 になってる
return (e.getX() < offset.width || e.getY() < offset.height)
&& (offset.width < viewRect.width && offset.height < viewRect.height);
}
/**
* 見せかけの部分(headImg部分)で発生した event を,実体の event に変換して伝えるリスナ
*/
private class PassThroughMouseListener implements MouseListener, MouseMotionListener, MouseWheelListener {
/**
* 捕獲したマウスイベントを処理する
* @param e
*/
private void processMouseEvent(MouseEvent e) {
// タブが切り替わって KarteScrollPane が見えなくなると isShowing == false となる
// そしたら,今のイベントを pass through してから,マウス捕獲 JPanel を消去する
if (isShowing()) {
if (shouldTranslate(e) && !e.isPopupTrigger()) {
translateMouseEvent(e);
//System.out.println("translate: " + e.getID());
} else {
passThroughMouseEvent(e);
//System.out.println("pass through: " + e.getID());
}
// マウスでクリックしたときスタンプの選択枠を再描画するに snap 必要
if (positionList != null) {
SwingUtilities.invokeLater(new Runnable(){
@Override
public void run() {
snap();
}
} );
}
} else {
passThroughMouseEvent(e);
disposeMouseCapturePanel();
//System.out.println("dispose: " + e.getID());
}
}
// process 必要なイベント
@Override
public void mouseClicked(MouseEvent e) {
//System.out.println("clicked");
processMouseEvent(e);
}
@Override
public void mousePressed(MouseEvent e) {
//System.out.println("pressed");
processMouseEvent(e);
}
@Override
public void mouseReleased(MouseEvent e) {
processMouseEvent(e);
}
@Override
public void mouseDragged(MouseEvent e) {
processMouseEvent(e);
}
// pass through でよいイベント
@Override
public void mouseEntered(MouseEvent e) {
passThroughMouseEvent(e);
}
@Override
public void mouseExited(MouseEvent e) {
passThroughMouseEvent(e);
}
@Override
public void mouseMoved(MouseEvent e) {
passThroughMouseEvent(e);
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
passThroughMouseEvent(e);
}
}
}