diff --git a/application.properties b/application.properties index 1a25e65..d300eba 100755 --- a/application.properties +++ b/application.properties @@ -1,4 +1,4 @@ #Thu Dec 17 08:19:45 CST 2009 -app.version=0.1 +app.version=2.0 app.griffon.version=0.2 app.name=TimeStamper diff --git a/griffon-app/actions/com/jdbernard/timestamper/TimeStamperMainActions.groovy b/griffon-app/actions/com/jdbernard/timestamper/TimeStamperMainActions.groovy index bc35272..060c9e6 100644 --- a/griffon-app/actions/com/jdbernard/timestamper/TimeStamperMainActions.groovy +++ b/griffon-app/actions/com/jdbernard/timestamper/TimeStamperMainActions.groovy @@ -1,21 +1,8 @@ package com.jdbernard.timestamper +import java.awt.event.KeyEvent + gracefulExitAction = action ( name: 'Graceful Exit', closure: controller.&exitGracefully ) - -toolsMenuAction = action ( - name: 'Show Tools Menu', - closure: controller.&showToolsMenu -) - -showNotesAction = action ( - name: 'Show Notes', - closure: controller.&showNotes -) - -showPunchcardAction = action ( - name: 'Show Punchcard', - closure: controller.&showPunchcard -) diff --git a/griffon-app/conf/Application.groovy b/griffon-app/conf/Application.groovy index dd667ae..192f52e 100755 --- a/griffon-app/conf/Application.groovy +++ b/griffon-app/conf/Application.groovy @@ -9,20 +9,27 @@ application { //frameClass = 'javax.swing.JFrame' } mvcGroups { + // MVC Group for "com.jdbernard.timestamper.PunchcardDialog" + 'PunchcardDialog' { + model = 'com.jdbernard.timestamper.PunchcardDialogModel' + view = 'com.jdbernard.timestamper.PunchcardDialogView' + controller = 'com.jdbernard.timestamper.PunchcardDialogController' + } + // MVC Group for "com.jdbernard.timestamper.NotesDialog" 'NotesDialog' { actions = 'com.jdbernard.timestamper.NotesDialogActions' model = 'com.jdbernard.timestamper.NotesDialogModel' - controller = 'com.jdbernard.timestamper.NotesDialogController' view = 'com.jdbernard.timestamper.NotesDialogView' + controller = 'com.jdbernard.timestamper.NotesDialogController' } // MVC Group for "com.jdbernard.timestamper.TimeStamperMain" 'TimeStamperMain' { actions = 'com.jdbernard.timestamper.TimeStamperMainActions' model = 'com.jdbernard.timestamper.TimeStamperMainModel' - controller = 'com.jdbernard.timestamper.TimeStamperMainController' view = 'com.jdbernard.timestamper.TimeStamperMainView' + controller = 'com.jdbernard.timestamper.TimeStamperMainController' } } diff --git a/griffon-app/controllers/com/jdbernard/timestamper/NotesDialogController.groovy b/griffon-app/controllers/com/jdbernard/timestamper/NotesDialogController.groovy index 08498c4..a566fc7 100644 --- a/griffon-app/controllers/com/jdbernard/timestamper/NotesDialogController.groovy +++ b/griffon-app/controllers/com/jdbernard/timestamper/NotesDialogController.groovy @@ -1,26 +1,11 @@ package com.jdbernard.timestamper -import java.awt.Point -import java.awt.Rectangle -import java.awt.Toolkit - class NotesDialogController { // these will be injected by Griffon def model def view - void mvcGroupInit(Map args) { } - Point mousePressRelativeToDialog - - def mousePressed = { evt -> mousePressRelativeToDialog = evt?.point } - - def mouseDragged = { evt -> - GUIUtil.componentDragged(view.notesDialog, evt, - mousePressRelativeToDialog, - new Rectangle(Toolkit.defaultToolkit.screenSize), - app.views.TimeStamperMain.frame.bounds) - } } diff --git a/griffon-app/controllers/com/jdbernard/timestamper/PunchcardDialogController.groovy b/griffon-app/controllers/com/jdbernard/timestamper/PunchcardDialogController.groovy new file mode 100644 index 0000000..1249ca0 --- /dev/null +++ b/griffon-app/controllers/com/jdbernard/timestamper/PunchcardDialogController.groovy @@ -0,0 +1,16 @@ +package com.jdbernard.timestamper + +class PunchcardDialogController { + // these will be injected by Griffon + def model + def view + + void mvcGroupInit(Map args) { + // this method is called after model and view are injected + } + + /* + def action = { evt = null -> + } + */ +} \ No newline at end of file diff --git a/griffon-app/controllers/com/jdbernard/timestamper/TimeStamperMainController.groovy b/griffon-app/controllers/com/jdbernard/timestamper/TimeStamperMainController.groovy index 0d18a19..0a2097b 100644 --- a/griffon-app/controllers/com/jdbernard/timestamper/TimeStamperMainController.groovy +++ b/griffon-app/controllers/com/jdbernard/timestamper/TimeStamperMainController.groovy @@ -1,49 +1,64 @@ package com.jdbernard.timestamper -import java.awt.Point -import java.awt.Rectangle -import java.awt.Toolkit -import java.util.Timer +import com.jdbernard.timestamper.core.TimelineMarker +import com.jdbernard.timestamper.core.TimelineProperties class TimeStamperMainController { // these will be injected by Griffon def model def view - Timer updateTimer - - Point mousePressRelativeToFrame - void mvcGroupInit(Map args) { def notes = buildMVCGroup('NotesDialog') + def punchcard = buildMVCGroup('PunchcardDialog') view.notesDialog = notes.view.notesDialog + view.punchcardDialog = punchcard.view.punchcardDialog - updateTimer + // load application properties + Properties prop = new Properties() + String userHomeDir = System.getProperty('user.home') + model.configFile = new File(userHomeDir, ".timestamperrc") + if (!model.configFile.exists()) model.configFile.createNewFile() + + try { model.configFile.withInputStream { prop.load(it) } } + catch (IOException ioe) { /* TODO */ } + + model.config = prop + + // load the last used timeline file + String lastUsed = model.config.getProperty('lastUsed', null) + if (lastUsed == null) { + lastUsed = 'timeline.default.properties' + model.config.setProperty('lastUsed', lastUsed) + } + File propertyFile = new File(lastUsed) + if (!propertyFile.exists()) propertyFile.createNewFile() + + model.timelineProperties = new TimelineProperties(propertyFile) + + // load the main timeline + model.timeline = model.timelineProperties.timeline + + // load the last marker + model.currentMarker = model.timeline.getLastMarker(new Date()) + } def exitGracefully = { evt = null -> + // save config + try { model.configFile.withOutputStream { out -> + model.config.store(out, null) } } + catch (IOException ioe) {} + + // save timeline and properties + model.timelineProperties.save() app.shutdown() } - def showToolsMenu = { evt = null -> - - } - - def showNotes = { evt = null -> - view.notesDialog.visible = view.notesVisibleButton.selected - } - - def showPunchcard = { evt = null -> - - } - - def mousePressed = { evt = null -> - mousePressRelativeToFrame = evt?.point - } - - def mouseDragged = { evt = null -> - GUIUtil.componentDragged(view.frame, evt, mousePressRelativeToFrame, - new Rectangle(Toolkit.defaultToolkit.screenSize)) + def newTask = { mark -> + model.currentMarker = new TimelineMarker(new Date(), mark, + "No comments.") + model.timeline.addMarker(model.currentMarker) } } diff --git a/griffon-app/models/com/jdbernard/timestamper/PunchcardDialogModel.groovy b/griffon-app/models/com/jdbernard/timestamper/PunchcardDialogModel.groovy new file mode 100644 index 0000000..2a89dba --- /dev/null +++ b/griffon-app/models/com/jdbernard/timestamper/PunchcardDialogModel.groovy @@ -0,0 +1,7 @@ +package com.jdbernard.timestamper + +import groovy.beans.Bindable + +class PunchcardDialogModel { + // @Bindable String propName +} \ No newline at end of file diff --git a/griffon-app/models/com/jdbernard/timestamper/TimeStamperMainModel.groovy b/griffon-app/models/com/jdbernard/timestamper/TimeStamperMainModel.groovy index 11e515e..6a855cc 100644 --- a/griffon-app/models/com/jdbernard/timestamper/TimeStamperMainModel.groovy +++ b/griffon-app/models/com/jdbernard/timestamper/TimeStamperMainModel.groovy @@ -1,6 +1,8 @@ package com.jdbernard.timestamper import groovy.beans.Bindable +import java.awt.Point +import java.util.Properties import com.jdbernard.timestamper.core.Timeline import com.jdbernard.timestamper.core.TimelineMarker import com.jdbernard.timestamper.core.TimelineProperties @@ -8,5 +10,9 @@ import com.jdbernard.timestamper.core.TimelineProperties class TimeStamperMainModel { @Bindable TimelineMarker currentMarker @Bindable Timeline timeline - @Bindable TimelineProperties properties + @Bindable TimelineProperties timelineProperties + @Bindable Properties config + File configFile + + @Bindable Point absoluteLocation } diff --git a/griffon-app/views/com/jdbernard/timestamper/NotesDialogView.groovy b/griffon-app/views/com/jdbernard/timestamper/NotesDialogView.groovy index b302f60..61595ba 100644 --- a/griffon-app/views/com/jdbernard/timestamper/NotesDialogView.groovy +++ b/griffon-app/views/com/jdbernard/timestamper/NotesDialogView.groovy @@ -1,27 +1,55 @@ package com.jdbernard.timestamper import java.awt.Color +import java.awt.Point +import java.awt.Rectangle +import java.awt.Toolkit import javax.swing.BoxLayout import net.miginfocom.swing.MigLayout +Point mousePressRelativeToDialog +Point offsetFromMainFrame + +mousePressed = { evt -> mousePressRelativeToDialog = evt?.point } + +mouseDragged = { evt -> + GUIUtil.componentDragged(view.notesDialog, evt, + mousePressRelativeToDialog, + new Rectangle(Toolkit.defaultToolkit.screenSize), + app.views.TimeStamperMain.frame.bounds) + + Point p = app.views.TimeStamperMain.frame.location + offsetFromMainFrame = new Point(notesDialog.location) + offsetFromMainFrame.translate((int) -p.x, (int) -p.y) +} + notesDialog = dialog( title: 'Notes', modal: false, undecorated: true, minimumSize: [325, 200], iconImage: imageIcon('/16-em-pencil.png').image, - iconImages: [imageIcon('/16-em-pencil.png').image] + iconImages: [imageIcon('/16-em-pencil.png').image], + location: bind(source: app.models.TimeStamperMain, + sourceProperty: 'absoluteLocation', + converter: { loc -> + Point p = new Point(offsetFromMainFrame) + p.translate((int) loc.x, (int) loc.y) + return p}) ) { panel( border:lineBorder(color: Color.BLACK, thickness:1, parent:true), - mousePressed: controller.&mousePressed, - mouseDragged: controller.&mouseDragged, + mousePressed: mousePressed, + mouseDragged: mouseDragged, layout: new MigLayout('insets 5 5 5 5, fill') ) { - scrollPane(constraints: 'growx, growy') { + scrollPane(constraints: 'growx, growy, spany 2') { notesTextArea = textArea(lineWrap: true, columns: 20, rows: 5, - wrapStyleWord: true) + wrapStyleWord: true, + text: bind(source: app.models.TimeStamperMain, + sourceProperty: 'currentMarker', + sourceValue: { app.models.TimeStamperMain.currentMarker?.notes})) } } diff --git a/griffon-app/views/com/jdbernard/timestamper/PunchcardDialogView.groovy b/griffon-app/views/com/jdbernard/timestamper/PunchcardDialogView.groovy new file mode 100644 index 0000000..76cf7b6 --- /dev/null +++ b/griffon-app/views/com/jdbernard/timestamper/PunchcardDialogView.groovy @@ -0,0 +1,12 @@ +package com.jdbernard.timestamper + +punchcardDialog = dialog( +/* title: 'Punchcard', + modal: false, + undecorated: true, + iconImage: iconImage('/16-file-archive.png').image, + iconImages: [iconImage('/16-file-archive.png').image], + minimumSize: [325, 600]*/ +) { + +} diff --git a/griffon-app/views/com/jdbernard/timestamper/TimeStamperMainView.groovy b/griffon-app/views/com/jdbernard/timestamper/TimeStamperMainView.groovy index 9f869ad..8ae817f 100644 --- a/griffon-app/views/com/jdbernard/timestamper/TimeStamperMainView.groovy +++ b/griffon-app/views/com/jdbernard/timestamper/TimeStamperMainView.groovy @@ -1,10 +1,88 @@ package com.jdbernard.timestamper +import groovy.beans.Bindable import java.awt.Color import java.awt.Font +import java.awt.Point +import java.awt.Rectangle +import java.awt.Toolkit +import java.awt.event.KeyEvent import javax.swing.BoxLayout +import javax.swing.Timer +import com.jdbernard.timestamper.core.Timeline import net.miginfocom.swing.MigLayout +Point mousePressRelativeToFrame + +/* ========== * + * GUI Events * + * ========== */ + +taskTextFieldChanged = { evt = null -> + if (evt.keyCode == KeyEvent.VK_ENTER) { + taskTextField.font = taskBoldFont + controller.newTask(taskTextField.text) + } + + else if (evt.keyCode == KeyEvent.VK_ESCAPE) { + taskTextField.font = taskBoldFont + taskTextField.text = model.currentMarker.mark + } + + else if (!evt.isActionKey()) + taskTextField.font = taskThinFont +} + +showToolsMenu = { evt = null -> + +} + +showNotes = { evt = null -> + notesDialog.visible = notesVisibleButton.selected +} + +showPunchcard = { evt = null -> + +} + +mousePressed = { evt = null -> + mousePressRelativeToFrame = evt?.point +} + +mouseDragged = { evt = null -> + GUIUtil.componentDragged(frame, evt, mousePressRelativeToFrame, + new Rectangle(Toolkit.defaultToolkit.screenSize)) +} + +/* ============== * + * GUI Definition * + * ============== */ + +updateTimer = new Timer(1000, action(name: 'GUI Refresh', closure: { + Date currentTime = new Date() + currentTimeLabel.text = Timeline.shortFormat.format(currentTime) + if (model.currentMarker != null) { + long seconds = currentTime.time - model.currentMarker.timestamp.time + seconds /= 1000 + long minutes = seconds / 60 + seconds = seconds % 60 + long hours = minutes / 60 + minutes %= 60 + long days = hours / 24 + hours %= 24 + + StringBuilder sb = new StringBuilder() + if (days > 0) sb.append(days + "day ") + if (hours > 0) sb.append(hours + "hr ") + if (minutes > 0) sb.append(minutes + "min ") + sb.append(seconds + "sec") + + totalTimeLabel.text = sb.toString() + } else totalTimeLabel.text = "" +})) + +updateTimer.start() + frame = application(title:'TimeStamper', //size:[320,480], pack:true, @@ -13,23 +91,29 @@ frame = application(title:'TimeStamper', locationByPlatform:true, iconImage: imageIcon('/appointment-new-32x32.png').image, iconImages: [imageIcon('/appointment-new-32x32.png').image, - imageIcon('/appointment-new-16x16.png').image] + imageIcon('/appointment-new-16x16.png').image], + componentMoved: { evt -> model.absoluteLocation = frame.location } ) { panel( border:lineBorder(color:Color.BLACK, thickness:1, parent:true), layout: new MigLayout('insets 0 5 0 0, fill','', '[]0[]0[]'), - mousePressed: controller.&mousePressed, - mouseDragged: controller.&mouseDragged + mousePressed: mousePressed, + mouseDragged: mouseDragged ) { def mainFont = new Font(Font.SANS_SERIF, Font.BOLD, 12) def timeFont = new Font(Font.SANS_SERIF, Font.BOLD, 14) label("Current task started at ", font: mainFont) - label("00:00:00", constraints: 'align leading', - font: timeFont, foreground: [0, 102, 102]) + label(constraints: 'align leading', font: timeFont, + foreground: [0, 102, 102], + text: bind(source: model, sourceProperty: 'currentMarker', + sourceValue: { + model.currentMarker == null ? "00:00:00" : + Timeline.shortFormat.format(model.currentMarker.timestamp) + })) panel(constraints: 'alignx trailing, aligny top, wrap') { boxLayout(axis: BoxLayout.X_AXIS) - button(toolsMenuAction, + button(actionPerformed: showToolsMenu, icon: imageIcon('/16-tool-a.png'), rolloverIcon: imageIcon('/16-tool-a-hover.png'), border: emptyBorder(0), @@ -43,23 +127,35 @@ frame = application(title:'TimeStamper', hideActionText: true) } - textField("Task name", constraints: "growx, span 2, w 250::") + taskTextField = textField("Task name", + constraints: "growx, span 2, w 250::", + keyReleased: taskTextFieldChanged, + text: bind(source: model, sourceProperty: 'currentMarker', + sourceValue: { model.currentMarker.mark })) + + taskThinFont = taskTextField.font + taskBoldFont = taskTextField.font.deriveFont(Font.BOLD) panel(constraints: 'alignx leading, aligny top, gapright 5px, wrap') { boxLayout(axis: BoxLayout.X_AXIS) - notesVisibleButton = toggleButton(showNotesAction, icon: imageIcon('/16-em-pencil.png'), + notesVisibleButton = toggleButton( + actionPerformed: showNotes, + icon: imageIcon('/16-em-pencil.png'), hideActionText: true, border: emptyBorder(4)) - punchcardVisibleButton = toggleButton(showPunchcardAction, + punchcardVisibleButton = toggleButton( + actionPerformed: showPunchcard, icon: imageIcon('/16-file-archive.png'), hideActionText: true, border: emptyBorder(4)) } - label("2hr 18min 56sec", constraints: 'alignx leading', font: timeFont, - foreground: [0, 153, 0]) + totalTimeLabel = label("", constraints: 'alignx leading', + font: timeFont, foreground: [0, 153, 0]) - label("00:00:00", constraints: 'align trailing', font: timeFont, - foreground: [204, 0, 0]) + currentTimeLabel = label("00:00:00", constraints: 'align trailing', + font: timeFont, foreground: [204, 0, 0]) } } + + diff --git a/src/main/com/jdbernard/timestamper/core/Timeline.java b/src/main/com/jdbernard/timestamper/core/Timeline.java index 9c559d0..17c828f 100755 --- a/src/main/com/jdbernard/timestamper/core/Timeline.java +++ b/src/main/com/jdbernard/timestamper/core/Timeline.java @@ -23,6 +23,8 @@ public class Timeline implements Iterable { timelineList = new TreeSet(); } + public void addMarker(TimelineMarker tm) { timelineList.add(tm); } + public void addMarker(Date timestamp, String name, String notes) { timelineList.add(new TimelineMarker(timestamp, name, notes)); } diff --git a/src/main/com/jdbernard/timestamper/core/TimelineProperties.java b/src/main/com/jdbernard/timestamper/core/TimelineProperties.java index ea07241..bcbd57d 100755 --- a/src/main/com/jdbernard/timestamper/core/TimelineProperties.java +++ b/src/main/com/jdbernard/timestamper/core/TimelineProperties.java @@ -68,7 +68,14 @@ public class TimelineProperties { // load local timeline strURI = config.getProperty(LOCAL_TIMELINE_URI, ""); if ("".equals(strURI)) { - timelineURI = new File("timeline.default.txt").toURI(); + File defaultTimelineFile = new File("timeline.default.txt"); + try { + if (!defaultTimelineFile.exists()) + defaultTimelineFile.createNewFile(); + } catch (IOException ioe) { + // TODO + } + timelineURI = defaultTimelineFile.toURI(); } else { try { timelineURI = new URI(strURI); } catch (URISyntaxException urise) { diff --git a/src/main/com/jdbernard/timestamper/gui/TimelineDayDisplay.java b/src/main/com/jdbernard/timestamper/gui/TimelineDayDisplay.java new file mode 100644 index 0000000..63d4a87 --- /dev/null +++ b/src/main/com/jdbernard/timestamper/gui/TimelineDayDisplay.java @@ -0,0 +1,660 @@ +/* TimelineDayDisplay.java + * Author: Jonathan Bernard - jonathan.bernard@gemalto.com + */ + +package jdbernard.timestamper.gui; + +import com.jdbernard.timestamper.core.TimelineMarker; +import com.jdbernard.timestamper.core.Timeline; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.Iterator; +import javax.swing.JComponent; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * + * @author jbernard + */ +public class TimelineDayDisplay extends JComponent implements MouseListener, + ChangeListener { + + private class MarkerDisplayEntry { + public TimelineMarker marker; + public float relY; + public float relHeight; + public Rectangle2D markBounds; + public Rectangle2D notesBounds; + public Rectangle bounds; + } + + private class TimeLegendEntry { + public double relY; + public String label; + } + + private enum TimeDelta { + Hourly (Calendar.HOUR_OF_DAY, 1) { + public String formatCalendar(Calendar c) { + return String.format("%1$02d:00", + c.get(Calendar.HOUR_OF_DAY)); + } + + public boolean fitsInHeight(double height, double millisec) { + return ((height * 1000l * 60l * 30l) / millisec < 25); + } + }, + ThirtyMin (Calendar.MINUTE, 30) { + public String formatCalendar(Calendar c) { + return String.format("%1$02d:%2$02d", + c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE)); + } + + public boolean fitsInHeight(double height, double millisec) { + return ((height * 1000l * 60l * 15l) / millisec < 25); + } + }, + FifteenMin (Calendar.MINUTE, 15) { + public String formatCalendar(Calendar c) { + return String.format("%1$02d:%2$02d", + c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE)); + } + + public boolean fitsInHeight(double height, double millisec) { + return ((height * 1000l * 60l * 10l) / millisec < 25); + } + }, + TenMin (Calendar.MINUTE, 10) { + public String formatCalendar(Calendar c) { + return String.format("%1$02d:%2$02d", + c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE)); + } + + public boolean fitsInHeight(double height, double millisec) { + return ((height * 1000l * 60l * 5l) / millisec < 25); + } + }, + FiveMin (Calendar.MINUTE, 5) { + public String formatCalendar(Calendar c) { + return String.format("%1$02d:%2$02d", + c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE)); + } + + public boolean fitsInHeight(double height, double millisec) { + return ((height * 1000l * 60l) / millisec < 25); + } + }, + Minute (Calendar.MINUTE, 1) { + public String formatCalendar(Calendar c) { + return String.format("%1$02d:%2$02d", + c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE)); + } + + public boolean fitsInHeight(double height, double millisec) { + return ((height * 1000l * 30l) / millisec < 25); + } + }, + ThirtySec (Calendar.SECOND, 30) { + public String formatCalendar(Calendar c) { + return String.format("%1$02d:%2$02d", + c.get(Calendar.MINUTE), c.get(Calendar.SECOND)); + } + + public boolean fitsInHeight(double height, double millisec) { + return ((height * 1000l * 15l) / millisec < 25); + } + }, + FifteenSec (Calendar.SECOND, 15) { + public String formatCalendar(Calendar c) { + return String.format("%1$02d:%2$02d", + c.get(Calendar.MINUTE), c.get(Calendar.SECOND)); + } + + public boolean fitsInHeight(double height, double millisec) { + return ((height * 1000l * 10l) / millisec < 25); + } + }, + TenSec (Calendar.SECOND, 10) { + public String formatCalendar(Calendar c) { + return String.format("%1$02d:%2$02d", + c.get(Calendar.MINUTE), c.get(Calendar.SECOND)); + } + + public boolean fitsInHeight(double height, double millisec) { + return ((height * 1000l * 5l) / millisec < 25); + } + }, + FiveSec (Calendar.SECOND, 5) { + public String formatCalendar(Calendar c) { + return String.format("%1$02d:%2$02d", + c.get(Calendar.MINUTE), c.get(Calendar.SECOND)); + } + + public boolean fitsInHeight(double height, double millisec) { + return ((height * 1000l) / millisec < 25); + } + }, + Second (Calendar.SECOND, 1) { + public String formatCalendar(Calendar c) { + return String.format("%1$02d:%2$02d", + c.get(Calendar.MINUTE), c.get(Calendar.SECOND)); + } + + public boolean fitsInHeight(double height, double millisec) { + return ((height * 1500l) / millisec < 25); + } + }, + SubSecond (Calendar.MILLISECOND, 100) { + public String formatCalendar(Calendar c) { + return String.format("%1$02d:%2$03d", + c.get(Calendar.SECOND), c.get(Calendar.MILLISECOND)); + } + + public boolean fitsInHeight(double height, double millisec) { + return true; + } + }; + + private int INTERVAL; + private int AMOUNT; + + private TimeDelta(int interval, int amount) { + INTERVAL = interval; + AMOUNT = amount; + } + + public Calendar addToCalendar(Calendar c) { + c.add(INTERVAL, AMOUNT); + return c; + } + + public abstract boolean fitsInHeight(double height, double millisec); + + public abstract String formatCalendar(Calendar c); { } + } + + private ArrayList markerEntries; + private ArrayList timeLegendLocations; + private TimelineMarker currentMarker; + private ArrayList changeListeners = new ArrayList(); + + private Point lastMousePress; + + private Font markFont;// = getFont().deriveFont(Font.BOLD); + private Font notesFont;// = getFont(); + + private Color evenTrans = new Color(0.75f, 0.75f, 0.75f, 0.4f); + private Color evenOpaque = new Color(0.75f, 0.75f, 0.75f, 1f); + private Color oddTrans = new Color(0.5f, 0.5f, 0.5f, 0.4f); + private Color oddOpaque = new Color(0.5f, 0.5f, 0.5f, 1f); + private Color selectedTrans = new Color(0.5f, 0.75f, 0.5f, 0.4f); + private Color selectedOpaque = new Color(0.5f, 0.75f, 0.5f, 1f); + private Color fontColor = new Color(0.1f, 0.1f, 0.1f, 1f); + + private Date rangeStartDate = new Date(); + private Date rangeEndDate = new Date(); + + public TimelineDayDisplay() { + super(); + setDay(new Date(), false); + addMouseListener(this); + } + + public TimelineDayDisplay(Calendar day) { + setDay(day.getTime(), false); + addMouseListener(this); + updateMarkers(getGraphics()); + } + + /** + * Set the range for the visible timeline segment. + * @param start The beginning of the desired timeline segment. + * @param end The end of the desired timeline segment. + */ + public void setDisplayInterval(Date start, Date end) { + rangeStartDate = start; + rangeEndDate = end; + } + + /** + * Set the component to show the timeline segment for a specific day. The + * visible area will show the full 24-hour day. + * @param d The date of the day to display. The exact time of the variable + * can be any time in the desired day. + */ + public void setDay(Date d) { + setDay(d, true); + } + + /** + * There is the special case of instance initialization, where it is + * desirable to call setDay to handle the range start and end calculations + * but where we do not want to immediately update the gui, because it may + * not be fully initialized yet. + * @param d Day to set as the current day (component will show the range + * representing the day from start to finish. + * @param update If
true
, + * updateMarkers(getGraphics) is called after the range + * calculations are made. + */ + private void setDay(Date d, boolean update) { + Calendar day = Calendar.getInstance(); + day.setTime(d); + day.set(Calendar.HOUR_OF_DAY, 0); + day.set(Calendar.MINUTE, 0); + day.set(Calendar.SECOND, 0); + rangeStartDate = day.getTime(); + + day.add(Calendar.DAY_OF_YEAR, 1); + rangeEndDate = day.getTime(); + + if (update) updateMarkers(getGraphics()); + } + + public void setMarkFont(Font f) { + markFont = f; + } + + public Font getMarkFont() { + return markFont; + } + + public void setNotesFont(Font f) { + notesFont = f; + } + + public Font getNotesFont() { + return notesFont; + } + + public void setFontColor(Color f) { + fontColor = new Color(f.getRGB()); + } + + public Color getFontColor() { + return fontColor; + } + public void setEvenColor(Color c) { + evenOpaque = new Color(c.getRGB()); + evenTrans = new Color((float) c.getRed() / 255f, + (float) c.getGreen() / 255f, + (float) c.getBlue() / 255f, 0.4f); + } + + public Color getEvenColor() { + return evenOpaque; + } + + public void setOddColor(Color c) { + oddOpaque = new Color(c.getRGB()); + oddTrans = new Color((float) c.getRed() / 255f, + (float) c.getGreen() / 255f, + (float) c.getBlue() / 255f, 0.4f); + } + + public Color getOddColor() { + return oddOpaque; + } + + public void setSelectedColor(Color c) { + selectedOpaque = new Color(c.getRGB()); + selectedTrans = new Color((float) c.getRed() / 255f, + (float) c.getGreen() / 255f, + (float) c.getBlue() / 255f, 0.4f); + } + + public Color getSelectedColor() { + return selectedOpaque; + } + + public TimelineMarker getSelectedTimelineMarker() { + return currentMarker; + } + + public void addMarker(Date timestamp, String mark, String notes) { + /*Timeline timeline = TimeStamperApp.getApplication() + .getTimelineProperties().getTimeline(); + timeline.addMarker(timestamp, mark, notes); + updateMarkers(getGraphics());*/ + } + + public void deleteSelectedMarker() { + /*Timeline timeline = TimeStamperApp.getApplication() + .getTimelineProperties().getTimeline(); + timeline.removeMarker(currentMarker); + updateMarkers(getGraphics());*/ + } + + public void updateSelectedMarker(String notes) { + currentMarker.setNotes(notes); + updateMarkers(getGraphics()); + } + + /** + * updateMarkers sets the internal list of TimelineMarkers, based on the + * currently visible timeline. The drawing of the display is split between + * this method, which constructs the data representation of what needs to + * be drawn, and the paintComponents method, which does the drawing. This is + * done to save computation, only recalculating markers when needed. + */ + private void updateMarkers(Graphics g) { + + /*Timeline timeline = TimeStamperApp.getApplication() + .getTimelineProperties().getTimeline(); + Insets insets = this.getInsets(); + Rectangle bounds = this.getBounds(); + Rectangle canvasBounds = new Rectangle(insets.left, insets.top, + bounds.width - insets.left - insets.right - 1, + bounds.height - insets.top - insets.bottom - 1); + + Rectangle2D stringBounds = getFontMetrics(getFont()).getStringBounds("00:00 ", g); + + long rangeDiff = rangeEndDate.getTime() - rangeStartDate.getTime(); + + markerEntries = new ArrayList(); + timeLegendLocations = new ArrayList(); + + if (markFont == null) markFont = getFont().deriveFont(Font.BOLD); + if (notesFont == null) notesFont = getFont(); + + // calculate positions of all visible hour lines + // choose the increment of time to view + TimeDelta timeDelta = TimeDelta.Hourly; + if (rangeDiff == 0) rangeDiff = 1; + + for (TimeDelta d : TimeDelta.values()) { + if (d.fitsInHeight(canvasBounds.getHeight(), rangeDiff)) { + timeDelta = d; + break; + } + } + + Calendar timeCounter = Calendar.getInstance(); + timeCounter.setTime(rangeStartDate); + timeCounter.set(Calendar.MINUTE, 0); + timeCounter.set(Calendar.SECOND, 0); + + while (rangeStartDate.after(timeCounter.getTime())) + timeDelta.addToCalendar(timeCounter); + + while (rangeEndDate.after(timeCounter.getTime())) { + TimeLegendEntry entry = new TimeLegendEntry(); + entry.relY = ((double) (timeCounter.getTimeInMillis() + - rangeStartDate.getTime()) / (double) rangeDiff); + entry.label = timeDelta.formatCalendar(timeCounter); + timeLegendLocations.add(entry); + timeDelta.addToCalendar(timeCounter); + } + + // get all relevant markers starting from the marker just before the + // visible start of the display + TimelineMarker tm = timeline.getLastMarker(rangeStartDate); + + // If there is no previous marker + if (tm == null) + // try to get the first marker + try { tm = timeline.iterator().next(); } + // and if there aren't any markers at all, just return, the array is + // empty so the display will be empty + catch (Exception e) { return; } + + // Now we want to step through the timeline, capturing all markers + // between the visible ranges. + Iterator itr = timeline.iterator(); + + while (!itr.next().equals(tm)); + + ArrayList markers = new ArrayList(); + while (rangeEndDate.after(tm.getTimestamp())) { + markers.add(tm); + if (itr.hasNext()) tm = itr.next(); + else break; + } + + markers.add(tm); + + for (int i = 0; i < markers.size() - 1; i++) { + MarkerDisplayEntry markerEntry = new MarkerDisplayEntry(); + + markerEntry.marker = markers.get(i); + + // set string bounds + markerEntry.markBounds = getFontMetrics(markFont) + .getStringBounds(markers.get(i).getMark(), g); + markerEntry.notesBounds = getFontMetrics(notesFont) + .getStringBounds(markers.get(i).getNotes(), g); + + // calculate upper bound + if ((i == 0) && rangeStartDate.after(markerEntry.marker.getTimestamp())) { + //if this is the first marker (before the start time) set the + // Y coor to 0, top of display + markerEntry.relY = 0; + } else { + // otherwise, calculate how far down (%-wise) the mark is + markerEntry.relY = (float) (((double) (markerEntry.marker.getTimestamp().getTime() + - rangeStartDate.getTime())) / (double) rangeDiff); + } + + // calculate lower bound + if ((i == 0) && rangeStartDate.after(markerEntry.marker.getTimestamp())) + // if this is the first marker (before the start time), set the + // height to equal the top of the next marker + markerEntry.relHeight = + markers.get(i + 1).getTimestamp().getTime() + - rangeStartDate.getTime(); + else if (i == markers.size() - 2) + // if this is the last visible marker, set the height to extend + // to the bottom of the display + markerEntry.relHeight = rangeEndDate.getTime() + - markerEntry.marker.getTimestamp().getTime(); + else + // set the height to the difference between this marker and the + // next. + markerEntry.relHeight = + markers.get(i + 1).getTimestamp().getTime() + - markerEntry.marker.getTimestamp().getTime(); + markerEntry.relHeight /= rangeDiff; + + markerEntries.add(markerEntry); + } + repaint();*/ + } + + @Override + public void paintComponent(Graphics g) { + removeAll(); + + if (markerEntries == null) updateMarkers(g); + + Insets insets = this.getInsets(); + Rectangle bounds = this.getBounds(); + Rectangle canvasBounds = new Rectangle(insets.left, insets.top, + bounds.width - insets.left - insets.right - 1, + bounds.height - insets.top - insets.bottom - 1); + double hourHeight = canvasBounds.getHeight() / 24.0; + + Graphics2D g2d = (Graphics2D) g; + Rectangle2D stringBounds = getFontMetrics(getFont()).getStringBounds("00:00 ", g); + + // draw hour lines + for (TimeLegendEntry legendEntry : timeLegendLocations) { + g.drawLine(canvasBounds.x + (int) stringBounds.getWidth(), + (int) (canvasBounds.y + (canvasBounds.height * legendEntry.relY)), + canvasBounds.x + canvasBounds.width, + (int) (canvasBounds.y + (canvasBounds.height * legendEntry.relY))); + + g.drawString(legendEntry.label, canvasBounds.x + 2, + (int) (canvasBounds.y + (canvasBounds.height * legendEntry.relY) + + (stringBounds.getHeight() / 2))); + } + + for (int i = 0; i < markerEntries.size(); i++) { + + MarkerDisplayEntry curEntry = markerEntries.get(i); + + Rectangle2D markBounds; + Rectangle2D notesBounds; + + boolean selected = curEntry.marker.equals(currentMarker); + + // if i == 0, this is the default + curEntry.bounds = new Rectangle(); + curEntry.bounds.y = 3; + curEntry.bounds.x = canvasBounds.x + (int) stringBounds.getWidth() + 5; + curEntry.bounds.height = 1; + curEntry.bounds.width = canvasBounds.width - (int) stringBounds.getWidth() - 8; + + double relTime; + + // calculate upper bound + curEntry.bounds.y = (int) Math.round(curEntry.relY + * canvasBounds.getHeight()); + + if (i == 0) curEntry.bounds.y += 3; + + // calculate lower bound + curEntry.bounds.height = (int) Math.round(curEntry.relHeight + * canvasBounds.getHeight()); + + if (i ==0) curEntry.bounds.height -= 6; + else curEntry.bounds.height -= 3; + + // draw box + if (selected) g.setColor(selectedTrans); + else g.setColor((i % 2 == 0 ? evenTrans : oddTrans)); + g.fillRect(curEntry.bounds.x, curEntry.bounds.y, curEntry.bounds.width, curEntry.bounds.height); + + if (selected) g.setColor(selectedOpaque); + else g2d.setColor((i % 2 == 0 ? evenOpaque : oddOpaque)); + g2d.setStroke(new BasicStroke(3f)); + g2d.drawRect(curEntry.bounds.x, curEntry.bounds.y, curEntry.bounds.width, curEntry.bounds.height); + + // draw timestamp name + markBounds = (Rectangle2D) curEntry.markBounds.clone(); + markBounds.setRect(curEntry.bounds.x + 3, + curEntry.bounds.y + stringBounds.getHeight(), + markBounds.getWidth(), markBounds.getHeight()); + + g.setColor(fontColor); + g.setFont(markFont); + g.drawString(curEntry.marker.getMark(), + (int) markBounds.getX(), (int) markBounds.getY()); + + // draw notes + notesBounds = (Rectangle2D) curEntry.notesBounds.clone(); + notesBounds.setRect(curEntry.bounds.x + 6, + curEntry.bounds.y + stringBounds.getHeight() + markBounds.getHeight(), + notesBounds.getWidth(), notesBounds.getHeight()); + + if (curEntry.bounds.contains(notesBounds)) { + g.setFont(notesFont); + g.drawString(curEntry.marker.getNotes(), + (int) notesBounds.getX(), (int) notesBounds.getY()); + } + } + + g.setColor(Color.BLACK); + g.drawRect(canvasBounds.x, canvasBounds.y, canvasBounds.width, canvasBounds.height); + } + + public void addChangeListener(ChangeListener cl) { + changeListeners.add(cl); + } + + public boolean removeChangeListener(ChangeListener cl) { + return changeListeners.remove(cl); + } + + private void fireChangeEvent() { + for (ChangeListener cl : changeListeners) + cl.stateChanged(new ChangeEvent(this)); + } + + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + Point topLeft = getLocationOnScreen(); + currentMarker = null; + for (MarkerDisplayEntry markerEntry : markerEntries) { + Rectangle absBounds = new Rectangle(markerEntry.bounds); + absBounds.translate(topLeft.x, topLeft.y); + + // should only match one entry + if (absBounds.contains(e.getLocationOnScreen())) { + currentMarker = markerEntry.marker; + break; + } + } + repaint(); + fireChangeEvent(); + } else if (e.getButton() == MouseEvent.BUTTON3) { + setDay(rangeStartDate); + } + } + + public void mousePressed(MouseEvent e) { + lastMousePress = e.getPoint(); + } + + public void mouseReleased(MouseEvent e) { + Insets insets = this.getInsets(); + Rectangle bounds = this.getBounds(); + Rectangle canvasBounds = new Rectangle(insets.left, insets.top, + bounds.width - insets.left - insets.right - 1, + bounds.height - insets.top - insets.bottom - 1); + + double rangeDiff = rangeEndDate.getTime() - rangeStartDate.getTime(); + double y1 = lastMousePress.getY(); + double y2 = e.getY(); + + if (Math.abs(y2 - y1) < 5) return; + + // get time for y1 + long time1 = (long) Math.round((((y1 - canvasBounds.y) + / canvasBounds.height) * rangeDiff) + rangeStartDate.getTime()); + long time2 = (long) Math.round((((y2 - canvasBounds.y) + / canvasBounds.height) * rangeDiff) + rangeStartDate.getTime()); + + // left click, scroll + if (e.getButton() == MouseEvent.BUTTON1) { + long difference = time1 - time2; + rangeStartDate.setTime(rangeStartDate.getTime() + difference); + rangeEndDate.setTime(rangeEndDate.getTime() + difference); + } + // right click, zoom + else if (e.getButton() == MouseEvent.BUTTON3) { + if (time1 < time2) { + rangeStartDate.setTime(time1); + rangeEndDate.setTime(time2); + } else { + rangeStartDate.setTime(time2); + rangeEndDate.setTime(time1); + } + } + updateMarkers(getGraphics()); + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + public void stateChanged(ChangeEvent ce) { + updateMarkers(getGraphics()); + repaint(); + } + +} diff --git a/test/integration/PunchcardDialogTests.groovy b/test/integration/PunchcardDialogTests.groovy new file mode 100644 index 0000000..c24f8df --- /dev/null +++ b/test/integration/PunchcardDialogTests.groovy @@ -0,0 +1,10 @@ +import griffon.util.IGriffonApplication + +class PunchcardDialogTests extends GroovyTestCase { + + IGriffonApplication app + + void testSomething() { + + } +}