/* * Copyright (C) Jakub Neubauer, 2007 * * This file is part of TaskBlocks * * TaskBlocks is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * TaskBlocks is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package taskblocks.graph; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Cursor; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.geom.Line2D; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import javax.swing.BorderFactory; import javax.swing.JComponent; import javax.swing.JScrollBar; import javax.swing.ToolTipManager; import javax.swing.event.ChangeListener; import taskblocks.utils.Colors; import taskblocks.utils.Pair; import taskblocks.utils.Utils; public class TaskGraphComponent extends JComponent implements ComponentListener, AdjustmentListener { static int ROW_HEIGHT = 30; private static int DEFAULT_DAY_WIDTH = 10; static int CONN_PADDING_FACTOR = 6; private static int HEADER_HEIGHT = 20; // how far from task left/right boundary will the mouse press will be recognized as pressing the boundary? private static final int TOLERANCE = 5; // constant indicating left boundary of task static Integer LEFT = Integer.valueOf(0); // constant indicating left boundary of task static Integer RIGHT = Integer.valueOf(1); /** * original mode of tasks. It is not updated when doing changes in graph. Explicit * call of {@link TaskGraphRepresentation#updateModel()} must be done. */ TaskModel _model; /** * This is the representation of Graph data (tasks and rows). It has more pre-counted * informations in comparison to original _model. */ TaskGraphRepresentation _builder; /** Paiter used to paint tasks and workers */ TaskGraphPainter _painter; /** Listener on user interaction in this graph. * Currently, just mouse click event is sent to outside world */ GraphActionListener _grActListener; /** Current width of one day column in pixels */ int _dayWidth = DEFAULT_DAY_WIDTH; /** first visible day (on left border) */ long _firstDay; /** left position of graph area */ int _graphLeft; /** top position of graph area */ private int _graphTop; /** width of graph area */ private int _graphWidth; /** height of graph area */ int _graphHeight; /** Width of the left column with workers */ int _headerWidth; /** Handler of mouse events */ GraphMouseHandler _mouseHandler; JScrollBar _verticalScroll; /** * Contains the bounds of the whole content displayed in the component. * Is used to display scrollbars at right position and with the right size */ Rectangle _contentBounds = new Rectangle(); int _scrollTop; /** Cursor - means shadow version of task that is being moved */ Task _cursorTempTask; public TaskGraphComponent(TaskModel model, TaskGraphPainter painter) { _painter = painter; setModel(model); _mouseHandler = new GraphMouseHandler(this); _verticalScroll = new JScrollBar(JScrollBar.VERTICAL); this.add(_verticalScroll); _verticalScroll.addAdjustmentListener(this); setBorder(BorderFactory.createEmptyBorder(0,0,15,0)); this.addMouseMotionListener(_mouseHandler); this.addMouseListener(_mouseHandler); this.addMouseWheelListener(_mouseHandler); this.addKeyListener(_mouseHandler); this.addComponentListener(this); this.setFocusable(true); ToolTipManager.sharedInstance().setDismissDelay(8000); ToolTipManager.sharedInstance().setReshowDelay(3000); } public TaskGraphRepresentation getGraphRepresentation() { return _builder; } public void moveRight() { _firstDay +=2; _builder.setPaintDirty(); repaint(); } public void moveLeft() { _firstDay-=2; _builder.setPaintDirty(); repaint(); } void recountBounds() { Insets insets = getInsets(); _graphTop = HEADER_HEIGHT; _graphTop += insets.top; _graphHeight = getHeight() - HEADER_HEIGHT; _graphLeft = _headerWidth; _graphWidth = getWidth() - _headerWidth; _graphLeft += insets.left; _graphHeight -= insets.top + insets.bottom; _graphWidth -= insets.left + insets.right; } public void paint(Graphics g) { Graphics2D g2 = (Graphics2D)g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); //_headerWidth = 100; g2.setColor(Color.white); g2.fillRect(0,0,getWidth(), getHeight()); synchronized(_builder) { Insets insets = getInsets(); // recount boundaries and positions and paddings if neccessary recountBounds(); // if cursor should be painted... if(_mouseHandler._cursorTaskRow != null && _mouseHandler._cursorTime >= 0 && _mouseHandler._pressedTask != null) { if(_cursorTempTask == null) { _cursorTempTask = new Task(null, null); } Task taskToMove = _mouseHandler._pressedTask; _cursorTempTask._builder = this._builder; _cursorTempTask._userObject = taskToMove._userObject; _cursorTempTask._row = _mouseHandler._cursorTaskRow; _cursorTempTask.setEffort(taskToMove.getEffort()); _cursorTempTask.setStartTime(_mouseHandler._cursorTime); _builder.setPaintDirty(); } else { _cursorTempTask = null; } if(_builder.isPaintDirty()) { TaskLayouter.recountBounds(_graphTop, ROW_HEIGHT, _builder, this, g2); } g2.clipRect(insets.left, insets.top, getWidth()-insets.left-insets.right, getHeight()-insets.top-insets.bottom); // reset content bounds _contentBounds.y = Integer.MAX_VALUE; _contentBounds.height = -1; // paint rows for(TaskRow row: _builder._rows) { row._bounds.x = insets.left; row._bounds.y = row._topPosition-3; row._bounds.width = row._selected ? _headerWidth + _graphWidth : _headerWidth; row._bounds.height = ROW_HEIGHT+6; _painter.paintRowHeader(row._userManObject, g2, row._bounds, row._selected); if(row._index > 0) { g2.setColor(Color.lightGray); int lineY = row._topPosition-row._topPadding*CONN_PADDING_FACTOR; g2.drawLine(insets.left, lineY, 2000, lineY); } // adjust vertical size of the content bounds if(_contentBounds.y > row._bounds.y) { if(_contentBounds.height != -1) { _contentBounds.height += (_contentBounds.y - row._bounds.y); } _contentBounds.y = row._bounds.y; } if(_contentBounds.y + _contentBounds.height < row._bounds.y + row._bounds.height + CONN_PADDING_FACTOR) { _contentBounds.height = row._bounds.y + row._bounds.height - _contentBounds.y + CONN_PADDING_FACTOR; } } // left header vertical line g2.setColor(Color.DARK_GRAY); g2.drawLine(_graphLeft, _graphTop-HEADER_HEIGHT, _graphLeft, _graphTop + _graphHeight+HEADER_HEIGHT); Color lightHeaderCol = Colors.TASKS_TOP_HEADER_COLOR.brighter().brighter(); g2.setColor(lightHeaderCol); g2.drawLine(_graphLeft+1, _graphTop-HEADER_HEIGHT, _graphLeft+1, _graphTop-1); //g2.drawLine(_graphLeft+2, _graphTop-HEADER_HEIGHT, _graphLeft+2, _graphTop + _graphHeight+HEADER_HEIGHT); paintWorkerHeader(g2); // paint tasks g2.clipRect(_graphLeft+2, _graphTop - HEADER_HEIGHT, _graphWidth-3, _graphHeight+HEADER_HEIGHT); Task t; for(int i = _builder._tasks.length-1; i >= 0; i--) { t = _builder._tasks[i]; _painter.paintTask(t._userObject, g2, t._bounds, t._selected || t == _mouseHandler._pressedTask); // adjust the holder of content size if(_contentBounds.y > t._bounds.y) {_contentBounds.y = t._bounds.y;} if(_contentBounds.y + _contentBounds.height < t._bounds.y + t._bounds.height) { _contentBounds.height = t._bounds.y + t._bounds.height - _contentBounds.y; } } // paint weekends and header paintHeaderAndWeekends(g2); // paint connections for(Connection c: _builder._connections) { paintConnection(g2, c); } // paint the insertion cursor paintCursor(g2); // paint just being created conection if(_mouseHandler._dragMode == GraphMouseHandler.DM_NEW_CONNECTION) { g2.setColor(Color.RED); if(_mouseHandler._destTask != null && _mouseHandler._destTask != _mouseHandler._pressedTask) { _painter.paintTask(_mouseHandler._destTask._userObject, g2, _mouseHandler._destTask._bounds, true); } g2.drawLine(_mouseHandler._pressX, _mouseHandler._pressY, _mouseHandler.getLastMouseX(), _mouseHandler.getLastMouseY()); } } adjustScrolls(); // paint children, at least scroll bar(s) paintChildren(g); } public void scaleDown() { long mouseDay = xToTime(_mouseHandler.getLastMouseX()); _dayWidth -=1; if(_dayWidth < 4) { _dayWidth = 4; } long newMouseDay = xToTime(_mouseHandler.getLastMouseX()); if(newMouseDay != mouseDay) { _firstDay -= newMouseDay-mouseDay; } _builder.setPaintDirty(); repaint(); } public void scaleUp() { long mouseDay = xToTime(_mouseHandler.getLastMouseX()); _dayWidth += 1; if(_dayWidth > 50) { _dayWidth = 50; } long newMouseDay = xToTime(_mouseHandler.getLastMouseX()); if(newMouseDay != mouseDay) { _firstDay -= newMouseDay-mouseDay; } _builder.setPaintDirty(); repaint(); } public void setGraphActionListener(GraphActionListener list) { _grActListener = list; } public void setGraphChangeListener(ChangeListener changeListener) { _builder.setGraphChangeListener(changeListener); } public void focusOnToday() { _firstDay = System.currentTimeMillis()/Utils.MILLISECONDS_PER_DAY; _builder.setPaintDirty(); repaint(); } public void scrollToTaskVisible(Object task) { // find the task Task taskToFocus = null; for(Task t: _builder._tasks) { if(t._userObject == task) { taskToFocus = t; break; } } if(taskToFocus == null) { // not found return; } // scroll to the task. // find the most right visible day. long lastVisibleDay = xToTime(getWidth()-getInsets().right); // is the task outside the right border? if(taskToFocus.getFinishTime() > lastVisibleDay) { _firstDay += (taskToFocus.getFinishTime() - lastVisibleDay); _builder.setPaintDirty(); } // is the task outside the left border? if(taskToFocus.getStartTime() < _firstDay) { _firstDay = taskToFocus.getStartTime(); _builder.setPaintDirty(); } // select the focused task _mouseHandler.clearSelection(); _mouseHandler._selection.add(taskToFocus); taskToFocus._selected = true; repaint(); } public void setModel(TaskModel model) { if(model == _model) { // just rebuilds the model _builder.buildFromModel(); return; } TaskGraphRepresentation oldBuilder = _builder; _model = model; _builder = new TaskGraphRepresentation(_model); if(oldBuilder != null) { _builder.setGraphChangeListener(oldBuilder.getGraphChangeListener()); oldBuilder.setGraphChangeListener(null); // not neccessary, just to be sure } _builder.buildFromModel(); // find the minimum task start time and use it as first day. _firstDay = Long.MAX_VALUE; for(Task t: _builder._tasks) { if(_firstDay > t.getStartTime()) { _firstDay = t.getStartTime(); } } if(_firstDay == Long.MAX_VALUE) { _firstDay = System.currentTimeMillis()/Utils.MILLISECONDS_PER_DAY; } repaint(); } TaskRow findRow(int y) { for(TaskRow row: _builder._rows) { if(y >= row._topPosition-row._topPadding*CONN_PADDING_FACTOR && y <= row._topPosition + ROW_HEIGHT + row._bottomPadding*CONN_PADDING_FACTOR) { return row; } } return null; } TaskRow findNearestRow(int y) { TaskRow myRow = null; int minDist = Integer.MAX_VALUE; for(TaskRow row: _builder._rows) { if(y > row._topPosition && y < row._topPosition + ROW_HEIGHT) { myRow = row; break; } else { int dist = Math.min( Math.abs(y-row._topPosition), Math.abs(y-(row._topPosition + ROW_HEIGHT)) ); if(dist < minDist) { myRow = row; minDist = dist; } } } return myRow; } void changeCursor(Object o) { if(o instanceof Task || o instanceof Connection) { this.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } else if(o instanceof Pair) { this.setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR)); } else { this.setCursor(Cursor.getDefaultCursor()); } } Object findObjectOnPo(int x, int y) { if(x < _graphLeft) { return findRow(y); } for(Task t: _builder._tasks) { if(y > t._bounds.y && y < (t._bounds.y + t._bounds.height)) { if(x > (t._bounds.x-TOLERANCE) && x < (t._bounds.x + TOLERANCE)) { return new Pair<Task, Integer>(t, LEFT); } if(x > (t._bounds.x+t._bounds.width-TOLERANCE) && x < (t._bounds.x + t._bounds.width + TOLERANCE)) { return new Pair<Task, Integer>(t, RIGHT); } } if(t._bounds.contains(x, y)) { return t; } } for(Connection c: _builder._connections) { // check the distance from the path double d = Line2D.ptSegDistSq(c._path.xpoints[0], c._path.ypoints[0], c._path.xpoints[1], c._path.ypoints[1], x, y); d = Math.min(d, Line2D.ptSegDistSq(c._path.xpoints[1], c._path.ypoints[1], c._path.xpoints[2], c._path.ypoints[2], x, y)); d = Math.min(d, Line2D.ptSegDistSq(c._path.xpoints[2], c._path.ypoints[2], c._path.xpoints[3], c._path.ypoints[3], x, y)); if(d < 5*5) { return c; } } return null; } private void paintConnection(Graphics2D g2, Connection c) { if(c._selected) { g2.setColor(Colors.SELECTOIN_COLOR); } else { g2.setColor(Colors.CONNECTION_COLOR); } int x2 = c._path.xpoints[3]; int y2 = c._path.ypoints[3]; if(y2 > c._path.ypoints[0]) { g2.drawLine(x2-3, y2-5, x2, y2); g2.drawLine(x2+3, y2-5, x2, y2); } else { g2.drawLine(x2-3, y2+5, x2, y2); g2.drawLine(x2+3, y2+5, x2, y2); } g2.drawLine(c._path.xpoints[0], c._path.ypoints[0], c._path.xpoints[1], c._path.ypoints[1]); g2.drawLine(c._path.xpoints[1], c._path.ypoints[1], c._path.xpoints[2], c._path.ypoints[2]); g2.drawLine(c._path.xpoints[2], c._path.ypoints[2], c._path.xpoints[3], c._path.ypoints[3]); } private void paintCursor(Graphics2D g2) { if(_cursorTempTask != null) { g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f)); _painter.paintTask(_cursorTempTask._userObject, g2, _cursorTempTask._bounds, true); } } private void paintWorkerHeader(Graphics2D g2) { g2.setColor(Colors.TASKS_TOP_HEADER_COLOR); FontMetrics fm = g2.getFontMetrics(); g2.fillRect(_graphLeft-_headerWidth, _graphTop-HEADER_HEIGHT, _headerWidth, HEADER_HEIGHT); g2.setColor(Color.WHITE); g2.drawString("Worker", _graphLeft-_headerWidth + 8, _graphTop-HEADER_HEIGHT + (fm.getHeight() + HEADER_HEIGHT)/2 - fm.getDescent() ); Color darkHeaderCol = Colors.TASKS_TOP_HEADER_COLOR.darker().darker(); Color lightHeaderCol = Colors.TASKS_TOP_HEADER_COLOR.brighter().brighter(); g2.setColor(darkHeaderCol); g2.drawRect(_graphLeft-_headerWidth, _graphTop-HEADER_HEIGHT, _graphWidth+_headerWidth-1, HEADER_HEIGHT+_graphHeight-1); g2.drawLine(_graphLeft-_headerWidth, _graphTop, _graphLeft+_graphWidth, _graphTop); g2.setColor(lightHeaderCol); g2.drawLine(_graphLeft-_headerWidth+1, _graphTop-1, _graphLeft-_headerWidth+1, _graphTop-HEADER_HEIGHT+1); g2.drawLine(_graphLeft-_headerWidth+1, _graphTop-HEADER_HEIGHT+1, _graphLeft-1, _graphTop-HEADER_HEIGHT+1); } private void paintHeaderAndWeekends(Graphics2D g2) { Color lightHeaderCol = Colors.TASKS_TOP_HEADER_COLOR.brighter().brighter(); Color darkHeaderCol = Colors.TASKS_TOP_HEADER_COLOR.darker().darker(); FontMetrics fm = g2.getFontMetrics(); g2.setColor(Colors.TASKS_TOP_HEADER_COLOR); g2.fillRect(_graphLeft+1, _graphTop-HEADER_HEIGHT+1, _graphWidth-2, HEADER_HEIGHT-1); g2.setColor(Color.WHITE); int skip = 1; if(_dayWidth < 10) { skip = 2; } Color weekEndColor = new Color(50,50,50,50); // transparent int x = 0, x1, x2; int mostRight = _graphLeft + _graphWidth; int firstDayInWeek = Utils.getDayInWeek(_firstDay); for(int i = -firstDayInWeek; x < mostRight; i+=7) { long time = _firstDay + i; x = timeToX(time); if(x >= mostRight) { break; } // draw weekend column g2.setColor(weekEndColor); x1 = timeToX(time+5); x2 = timeToX(time+7); g2.fillRect(x1, _graphTop, x2-x1, _graphHeight); // draw the date string if((time/7) % skip == 0) { DateFormat df = new SimpleDateFormat("d.M."); String timeFormatted = df.format(new Date(time*Utils.MILLISECONDS_PER_DAY)); g2.setColor(Color.WHITE); g2.drawString(timeFormatted, x+4, _graphTop-HEADER_HEIGHT + (fm.getHeight() + HEADER_HEIGHT)/2 - fm.getDescent() -1 ); g2.setColor(darkHeaderCol); } } // paint the scale on the top //g2.setColor(new Color(100,10,5)); g2.setColor(darkHeaderCol); x = 0; for(int i = 0; x < mostRight; i++) { long time = _firstDay + i; x = timeToX(time); x2 = timeToX(time + skip*7); int dayInWeek = Utils.getDayInWeek(time); g2.setColor(darkHeaderCol); g2.drawLine(x, _graphTop-3, x, _graphTop-1); if(dayInWeek == 0 && (time/7) % skip == 0) { g2.drawLine(x, _graphTop-HEADER_HEIGHT, x, _graphTop-1); g2.setColor(lightHeaderCol); g2.drawLine(x+1, _graphTop-1, x+1, _graphTop-HEADER_HEIGHT+1); g2.drawLine(x+1, _graphTop - HEADER_HEIGHT+1, x2-1, _graphTop-HEADER_HEIGHT+1); } } g2.setColor(darkHeaderCol); //g2.drawLine(_graphLeft, _graphTop, _graphLeft+_graphWidth, _graphTop); // paint today line long time = System.currentTimeMillis()/ Utils.MILLISECONDS_PER_DAY; g2.setColor(new Color(255,0,0,100)); x = timeToX(time); g2.drawLine(x, _graphTop, x, _graphTop + _graphHeight); } /** Converts time to the component x-coordinate in pixels */ int timeToX(long time) { int relativeTime = (int)(time - _firstDay); return _graphLeft + relativeTime * _dayWidth; } long xToTime(int x) { x+= _dayWidth/2; // we want the nearest day, not the one rounded down. return (x - _graphLeft) / _dayWidth + _firstDay; } public void componentHidden(ComponentEvent e) { } public void componentMoved(ComponentEvent e) { } public void componentResized(ComponentEvent e) { recountBounds(); Insets insets = getInsets(); _verticalScroll.setBounds( getWidth() - _verticalScroll.getWidth() - insets.right, _graphTop+1, _verticalScroll.getPreferredSize().width, getHeight() - _graphTop - insets.bottom-2 ); } public void componentShown(ComponentEvent e) { } boolean _adjustingScrolls; public void adjustmentValueChanged(AdjustmentEvent e) { if(_adjustingScrolls) { return; } if(_contentBounds.height == -1) { return; } if(e.getSource() == _verticalScroll) { // helper variable Rectangle b = new Rectangle(_contentBounds); b.y-=10; b.height+=20; int top = _graphTop; int bottom = getHeight() - getInsets().bottom; // TODO + horizScroll.height int canScrollUp = Math.max(0, Math.max(bottom - b.y - b.height, top - b.y)); int diff = _verticalScroll.getValue() - canScrollUp; _scrollTop -= diff; _builder.setPaintDirty(); repaint(); } } private void adjustScrolls() { // helper variable _adjustingScrolls = true; if(_contentBounds.height == -1) { return; } try { Rectangle b = new Rectangle(_contentBounds); b.y-=10; b.height+=20; int top = _graphTop; int bottom = getHeight() - getInsets().bottom; // TODO + horizScroll.height int canScrollDown = Math.max(0, Math.max(b.y-top, b.y + b.height - bottom)); int canScrollUp = Math.max(0, Math.max(bottom - (b.y + b.height), top - b.y)); //System.out.println(_contentBounds.y + ", " + contentBottom + ", " + top + ", " + bottom + " : " + canScrollUp + ", " + canScrollDown); _verticalScroll.setMaximum(canScrollUp + canScrollDown); _verticalScroll.setBlockIncrement((canScrollUp + canScrollDown) / 5); _verticalScroll.setValue(canScrollUp); //System.out.println(_verticalScroll.getMinimum() + ", " + _verticalScroll.getMaximum() + ", " + _verticalScroll.getValue()); } finally { _adjustingScrolls = false; } } public void deleteSelection() { _mouseHandler.deleteSelection(); } }