/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package TimeTable;
import TimeTable.model.TimeTableModel;
import TimeTable.model.TableModelListener;
import TimeTable.model.TimeTableModelDefault;
import TimeTable.relocators.RelocationEngine;
import TimeTable.relocators.SimpleRectangleEngine;
import TimeTable.relocators.SpanRelocationInfo;
import TimeTable.spans.SpanException;
import TimeTable.spans.TimeSpan;
import TimeTable.scaler.TimeScaler;
import TimeTable.scaler.TimeScalerFlat;
import TimeTable.scaler.TimeScalerListener;
import TimeTable.spans.TimeSpanListener;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import javax.swing.JComponent;
import java.awt.RenderingHints;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.Collections;
import java.util.Date;
/**
* Главный элемент управления
* @author Axe Ilshat
*/
public class JTimeTable extends JComponent
implements TimeSpanListener, TableModelListener, TimeScalerListener {
private ArrayList<TimeSpanInternal> spans = new ArrayList<TimeSpanInternal>();
protected static final int TTE_RESIZE = 1;
protected static final int TTE_MOVE = 2;
protected static final int TTE_OVER = 3;
protected static final int TTE_MOUSELEFT = 4;
protected static final int TTE_MOUSEENTER = 5;
protected static final int TTE_ACTIVATE = 6;
protected static final int TTE_CONTEXTMENU = 7;
protected static final int TTE_SPANMENU = 8;
private Dimension preferredSize =new Dimension(100, 300);
private Day day;
private TimeSpan mouseOverSpan;
private boolean canExceed24 = false;
private RelocationEngine relocator;
private TimeTableModel model;
private TimeScaler scaler;
boolean recalcLayout = true;
boolean rebuildInternal = true;
private int shiftBegin;
private int shiftEnd;
private GenericMouseEventsListener genericMouseEventsListener = new GenericMouseEventsListener();
private ReadOnlyMouseListener readOnlyMouseListener = new ReadOnlyMouseListener();
private boolean readOnly;
static AlphaComposite readOnlyBlend = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.15f);
static AlphaComposite disabledBlend = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.50f);
public JTimeTable() {
this(new Day(2007, 0, 1));
}
public JTimeTable(Day day) {
this(day, new SimpleRectangleEngine(), new TimeScalerFlat());
}
/**
*
* @param relocator
* @param scaler
*/
public JTimeTable(Day day, RelocationEngine relocator, TimeScaler scaler) {
this(day, new TimeTableModelDefault(), relocator, scaler);
}
/**
* Создает новый объект
* @param relocator - релокатор
* @param model - модель
* @param scaler - скалер
*/
public JTimeTable(Day day, TimeTableModel model, RelocationEngine relocator, TimeScaler scaler) {
this.day = day;
this.scaler = scaler;
scaler.addTimeScalerListener(this);
this.relocator = relocator;
addComponentListener(new ComponentListenerInt());
updateListeners();
setModel(model);
}
/**
*
* @param model
*/
public void setModel(TimeTableModel model) {
if (this.model != null) {
this.model.removeTimeModelListener(this);
}
this.model = model;
model.addTimeModelListener(this);
rebuildInternal = true;
rebuildInternalSpans();
recalcLayout = true;
repaint();
}
public void setScaler(TimeScaler scaler) {
if(this.scaler != null) {
this.scaler.removeTimeScalerListener(this);
}
this.scaler = scaler;
recalcLayout = true;
scaler.setMaxPoint(getHeight());
scaler.addTimeScalerListener(this);
repaint();
}
@Override
public void setEnabled(boolean enable) {
super.setEnabled(enable);
updateListeners();
repaint();
}
private void updateListeners() {
if(!isEnabled()) {
removeMouseListener(genericMouseEventsListener);
removeMouseListener(readOnlyMouseListener);
removeMouseMotionListener(genericMouseEventsListener);
removeMouseMotionListener(readOnlyMouseListener);
} else if(!isReadOnly()) {
removeMouseMotionListener(readOnlyMouseListener);
removeMouseListener(readOnlyMouseListener);
removeMouseListener(genericMouseEventsListener);
removeMouseMotionListener(genericMouseEventsListener);
addMouseListener(genericMouseEventsListener);
addMouseMotionListener(genericMouseEventsListener);
} else {
removeMouseListener(genericMouseEventsListener);
removeMouseMotionListener(genericMouseEventsListener);
removeMouseMotionListener(readOnlyMouseListener);
removeMouseListener(readOnlyMouseListener);
addMouseMotionListener(readOnlyMouseListener);
addMouseListener(readOnlyMouseListener);
}
}
/**
* Setups day for time table
* @param year
* @param month
* @param day
*/
public void setDay(Day day) {
this.day = day;
}
/**
*
* @return
*/
public final Day getDay() {
return day;
}
/**
*
* @return
*/
@Override
public Dimension getPreferredSize() {
return getMinimumSize();
}
/**
*
* @return
*/
@Override
public Dimension getMaximumSize() {
return preferredSize;
}
/**
*
* @return
*/
@Override
public Dimension getMinimumSize() {
return new Dimension(100, 300);
}
/**
* Setups new relocator engine
* @param relocator
*/
public void setRelocator(RelocationEngine relocator) {
this.relocator = relocator;
}
/**
* Paints component
* @param g
*/
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Insets insets = getInsets();
int currentWidth = getWidth() - insets.left - insets.right;
int currentHeight = getHeight() - insets.top - insets.bottom;
Graphics2D g2d = (Graphics2D) g.create();
//antialias on
RenderingHints rh = new RenderingHints(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHints(rh);
rh = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHints(rh);
//erase bg
if(shiftBegin != 0 && shiftEnd != 0) {
g2d.setColor(Color.white);
g2d.fillRect(insets.left, insets.top, currentWidth, currentHeight);
//SHIFTS
g2d.setColor(Color.GRAY);
int shiftY = scaler.getPosAtTime(shiftBegin);
if(shiftY > 0) {
g2d.fillRect(insets.left, insets.top, currentWidth, shiftY);
}
shiftY = scaler.getPosAtTime(shiftEnd);
if(shiftY > 0) {
g2d.fillRect(insets.left, shiftY, currentWidth, currentHeight-shiftY);
}
} else {
g2d.setColor(Color.GRAY);
g2d.fillRect(insets.left, insets.top, currentWidth, currentHeight);
}
//draw grid and rules
g2d.setColor(Color.LIGHT_GRAY);
g2d.drawRect(insets.left, insets.top, currentWidth, currentHeight);
int iHours = 1;
while (iHours < 24) {
int y2 = scaler.getPosAtTime(iHours * 3600 * 1000);
g2d.drawLine(insets.left, y2, currentWidth + insets.left, y2);
iHours++;
}
Rectangle rc = new Rectangle(insets.left, insets.top, currentWidth, currentHeight);
if (recalcLayout) {
rebuildInternalSpans();
recalcLayout = false;
}
//SPANS
Rectangle activeRC = null;
for (TimeSpanInternal s : spans) {
if (s.visible) {
if (mouseOverSpan != s.span) {
s.span.paint(g2d, false, s.rc);
} else {
activeRC = s.rc;
}
}
}
if (mouseOverSpan != null && activeRC != null) {
mouseOverSpan.paint(g2d, true, activeRC);
}
if(!isEnabled()){
g2d.setComposite(disabledBlend);
g2d.setColor(Color.BLACK);
g2d.fillRect(insets.left, insets.top, currentWidth, currentHeight);
}
//READ ONLY
else if(isReadOnly()) {
g2d.setComposite(readOnlyBlend);
g2d.setColor(Color.BLACK);
g2d.fillRect(insets.left, insets.top, currentWidth, currentHeight);
}
g2d.dispose();
}
/**
*
*/
public void relocateSpans() {
ArrayList<TimeSpanInternal> info = new ArrayList<TimeSpanInternal>();
long beginTime = day.getMilliseconds();
long endTime = beginTime + 24*3600*1000;
// Y coord
for (int i = 0; i < spans.size(); i++) {
TimeSpanInternal s = spans.get(i);
TimeSpan ts = s.span;
long spanBegin = ts.getBegin().getTime();
long spanEnd = spanBegin + ts.getLength()*60*1000;
//pass hidden
if (spanBegin >= endTime || spanEnd <= beginTime) {
s.visible = false;
continue;
}
if (s.relocateY) {
int begin = (int) (spanBegin - beginTime);
int end = (int) (spanEnd - beginTime);
s.rc.y = scaler.getPosAtTime(begin);
s.rc.height = scaler.getPosAtTime(end) - s.rc.y;
s.relocateY = false;
}
info.add(s);
s.visible = true;
}
//System.out.println("Relocate Y " + day + " size: " + info.size());
if (info.size() > 0) {
SpanRelocationInfo[] out = new SpanRelocationInfo[info.size()];
for (int i = 0; i < info.size(); i++) {
TimeSpanInternal s = info.get(i);
out[i] = new SpanRelocationInfo();
out[i].begin = s.span.getBegin().getTime();
out[i].duration = s.span.getLength()*60*1000;
}
Insets insets = getInsets();
int currentWidth = getWidth() - insets.left - insets.right;
relocator.updateTimeSpanPositions(out, insets.left, currentWidth);
for (int i = 0; i < out.length; i++) {
TimeSpanInternal s = info.get(i);
s.rc.x = out[i].x;
s.rc.width = out[i].width;
}
}
}
/**
*
*/
private void rebuildInternalSpans() {
if (rebuildInternal) {
for (int i = 0; i < spans.size(); i++) {
spans.get(i).span.removeTimeSpanListener(this);
}
spans.clear();
for (int i = 0; i < model.getSpanCount(); i++) {
TimeSpan span = model.getSpan(i);
TimeSpanInternal internal = new TimeSpanInternal();
internal.span = span;
internal.rc = new Rectangle();
internal.relocateY = true;
spans.add(internal);
span.addTimeSpanListener(this);
System.out.println("rebuild content: item added at " + span.getBegin()
+ ", length " + span.getLength() + " LOADED");
}
rebuildInternal = false;
System.out.println("rebuild content finished: " + model.getSpanCount() + " LOADED");
}
Collections.sort(spans);
relocateSpans();
}
/**
* Post custom event to avery listener
* @param type - event type
* @param span - event subject
*/
private void fireEvent(int type, TimeSpan span, int x, int y) {
switch (type) {
case TTE_RESIZE: {
model.spanResized(span, this);
break;
}
case TTE_MOVE: {
model.spanMoved(span, this);
break;
}
case TTE_OVER: {
model.mouseOverSpan(span, this);
break;
}
case TTE_MOUSELEFT: {
model.mouseLeftSpan(span, this);
break;
}
case TTE_MOUSEENTER: {
model.mouseEnterSpan(span, this);
break;
}
case TTE_ACTIVATE: {
model.spanActivated(span, this);
break;
}
case TTE_CONTEXTMENU: {
model.contextMenu(this, x, y);
break;
}
case TTE_SPANMENU: {
model.spanContextMenu(span, this, x, y);
break;
}
}
}
public boolean isCanExceed24() {
return canExceed24;
}
public void setCanExceed24(boolean canExceed24) {
this.canExceed24 = canExceed24;
}
/**
* Возвращает время начала смены.
* Время до начала смены и после конца смены закрашивается серым цветом
* @return миллисекунды с начала суток
*/
public int getShiftBegin() {
return shiftBegin;
}
/**
* Задает время начала смены.
* Время до начала смены и после конца смены закрашивается серым цветом
* @param shiftBegin - миллисекунды с начала суток
*/
public void setShiftBegin(int shiftBegin) {
this.shiftBegin = shiftBegin;
}
/**
* Возвращает время конца смены.
* Время до начала смены и после конца смены закрашивается серым цветом
* @return миллисекунды с начала суток
*/
public int getShiftEnd() {
return shiftEnd;
}
/**
* Задает время конца смены.
* Время до начала смены и после конца смены закрашивается серым цветом
* @param shiftBegin миллисекунды с начала суток
*/
public void setShiftEnd(int shiftEnd) {
this.shiftEnd = shiftEnd;
}
/**
* Является ли элемент только для чтения
* @return
*/
public boolean isReadOnly() {
return readOnly;
}
/**
* Устанаваливает состояние элемента в "только для чтения" или "чтения-записи"
* @param readOnly
*/
public void setReadOnly(boolean readOnly) {
this.readOnly = readOnly;
updateListeners();
}
/**
* возвращает количество миллисекунд, прошедших с начала суток для
* координаты y
* @param y координата в пикселях
*/
public int getTimeAt(int y) {
return scaler.getTimeAtPos(y);
}
/**
* Helper class
*/
private class ComponentListenerInt implements ComponentListener {
public void componentResized(ComponentEvent e) {
Insets insets = getInsets();
int currentHeight = getHeight() - insets.top - insets.bottom;
scaler.setMaxPoint(currentHeight);
preferredSize.height = scaler.getMinHeight();
for (int i = 0; i < spans.size(); i++) {
TimeSpanInternal timeSpanInternal = spans.get(i);
timeSpanInternal.relocateY = true;
}
relocateSpans();
repaint();
}
public void componentMoved(ComponentEvent e) {}
public void componentShown(ComponentEvent e) {}
public void componentHidden(ComponentEvent e) { }
} // end of ComponentListener
/**
* Helper class
* Processes mouse input events
*/
private class GenericMouseEventsListener implements MouseListener, MouseMotionListener {
TimeSpan spanDragged;
TimeSpan spanResizedTop;
TimeSpan spanResizedBottom;
int dragOffsetY;
/**
* @param event
*/
public void mouseEntered(MouseEvent event) {
}
/**
* @param event
*/
public void mouseExited(MouseEvent event) {
if (spanDragged == null) {
if (mouseOverSpan != null) {
fireMouseEvents(mouseOverSpan, null, event.getX(), event.getY());
mouseOverSpan = null;
repaint();
}
}
}
/**
*
* @param event
*/
public void mouseClicked(MouseEvent event) {
if(!event.isPopupTrigger()) {
if (event.getClickCount() == 2 && mouseOverSpan != null) {
fireEvent(TTE_ACTIVATE, mouseOverSpan, event.getX(), event.getY());
}
}
}
private boolean mouseContext(MouseEvent event) {
if (event.isPopupTrigger()) {
System.out.println("Mouse context ");
if (mouseOverSpan != null) {
fireEvent(TTE_SPANMENU, mouseOverSpan, event.getX(), event.getY());
} else {
fireEvent(TTE_CONTEXTMENU, mouseOverSpan, event.getX(), event.getY());
}
return true;
}
return false;
}
/**
*
* @param event
*/
public void mouseReleased(MouseEvent event) {
if(!mouseContext(event)) {
if (spanDragged != null) {
fireEvent(TTE_MOVE, spanDragged, event.getX(), event.getY());
spanDragged = null;
repaint();
} else if (spanResizedBottom != null) {
fireEvent(TTE_RESIZE, spanResizedBottom, event.getX(), event.getY());
spanResizedBottom = null;
repaint();
} else if (spanResizedTop != null) {
fireEvent(TTE_RESIZE, spanResizedTop, event.getX(), event.getY());
spanResizedTop = null;
repaint();
}
}
}
public void mousePressed(MouseEvent event) {
if(mouseContext(event)) {
return;
}
Point pt = event.getPoint();
//System.out.println("Mouse pressed: " + pt.x + "," + pt.y);
for (int i = spans.size() - 1; i >= 0; i--) {
TimeSpanInternal si = spans.get(i);
TimeSpan span = si.span;
int resize = span.checkPointResize(pt, si.rc);
if (resize != TimeSpan.NORESIZE) {
if (resize == TimeSpan.RESIZE_TOP) {
spanResizedTop = span;
spanResizedBottom = null;
} else if (resize == TimeSpan.RESIZE_BOTTOM) {
spanResizedTop = null;
spanResizedBottom = span;
}
break;
} else if (span.checkOverDragArea(pt, si.rc)) {
//System.out.println("Drag on: " + pt.x + "," + pt.y);
spanDragged = span;
dragOffsetY = si.rc.y - pt.y;
break;
}
}
}
/**
*
* @param event
*/
public void mouseDragged(MouseEvent event) {
Point pt = event.getPoint();
Insets insets = getInsets();
if (spanDragged != null) {
pt.y += dragOffsetY;
long time = day.getMilliseconds() + scaler.getTimeAtPos(pt.y - insets.top);
if (!isCanExceed24()) {
long maxTime = day.getMilliseconds() + 24*3600*1000 - (spanDragged.getLength()*60*1000);
if (time > maxTime) {
time = maxTime;
}
if (time < day.getMilliseconds()) {
time = day.getMilliseconds();
}
}
time = granulateTime(time, spanDragged.getTimeGranulation());
spanDragged.setBegin(new Date(time));
repaint();
} else if (spanResizedTop != null) {
long time = day.getMilliseconds() + scaler.getTimeAtPos(pt.y - insets.top);
if (!isCanExceed24()) {
if (time < day.getMilliseconds()) {
time = day.getMilliseconds();
}
}
time = granulateTime(time, spanResizedTop.getTimeGranulation());
try {
spanResizedTop.resizeBegin(new Date(time));
} catch (SpanException ex) {
ex.showError(JTimeTable.this);
spanResizedTop = null;
}
repaint();
} else if (spanResizedBottom != null) {
long time = day.getMilliseconds() + scaler.getTimeAtPos(pt.y - insets.top);
if (!isCanExceed24()) {
long maxTime = day.getMilliseconds() + 24*3600*1000;
if (time > maxTime) {
time = maxTime;
}
}
time = granulateTime(time, spanResizedBottom.getTimeGranulation());
try {
spanResizedBottom.resizeEnd(new Date(time));
} catch (SpanException ex) {
ex.showError(JTimeTable.this);
spanResizedBottom = null;
}
repaint();
}
}
/**
* granulates time
* @param time
* @return
*/
protected long granulateTime(long time, int granulation) {
int f = granulation * 30000;
long granulated = ((time + f / 2) / f);
return granulated * f;
}
/**
*
* @param event
*/
public void mouseMoved(MouseEvent event) {
if (spanDragged != null) {
return;
}
Point pt = event.getPoint();
TimeSpan last = mouseOverSpan;
mouseOverSpan = null;
//find active timespan
for (int i = spans.size() - 1; i >= 0; i--) {
TimeSpanInternal si = spans.get(i);
if (!si.visible) {
continue;
}
TimeSpan span = si.span;
int resize = span.checkPointResize(pt, si.rc);
if (resize != TimeSpan.NORESIZE) {
if (resize == TimeSpan.RESIZE_TOP) {
mouseOverSpan = span;
setCursor(Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR));
} else if (resize == TimeSpan.RESIZE_BOTTOM) {
mouseOverSpan = span;
setCursor(Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR));
}
break;
} else if (span.checkOverDragArea(pt, si.rc)) {
mouseOverSpan = span;
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
break;
} else {
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
}
//events handling
fireMouseEvents(last, mouseOverSpan, event.getX(), event.getY());
if (last != mouseOverSpan) {
repaint();
}
}
/**
* Post LEFT and ENTER events to listeners
* @param oldOver - prevouse mouse over span
* @param currentOver - current mouse over span
* @param x - x mouse cursor pos
* @param y - y mouse cursor pos
*/
protected void fireMouseEvents(TimeSpan oldOver, TimeSpan currentOver, int x, int y) {
if (oldOver != currentOver) {
if (oldOver != null) {
fireEvent(TTE_MOUSELEFT, oldOver, x, y);
}
if (currentOver != null) {
fireEvent(TTE_MOUSEENTER, currentOver, x, y);
}
}
}
} // end of mouseHandling
class ReadOnlyMouseListener implements MouseMotionListener, MouseListener {
public void mouseDragged(MouseEvent arg0) {}
public void mouseMoved(MouseEvent event) {
Point pt = event.getPoint();
TimeSpan last = mouseOverSpan;
mouseOverSpan = null;
//find active timespan
for (int i = spans.size() - 1; i >= 0; i--) {
TimeSpanInternal si = spans.get(i);
if (!si.visible) {
continue;
}
TimeSpan span = si.span;
if (span.checkOverDragArea(pt, si.rc)) {
mouseOverSpan = span;
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
break;
} else {
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
}
if (last != mouseOverSpan) {
repaint();
}
}
public void mouseClicked(MouseEvent arg0) {}
public void mousePressed(MouseEvent arg0) {}
public void mouseReleased(MouseEvent arg0) {}
public void mouseEntered(MouseEvent arg0) {}
public void mouseExited(MouseEvent arg0) {
if (mouseOverSpan != null) {
mouseOverSpan = null;
repaint();
}
}
}
/**
* Вызывается спаном, если у него изменилось время начала или конца.
* Устанавливает флаг необходимости перерасчета позиции спанов.
* @param span
*/
public void timeSpanChanged(TimeSpan span) {
for (int i = 0; i < spans.size(); i++) {
TimeSpanInternal si = spans.get(i);
if (span == si.span) {
si.relocateY = true;
}
}
recalcLayout = true;
repaint();
}
/**
* Вызывается спаном, если у него изменилось время начала или конца.
* Устанавливает флаг необходимости перерасчета
*/
public void spanCountChanged() {
recalcLayout = true;
rebuildInternal = true;
repaint();
}
public void scaleChanged() {
recalcLayout = true;
for (int i = 0; i < spans.size(); i++) {
spans.get(i).relocateY = true;
}
repaint();
}
}
/**
* Класс используется для того что бы хранить ссылки на
* объекты-спаны внтури элемента управления.
* Кроме всего прочего хранят информацию о том, видим ли
* спан, его размеры и необходим ли перерасчет его позиции
* по оси У
* @author Axe Ilshat
*/
class TimeSpanInternal implements Comparable<TimeSpanInternal> {
public TimeSpan span;
public Rectangle rc;
public boolean visible;
public boolean relocateY;
/**
* Comparator. Need for sorting.
* @param o
* @return
*/
public int compareTo(TimeSpanInternal o) {
if (span.getBegin().equals(o.span.getBegin())) {
return 0;
}
return span.getBegin().after(o.span.getBegin()) ? 1 : -1;
}
}