001: /**
002: * Copyright 2004 Carlos Silva A.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: *
016: */package com.csa.lgantt.view.tree;
017:
018: import java.awt.Adjustable;
019: import java.awt.Dimension;
020: import java.awt.FocusTraversalPolicy;
021: import java.awt.FontMetrics;
022: import java.awt.Graphics;
023: import java.awt.Image;
024: import java.awt.MediaTracker;
025: import java.awt.Rectangle;
026: import java.awt.Toolkit;
027: import java.awt.event.AdjustmentEvent;
028: import java.awt.event.AdjustmentListener;
029: import java.awt.event.KeyEvent;
030: import java.net.URL;
031: import java.text.SimpleDateFormat;
032: import java.util.Hashtable;
033: import java.util.Iterator;
034: import java.util.ResourceBundle;
035: import java.util.StringTokenizer;
036: import java.util.Vector;
037:
038: import javax.swing.JComponent;
039: import javax.swing.JOptionPane;
040: import javax.swing.JScrollBar;
041:
042: import com.csa.lgantt.Messages;
043: import com.csa.lgantt.model.Project;
044: import com.csa.lgantt.model.ProjectChange;
045: import com.csa.lgantt.model.ProjectListener;
046: import com.csa.lgantt.model.Task;
047: import com.csa.lgantt.model.TaskTreeOptions;
048: import com.csa.lgantt.model.ViewOptions;
049: import com.csa.lgantt.view.adapters.ProjectViewModel;
050: import com.csa.lgantt.view.adapters.ProjectViewModelChange;
051: import com.csa.lgantt.view.adapters.ProjectViewModelListener;
052:
053: /**
054: * TaskTree2 representa el editor de datos a la izquierda
055: * <p>
056: * $Date: 2006/10/10 02:47:58 $
057: * </p>
058: *
059: * @version $Revision: 1.2 $
060: * @author Carlos Silva
061: */
062: public class TaskTree extends JComponent implements ProjectListener,
063: ProjectViewModelListener, ViewOptions.Observer,
064: AdjustmentListener {
065:
066: private static final long serialVersionUID = 1L;
067:
068: // / Scrollbar horizontal
069: private JScrollBar scroll;
070:
071: // / Icono de tarea resumen cerrada
072: private Image iplus = null;
073: // / Icono de tarea resumen abierta
074: private Image iminus = null;
075: // / Ancho de los iconos, se establece despues de cargarlos.
076: private int iconWidth = 0;
077: private int iconHeight = 0;
078: // / Es necesario recalcular el ancho de las columnas?
079: boolean recalcWidths = true;
080: // / Editor usado para escribir las filas
081: private MovilEditor movilEditor;
082: // / LIsta de columnas por omision (archivos .xml)
083: private Vector defaultColumns = new Vector();
084:
085: // / Todas las columnas posibles
086: Hashtable allColumns = new Hashtable();
087:
088: // / Proyecto
089: Project project;
090:
091: // / Modelo
092: ProjectViewModel pvModel;
093:
094: // / Columnas visibles
095: Vector columns = new Vector();
096:
097: // / formato para las fechas en columnas
098: SimpleDateFormat sdf = null;
099:
100: // / Colores y opciones de la tabla
101: TaskTreeOptions treeOptions;
102:
103: // / rectangulo del foco en coordenadas de pantalla(con scroll?)
104: Rectangle focusRect = null;
105:
106: // / Tarea con el foco ahora. Emula la tarea con el foco de viewModel
107: Task focusTask = null;
108:
109: // / Columna con el foco.
110: Column focusColumn = null;
111:
112: // / indica si se esta realizando una actualizacion de los scroll
113: boolean reactToScrolling = true;
114:
115: /**
116: * Permite que el foco de teclado se asigne a este objeto
117: */
118: public boolean isFocusTraversable() {
119: return true;
120: }
121:
122: /**
123: * Constructor for TaskTree
124: */
125: public TaskTree(ProjectViewModel pvm) {
126: super ();
127:
128: ResourceBundle bundle = ResourceBundle
129: .getBundle("com.csa.lgantt.view.tree.Columns");
130: String columnList = bundle.getString("columns");
131: StringTokenizer st = new StringTokenizer(columnList, " ,\t");
132: while (st.hasMoreTokens()) {
133: String id = st.nextToken();
134: String desc = bundle.getString("column." + id);
135: StringTokenizer elems = new StringTokenizer(desc, " ,\t");
136: int length = Integer.parseInt(elems.nextToken());
137: boolean readOnly = Integer.parseInt(elems.nextToken()) == 1;
138: int align = Integer.parseInt(elems.nextToken());
139: String title = bundle.getString("column." + id + ".title");
140: allColumns.put(id, new Column(this , title, length, id,
141: readOnly, align));
142: }
143:
144: st = new StringTokenizer(bundle.getString("columns.default"),
145: " ,\t");
146: while (st.hasMoreTokens()) {
147: defaultColumns.add(st.nextToken());
148: }
149:
150: iplus = loadImage(Messages.getString("tree.img.plus"));
151: iminus = loadImage(Messages.getString("tree.img.minus"));
152:
153: iconWidth = iplus.getWidth(null);
154: iconHeight = iplus.getHeight(null);
155:
156: movilEditor = new MovilEditor(this );
157:
158: setLayout(null);
159: scroll = new JScrollBar(Adjustable.HORIZONTAL);
160: scroll.addAdjustmentListener(this );
161: add(scroll);
162:
163: assignViewModel(pvm);
164: pvModel.setTaskTree(this );
165: pvModel.addListener(this );
166:
167: addMouseListener(new TreeMouseHandler(this ));
168: addKeyListener(new TreeKeyHandler(this ));
169: // TODO: research how to define default focus.
170: // requestDefaultFocus();
171: }
172:
173: /**
174: * Retorna la posicion de partida de la coordenada 0
175: *
176: * @return
177: */
178: public int getLeftPos() {
179: return scroll.getValue();
180: }
181:
182: /**
183: * Asigna el nivel de scroll de la tabla
184: *
185: * @param x
186: */
187: public void setLeftPos(int x) {
188: scroll.setValue(x);
189: }
190:
191: /**
192: * Coloca los componentes en la pantalla: tabla y scroller Luego llama a la
193: * funcion que indica que las propiedades graficas han cambiado
194: *
195: * @see java.awt.Component#doLayout()
196: */
197: public void doLayout() {
198: int sbh = 18;
199: Rectangle bounds = getBounds();
200:
201: Rectangle r = new Rectangle(0, bounds.height - sbh,
202: bounds.width, sbh);
203: scroll.setBounds(r);
204:
205: adjustScrollValues();
206: }
207:
208: /**
209: * Carga una imagen desde el paquete de la clase
210: *
211: * @param imgName
212: * @return Image
213: */
214: Image loadImage(String imgName) {
215: URL imgURL = getClass().getClassLoader().getResource(imgName);
216: Toolkit tk = Toolkit.getDefaultToolkit();
217: Image img = null;
218: try {
219: MediaTracker m = new MediaTracker(this );
220: img = tk.getImage(imgURL);
221: m.addImage(img, 0);
222: m.waitForAll();
223: } catch (Exception e) {
224: e.printStackTrace();
225: }
226: return img;
227: }
228:
229: /**
230: * Pinta el componente
231: */
232: public void paintComponent(Graphics g) {
233: super .paintComponent(g);
234:
235: paintComponent(g, getBounds());
236: }
237:
238: /**
239: * Asigna los anchos de las columnas usando los fonts adecuados
240: *
241: * @param g
242: */
243: void guessColWidths(Graphics g) {
244: if (!recalcWidths)
245: return;
246: g.setFont(treeOptions.cellResumeFont);
247: FontMetrics resumeFM = g.getFontMetrics();
248:
249: g.setFont(treeOptions.cellFont);
250: FontMetrics normalFM = g.getFontMetrics();
251: ViewOptions viewOptions = pvModel.getViewOptions();
252: int xp = 0;
253: for (Iterator j = columns.iterator(); j.hasNext();) {
254: Column c = (Column) j.next();
255: int maxWidth = 0;
256: int taskCount = project.getTaskCount();
257: for (int i = 0; i < taskCount; i++) {
258: Task t = project.getTask(i);
259:
260: String value = c.getValue(t);
261: if (value == null)
262: value = "";
263: int w = 10;
264: if (t.isResume())
265: w += resumeFM.stringWidth(value);
266: else
267: w += normalFM.stringWidth(value);
268: if (c.getProperty().equals("name"))
269: w += t.getChildLevel() * iconWidth;
270: maxWidth = Math.max(w, maxWidth);
271: }
272:
273: c.setX(xp);
274: c.setLength(maxWidth);
275: xp += c.getLength();
276: }
277: recalcWidths = false;
278: adjustScrollValues();
279: }
280:
281: /**
282: * Funcion de apoyo para pintar o generar imagenes
283: */
284: public void paintComponent(Graphics g, Rectangle bounds) {
285: super .paintComponent(g);
286: g.setColor(getBackground());
287: g.fillRect(0, 0, bounds.width, bounds.height);
288:
289: guessColWidths(g);
290: drawHeaders(g, bounds);
291: drawData(g, bounds);
292: }
293:
294: /**
295: * Dibuja los encabezados de las columnas
296: *
297: * @param g
298: * @param bounds
299: */
300: public void drawHeaders(Graphics g, Rectangle bounds) {
301: int x = -scroll.getValue();
302: int y = 0;
303: int h = treeOptions.headerHeight - 1;
304: // drawRect incluye el pixel en width
305: for (Iterator i = columns.iterator(); i.hasNext();) {
306: Column c = (Column) i.next();
307: int l = c.getLength();
308:
309: g.setColor(treeOptions.headerBorder);
310: g.drawRect(x, y, l, h);
311:
312: g.setColor(treeOptions.headerBg);
313: g.fillRect(x + 2, y + 2, l - 2, h - 2);
314:
315: g.setColor(treeOptions.headerFontColor);
316: g.setFont(treeOptions.headerFont);
317: g.drawString(c.getName(), x + 5, y + 18);
318: x += l;
319: }
320: }
321:
322: /**
323: * dibuja la tabla con los valores...
324: *
325: * @param g
326: * @param bounds
327: */
328: public void drawData(Graphics g, Rectangle bounds) {
329: int h = treeOptions.rowHeight;
330: int x = -scroll.getValue();
331: int y = treeOptions.headerHeight - 1;
332: int taskCount = project.getVisibleTaskCount();
333: Rectangle frect = null;
334:
335: ViewOptions viewOptions = pvModel.getViewOptions();
336: int fontOffset = treeOptions.rowHeight - 4;
337: int imgOffset = (treeOptions.rowHeight - iconHeight) / 2;
338: for (int i = viewOptions.getTopRow(); i < taskCount; i++) {
339: Task t = project.getVisibleTask(i);
340: if (!t.isVisible())
341: continue;
342:
343: for (Iterator j = columns.iterator(); j.hasNext();) {
344: Column c = (Column) j.next();
345: int l = c.getLength();
346: if ((focusTask == t) && (focusColumn == c)) {
347: frect = new Rectangle(x + 1, y + 1, l - 1, h - 1);
348: g.setColor(treeOptions.focusBg);
349: g.fillRect(x, y, l, h);
350: g.setColor(treeOptions.focusBorder);
351: g.drawRect(x, y, l, h);
352: } else {
353: g.setColor(treeOptions.cellBorder);
354: g.drawRect(x, y, l, h);
355: }
356: g.setColor(treeOptions.cellFontColor);
357: if (t.isResume())
358: g.setFont(treeOptions.cellResumeFont);
359: else
360: g.setFont(treeOptions.cellFont);
361: if (c.getProperty().equals("name")) {
362: int xp = x + 3 + (t.getChildLevel() - 1)
363: * iconWidth;
364:
365: if (t.isResume()) {
366: Image folderImage = (t.getChildsVisible() ? iminus
367: : iplus);
368: g.drawImage(folderImage, xp, y + imgOffset,
369: null);
370: }
371:
372: g.drawString(c.getValue(t), xp + iconWidth + 1, y
373: + fontOffset);
374: } else {
375: String s = c.getValue(t);
376: if (s == null)
377: s = "";
378: switch (c.getAlign()) {
379: case Column.LEFT:
380: g.drawString(s, x + 5, y + 14);
381: break;
382: case Column.RIGHT: {
383: int w = g.getFontMetrics().stringWidth(s);
384: g.drawString(s, x + l - 5 - w, y + fontOffset);
385: }
386: break;
387: default: {
388: int w = g.getFontMetrics().stringWidth(s);
389: g.drawString(s, x + (l - 3 - w) / 2, y
390: + fontOffset);
391: }
392: }
393:
394: }
395:
396: x += l;
397: }
398: y += h;
399: x = -scroll.getValue();
400: }
401: focusRect = frect;
402: }
403:
404: // ******************************************************************************
405: // Manejo del foco
406: // ******************************************************************************
407: /**
408: * Asigna el foco de este componente en la celda apuntada por x e y. Ademas
409: * cambia el estado de visualizacion de las tareas hijas del arbol
410: *
411: * @param x
412: * @param y
413: */
414: void setFocus(int x, int y) {
415: x += scroll.getValue();
416: int row = (y - treeOptions.headerHeight)
417: / treeOptions.rowHeight;
418: row += project.getViewOptions().getTopRow();
419: if ((row >= 0) && (row < project.getVisibleTaskCount())) {
420: // buscar columna
421: int xp = 0;
422: for (Iterator i = columns.iterator(); i.hasNext();) {
423: Column c = (Column) i.next();
424: xp += c.getLength();
425: if (x < xp) {
426: // found!!
427: Task task = project.getVisibleTask(row);
428:
429: if (c.getProperty().equals("name")) {
430: // marcar columna si se hizo click en la imagen.
431:
432: int left = x - xp + c.getLength();
433: int x1 = 3 + (task.getChildLevel() - 1)
434: * iconWidth;
435: int x2 = x1 + iconWidth;
436:
437: if ((x1 < left) && (left < x2))
438: if (task.isResume())
439: task.setChildsVisible(!task
440: .getChildsVisible());
441: }
442:
443: setFocus(task, c, true);
444: return;
445: }
446: }
447: }
448: setFocus(null, null);
449: }
450:
451: /**
452: * Reasigna el foco en otra tarea, mantendiendo la columna
453: *
454: * @param newTask
455: */
456: public void setFocus(Task newTask) {
457: setFocus(newTask, focusColumn, false);
458: }
459:
460: /**
461: * Asigna el foco de esta tabla editora y se asegura que este visible.
462: *
463: * @param newTask
464: * @param newColumn
465: */
466: public void setFocus(Task newTask, Column newColumn) {
467: setFocus(newTask, newColumn, false);
468: }
469:
470: /**
471: * Asigna el foco
472: *
473: * @param newTask
474: * @param newColumn
475: * @param forceRepaint
476: */
477: public void setFocus(Task newTask, Column newColumn,
478: boolean forceRepaint) {
479: boolean a = focusColumn != newColumn;
480: boolean b = focusTask != newTask;
481: focusColumn = newColumn;
482: focusTask = newTask;
483: finishEdit();
484:
485: if (newColumn == null)
486: return;
487: if (newTask == null)
488: return;
489: Rectangle bounds = getBounds();
490:
491: // asegurar que se vea la fila/columna seleccionada
492: // 0---------------s----e
493: // x<------w----->r
494: //
495: int inicial = newColumn.getX() - scroll.getValue();
496: reactToScrolling = false;
497: if (inicial < 0) // si esta a la izquierda de lo visible
498: scroll.setValue(newColumn.getX());
499: else {
500: int end = newColumn.getX() + newColumn.getLength() + 1;
501: int right = bounds.width + scroll.getValue();
502: if (end > right) {
503: if (newColumn.getLength() > bounds.width)
504: // mas grande que la pantalla
505: scroll.setValue(newColumn.getX());
506: else
507: scroll.setValue(end - bounds.width);
508: }
509: }
510: reactToScrolling = true;
511: if (b) {
512: int rowsInScreen = (int) (bounds.getHeight() - treeOptions.headerHeight)
513: / treeOptions.rowHeight;
514: int topRow = project.getViewOptions().getTopRow();
515: int p = focusTask.getVisibleIndex() - topRow;
516:
517: if (p >= rowsInScreen) {
518: project.getViewOptions().setTopRow(
519: focusTask.getVisibleIndex() + 1 - rowsInScreen);
520: } else if (p < 1) { // el primer visible index==1
521: project.getViewOptions().setTopRow(
522: focusTask.getVisibleIndex() - 1);
523: }
524:
525: pvModel.setCurrentTask(focusTask);
526: }
527: if (a || b || forceRepaint)
528: repaint();
529:
530: requestFocus();
531: }
532:
533: // ******************************************************************************
534: // Edicion volante
535: // ******************************************************************************
536:
537: /**
538: * Inicia la edicion de los datos presentes en la fila y columna actual.
539: * Esto se realiza si y solo si la columna es editable.
540: * <P>
541: * Asigna acciones para aceptar y cancelar la edicion en base a que el
542: * usuario presione ENTER o ESC.
543: * </P>
544: */
545: public void initEdit(KeyEvent ev) {
546: if (focusTask == null)
547: return;
548: if (focusColumn.isReadOnly())
549: return;
550:
551: Rectangle frect = new Rectangle(focusRect);
552: frect.x -= 1;
553: frect.y -= 2;
554: frect.width += 3;
555: frect.height += 5;
556:
557: String text = null;
558: if ((ev != null) && (ev.getKeyChar() != '?'))
559: movilEditor.start(frect, "" + ev.getKeyChar(), false);
560: else
561: movilEditor.start(frect, focusColumn.getValue(focusTask),
562: true);
563: }
564:
565: /**
566: * termina la edicion del dato en el foco. esto se realiza eliminando el
567: * componente d ela visualizacion.
568: */
569: public void finishEdit() {
570: if (movilEditor.getParent() == null)
571: return;
572: movilEditor.stop();
573: grabFocus();
574: recalcWidths = true;
575: repaint();
576: }
577:
578: /**
579: * Transfiere la informacion desde el editor puesto en la pantalla con
580: * {@link #initEdit} hacia el campo adecuado en la tarea. Ademas termina la
581: * ejecucion del componente de edicion.
582: */
583: public void acceptEdit() {
584: try {
585: focusColumn.setValue(focusTask, movilEditor.getText());
586: } catch (Exception e) {
587: JOptionPane
588: .showMessageDialog(this , e,
589: "Error al asignar valor",
590: JOptionPane.ERROR_MESSAGE);
591: }
592: finishEdit();
593: }
594:
595: // ******************************************************************************
596: // Notificaciones
597: // ******************************************************************************
598: /**
599: * Cambios en el proyecto
600: *
601: * @see com.csa.lgantt.model.ProjectListener#projectChanged(com.csa.lgantt.model.ProjectChange)
602: */
603: public void projectChanged(ProjectChange c) {
604: recalcWidths = true;
605: repaint();
606: }
607:
608: /**
609: * Eventos del modelo
610: *
611: * @see com.csa.lgantt.view.adapters.ProjectViewModelListener#projectChanged(com.csa.lgantt.view.adapters.ProjectViewModelChange)
612: */
613: public void viewModelChanged(ProjectViewModelChange c) {
614: switch (c.getId()) {
615: case ProjectViewModelChange.NEW_PROJECT_LOADED:
616: assignViewModel(c.getProjectViewModel());
617: break;
618: case ProjectViewModelChange.CURRENT_TASK_CHANGED:
619: setFocus((Task) c.getSource(), focusColumn);
620: break;
621: }
622: }
623:
624: /**
625: * Asigna el Modelo de Visualizacion
626: */
627: public void assignViewModel(ProjectViewModel pvm) {
628: if (project != null)
629: project.removeListener(this );
630:
631: pvModel = pvm;
632: project = pvm.getProject();
633: project.addListener(this );
634: pvModel.getViewOptions().addObserver(this );
635:
636: recalcWidths = true;
637:
638: treeOptions = pvModel.getProject().getTreeColors();
639: treeOptions.headerHeight = pvModel.getViewOptions().headerHeight;
640: treeOptions.rowHeight = pvModel.getViewOptions().taskHeight;
641:
642: sdf = new SimpleDateFormat(pvModel.getViewOptions().dateFormat);
643: setBackground(treeOptions.cellBg);
644:
645: // Si no hay columnas preseleccionadas usar un default.
646: if (treeOptions.columns == null)
647: treeOptions.columns = (Vector) defaultColumns.clone();
648:
649: columns.clear();
650: int minWidth = 0;
651: for (Iterator iter = treeOptions.columns.iterator(); iter
652: .hasNext();) {
653: String element = (String) iter.next();
654: Column col = (Column) allColumns.get(element);
655: columns.add(col);
656: minWidth += col.getLength();
657: }
658:
659: setPreferredSize(new Dimension(minWidth + 2,
660: treeOptions.rowHeight * 3));
661: focusColumn = (Column) columns.get(0);
662:
663: repaint();
664: }
665:
666: /**
667: * Se ejecuta cuando cambia la posicion del scrollbar, se debe alterar la
668: * columna inicial.
669: *
670: * @see java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event.AdjustmentEvent)
671: */
672: public void adjustmentValueChanged(AdjustmentEvent ev) {
673: if (!reactToScrolling)
674: return;
675: repaint();
676: }
677:
678: /**
679: * Repintar la la tarea cuando haya cambios en las opciones de visualizacion
680: * como el formato de fechas.
681: */
682: public void viewOptionsChanged() {
683: sdf = new SimpleDateFormat(pvModel.getViewOptions().dateFormat);
684: treeOptions.rowHeight = pvModel.getViewOptions().taskHeight;
685: recalcWidths = true;
686: repaint();
687: }
688:
689: void adjustScrollValues() {
690: int cl = 0;
691: for (Iterator i = columns.iterator(); i.hasNext();) {
692: Column c = (Column) i.next();
693: cl += c.getLength();
694: }
695: Rectangle r = getBounds();
696: reactToScrolling = false;
697: scroll.setMinimum(0);
698: scroll.setMaximum(cl + 1);
699: scroll.setVisibleAmount(r.width);
700: reactToScrolling = true;
701: }
702:
703: // ******************************************************************************
704: // Exportacion de la imagen
705: // ******************************************************************************
706:
707: /**
708: * Retorna el tamaņo de esta imagen (solo el arbol)
709: *
710: * @return
711: */
712: public Dimension getImageSize() {
713: int width = 1;
714: for (Iterator i = columns.iterator(); i.hasNext();) {
715: Column c = (Column) i.next();
716: width += c.getLength();
717: }
718: // altura tiene que ver solo con lo visible
719: int visibles = 0;
720: for (int i = 0; i < project.getTaskCount(); i++) {
721: if (project.getTask(i).isVisible()) {
722: visibles++;
723: }
724: }
725: int height = (treeOptions.headerHeight) + treeOptions.rowHeight
726: * visibles;
727: return new Dimension(width, height);
728: }
729:
730: /**
731: * Pinta una carta gantt. incluye el header y el viewer
732: *
733: * @param g
734: * @param pvm
735: * @param rect
736: * @return int ancho de la tabla
737: */
738: public void paintTree(Graphics g) {
739: Dimension dim = getImageSize();
740: Rectangle bounds = new Rectangle(0, 0, dim.width, dim.height);
741: // no mostrar la fila con foco.
742: Column c = focusColumn;
743: Task t = focusTask;
744: focusColumn = null;
745: focusTask = null;
746: paintComponent(g, bounds);
747: focusColumn = c;
748: focusTask = t;
749: }
750: }
|