package scriptbuilder.gui.panels; import event.editor.frame.Editor; import event.editor.frame.Properties; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.io.File; import java.util.HashMap; import java.util.Map; import javax.swing.JFrame; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import javax.swing.event.MouseInputAdapter; import scriptbuilder.gui.IncidentEditorFrame; import scriptbuilder.gui.ScriptBuilderFrame; import scriptbuilder.gui.ScriptBuilderGuiConstants; import scriptbuilder.gui.drawers.CursorDrawer; import scriptbuilder.gui.drawers.EventIconDrawer; import scriptbuilder.gui.drawers.IncidentTimelineDrawer; import scriptbuilder.structures.ScriptEvent; import scriptbuilder.structures.ScriptEvent.ScriptEventType; import scriptbuilder.structures.ScriptIncident; import scriptbuilder.structures.SimulationScript; import scriptbuilder.structures.TimeSlice; import scriptbuilder.structures.events.I_ScriptEvent; /** * Represents a single incident timeline in the GUI. Listens for mouse actions. * * @author Greg Eddington * @author Bryan McGuffin * @version 2017/06/30 */ public class IncidentTimelinePanel extends JPanel { /** * The incident this panel represents. */ ScriptIncident incident; /** * If true, this panel is in its minimized state. */ //boolean collapsed; /** * If false, this panel won't be drawn. */ boolean visible; /** * If true, this panel has focus. */ boolean focused; /** * If true, right-clicking on this panel will produce the popup menu. */ private boolean hasPopupAccessRightClick; /** * If true, left-clicking on this panel will produce the popup menu. */ private boolean hasPopupAccessLeftClick; int cursorTime, lastSlice, x, y; /** * Filler time at the end of the screen for a particular incident */ public int requestedEditorFillerTime; /** * Filler time at the end of the screen for the whole script */ public static int requestedScriptBuilderFillerTime = 0; /** * Constant for amount of filler to add, in seconds. Set to 15 minutes */ public static final int FILLER_INTERVAL_SECONDS = 900; /** * The map representing the properties of this incident's events. Keys: * event types. Values: Properties objects for those events. */ public static Map eventTypeToPropertyMap; /** * Listener for the mouse. Receives notifications when the mouse enters, * exits, moves through, or clicks inside the panel. */ public class IncidentTimelineMouseListener extends MouseInputAdapter { /** * Action to take when the mouse enters the panel. Here, the incident * corresponding to this panel gains focus. * * @param e the mouse event */ @Override public void mouseEntered(MouseEvent e) { incident.setIncidentActive(); focused = true; } /** * Action to take when the mouse leaves the panel. Here, the incident * loses focus and the panel gets repainted. * * @param e the mouse event */ @Override public void mouseExited(MouseEvent e) { focused = false; repaint(); } /* * Popup menu for incident actions */ private JPopupMenu createPopup() { JPopupMenu menu = new JPopupMenu(); PopupMenuItemListener menuItemListener = new PopupMenuItemListener(); if(getTopLevelAncestor() instanceof ScriptBuilderFrame) { JMenuItem eventsMenuItem = new JMenuItem("Events"); JMenuItem propsMenuItem = new JMenuItem("Properties"); eventsMenuItem.setActionCommand("Edit Events"); propsMenuItem.setActionCommand("Modify Incident Properties"); eventsMenuItem.addActionListener(menuItemListener); propsMenuItem.addActionListener(menuItemListener); menu.add(eventsMenuItem); menu.add(propsMenuItem); } return menu; } private JPopupMenu createPopup(int x, int y) { JPopupMenu menu = new JPopupMenu(); PopupMenuItemListener menuItemListener = new PopupMenuItemListener(); if(getTopLevelAncestor() instanceof IncidentEditorFrame) { JMenuItem addTimeMenuItem = new JMenuItem("Add time here"); JMenuItem removeTimeMenuItem = new JMenuItem("Remove time here"); cursorTime =x; //logic that follows is used to "snap" the cursor location to the lines displayed on the timeline panel if (x % ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK > ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK / 2) { cursorTime += ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK - x % ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK; } else { cursorTime -= x % ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK; } int newSlice = (cursorTime / ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK); newSlice *= ScriptBuilderGuiConstants.HORIZONTAL_TICK_RESOLUTION; addTimeMenuItem.setActionCommand("Add Time " + newSlice); removeTimeMenuItem.setActionCommand("Remove Time " + newSlice); removeTimeMenuItem.addActionListener(menuItemListener); addTimeMenuItem.addActionListener(menuItemListener); menu.add(addTimeMenuItem); menu.add(removeTimeMenuItem); } return menu; } class PopupMenuItemListener implements ActionListener { public void actionPerformed(ActionEvent e) { JFrame topFrame = (JFrame) getTopLevelAncestor(); if (topFrame instanceof ScriptBuilderFrame) { SimulationScript script = ((ScriptBuilderFrame) topFrame).getScript(); if (e.getActionCommand().equals("Edit Events")) { IncidentEditorFrame editor = new IncidentEditorFrame(incident, (ScriptBuilderFrame) topFrame); script.addObserver(editor); editor.setVisible(true); ((ScriptBuilderFrame) topFrame).update(script, script); } if (e.getActionCommand().equals("Modify Incident Properties")) { ((ScriptBuilderFrame) topFrame).incidentDetailsScreen(incident); ((ScriptBuilderFrame) topFrame).update(script, script); } } if(topFrame instanceof IncidentEditorFrame) { if(e.getActionCommand().substring(0,8).equals("Add Time")) { int offset = Integer.parseInt(e.getActionCommand().substring(9,e.getActionCommand().length())); String addTimeInput = JOptionPane.showInputDialog("Add time(rounded to lowest 20s): "); int addTime = 0; try { addTime = Integer.parseInt(addTimeInput); }catch(NumberFormatException exception){ addTime = 0; System.err.print("an invalid number was inputted"); } addTime = addTime-(addTime%20); if(offset<=incident.offset+incident.length) { incident.moveAllFollowingEvents(offset, addTime); } } if(e.getActionCommand().substring(0,11).equals("Remove Time")) { //does addTime but with a negative time int offset = Integer.parseInt(e.getActionCommand().substring(11,e.getActionCommand().length()).trim()); String addTimeInput = JOptionPane.showInputDialog("Remove time(rounded to lowest 20s): "); int addTime = 0; try { addTime = Integer.parseInt(addTimeInput); }catch(NumberFormatException exception){ addTime = 0; System.err.print("an invalid number was inputted"); } addTime = -(addTime-(addTime%20)); //this if statement might be useless? if(offset<=incident.offset+incident.length) { incident.moveAllFollowingEvents(offset, addTime); } } //add my own handling of click menu here } topFrame.repaint(); } } /** * Note: Popup menus are triggered differently on different systems. * Therefore, isPopupTrigger should be checked in both mousePressed and * mouseReleased for proper cross-platform functionality. * @param e event that triggered this method */ @Override public void mousePressed(MouseEvent e) { int currentMouseX = e.getX(); int currentMouseY = e.getY(); // Does user want a popup menu? if (e.isPopupTrigger() && hasPopupAccessRightClick) { if(getTopLevelAncestor() instanceof IncidentEditorFrame) { if (((IncidentEditorFrame)getTopLevelAncestor()).currentEventType == null){ JPopupMenu popup = createPopup(currentMouseX,currentMouseY); popup.show(e.getComponent(), currentMouseX, currentMouseY); } }else { JPopupMenu popup = createPopup(); popup.show(e.getComponent(), currentMouseX, currentMouseY); } } } @Override public void mouseReleased(MouseEvent e) { int currentMouseX = e.getX(); int currentMouseY = e.getY(); // Does user want a popup menu? if (e.isPopupTrigger() && hasPopupAccessRightClick) { if(getTopLevelAncestor() instanceof IncidentEditorFrame) { JPopupMenu popup = createPopup(currentMouseX,currentMouseY); popup.show(e.getComponent(), currentMouseX, currentMouseY); }else { JPopupMenu popup = createPopup(); popup.show(e.getComponent(), currentMouseX, currentMouseY); } } } /** * Determine if the mouse click happened within a valid timeSlice on * this incident; if so, activate the Editor window for that timeSlice. *todo: fix bug where the event editor window appears even when a event type is not selected. * @param clickEvent the mouse event */ @Override public void mouseClicked(MouseEvent clickEvent) { Editor editor = null; ScriptBuilderFrame scriptBuilderFrameCurrent = null; IncidentEditorFrame incidentEditFrameCurrent = null; if (getTopLevelAncestor() instanceof ScriptBuilderFrame) { scriptBuilderFrameCurrent = (ScriptBuilderFrame) getTopLevelAncestor(); } else if (getTopLevelAncestor() instanceof IncidentEditorFrame) { incidentEditFrameCurrent = (IncidentEditorFrame) getTopLevelAncestor(); editor = new Editor(incidentEditFrameCurrent); } x = cursorTime = clickEvent.getX(); y = clickEvent.getY(); //System.out.println(cursorTime); //logic that follows is used to "snap" the cursor location to the lines displayed on the timeline panel if (clickEvent.getX() % ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK > ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK / 2) { cursorTime += ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK - clickEvent.getX() % ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK; } else { cursorTime -= clickEvent.getX() % ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK; } int newSlice = (cursorTime / ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK); newSlice *= ScriptBuilderGuiConstants.HORIZONTAL_TICK_RESOLUTION; /** * Check if click is out of bounds * */ if (newSlice < 0 || incident == null) { return; } /** * Add a new icon if left mouse button was clicked */ if (clickEvent.getButton() == MouseEvent.BUTTON1) { if (getTopLevelAncestor() instanceof IncidentEditorFrame) { if (incidentEditFrameCurrent.currentEventType != null) { if ((incident.slices.size() == 0 && newSlice != 0)||(incident.slices.get(0).events.isEmpty())) { JOptionPane.showMessageDialog(incidentEditFrameCurrent, "This is the first event in the incident.\n" + "Therefore, it will be automatically positioned at time 00:00:00,\n" + "relative to the start of the incident.", "Event will be moved", JOptionPane.INFORMATION_MESSAGE); newSlice = 0; } //add the time of addition to be passed through to the event itself somewhere in here? Stored in newSlice I_ScriptEvent newScriptEvent = ScriptEvent.factoryByType(incidentEditFrameCurrent.currentEventType); // if (editor != null) // { // //this is not necessary if we just load the slices after we create a new one in the window, creates some extra dummy slice for no reason. // //this adds a new event to the existing editor window // editor.addEvent(eventTypeToPropertyMap.get(incidentEditFrameCurrent.currentEventType), newScriptEvent); // } if (incident.slices.get(newSlice) == null || incident.slices.get(newSlice).events.isEmpty()) { //if there is no slice at the newSlice time, then create a new event with a new slice at that time incident.addNewEvent(newScriptEvent, newSlice); //find out where the new slice is added and then make it //possible to //editor.setSlice(incident.getSlices().get(newSlice)); } else { //if there is already a slice there, just add the event to the incident.slices.get(newSlice).addEvent(newScriptEvent); } incidentEditFrameCurrent.update(null, incidentEditFrameCurrent.getIncident()); } } } // //code below may be useless // if(SwingUtilities.isRightMouseButton(clickEvent)) // { // if (getTopLevelAncestor() instanceof IncidentEditorFrame && false) // { // incidentEditFrameCurrent = (IncidentEditorFrame) getTopLevelAncestor(); // if(incidentEditFrameCurrent.currentEventType == null) // { // JPopupMenu timeAddMenu = new JPopupMenu(); // JMenuItem addTimeButton = new JMenuItem("Add time here"); // //addTimeButton.addActionListener(al); // timeAddMenu.add(addTimeButton); // timeAddMenu.show(clickEvent.getComponent(),clickEvent.getX(),clickEvent.getY()); // // } // // } // } if (editor != null) { editor.setSlice(incident.slices.get(newSlice)); } if (incident.slices.get(newSlice) != null && getTopLevelAncestor() instanceof IncidentEditorFrame) { editor.setVisible(true); } } /** * Determine if the mouse is now hovering over a valid timeslice; if so, * alter tooltip text and info window text to reflect the new timeslice. * * @param e the mouse event */ @Override public void mouseMoved(MouseEvent e) { x = cursorTime = e.getX(); y = e.getY(); if (e.getX() % ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK > ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK / 2) { cursorTime += ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK - e.getX() % ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK; } else { cursorTime -= e.getX() % ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK; } if (incident != null) { int newSlice = (cursorTime / ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK); newSlice *= ScriptBuilderGuiConstants.HORIZONTAL_TICK_RESOLUTION; if (newSlice >= 0 && incident.slices.get(newSlice) != null) { incident.setSliceActive(newSlice); lastSlice = newSlice; String newToolTip; newToolTip = incident.slices.get(newSlice).getToolTipText(y); setToolTipText((newToolTip == null || newToolTip.equals("")) ? null : newToolTip); } } repaint(); } } /** * Constructor. Generates a HashMap of all possible event types. */ public IncidentTimelinePanel() { super(); hasPopupAccessRightClick = true; requestedEditorFillerTime = 0; // FACILITATOR_EVAL_EVENT, RADIO_EVAL_EVENT eventTypeToPropertyMap = new HashMap(); eventTypeToPropertyMap.put(ScriptEventType.AUDIO_EVENT, Properties.Audio); eventTypeToPropertyMap.put(ScriptEventType.CAD_EVENT, Properties.CADLog); eventTypeToPropertyMap.put(ScriptEventType.CCTV_EVENT, Properties.CCTV); eventTypeToPropertyMap.put(ScriptEventType.CHP_RADIO_EVENT, Properties.CHPRadio); eventTypeToPropertyMap.put(ScriptEventType.PARAMICS_EVENT, Properties.Paramics); eventTypeToPropertyMap.put(ScriptEventType.TOW_EVENT, Properties.Tow); eventTypeToPropertyMap.put(ScriptEventType.UNIT_EVENT, Properties.Unit); eventTypeToPropertyMap.put(ScriptEventType.WITNESS_EVENT, Properties.Witness); eventTypeToPropertyMap.put(ScriptEventType.MAINTENANCE_RADIO_EVENT, Properties.MaintenanceRadio); eventTypeToPropertyMap.put(ScriptEventType.TMT_RADIO_EVENT, Properties.TMTRadio); eventTypeToPropertyMap.put(ScriptEventType.TELEPHONE_EVENT, Properties.Telephone); eventTypeToPropertyMap.put(ScriptEventType.ATMS_EVAL_EVENT, Properties.ATMS); eventTypeToPropertyMap.put(ScriptEventType.ACTIVITY_LOG_EVAL_EVENT, Properties.ActivityLog); eventTypeToPropertyMap.put(ScriptEventType.CAD_EVAL_EVENT, Properties.CAD); eventTypeToPropertyMap.put(ScriptEventType.CMS_EVAL_EVENT, Properties.CMS); eventTypeToPropertyMap.put(ScriptEventType.FACILITATOR_EVAL_EVENT, Properties.Facilitator); eventTypeToPropertyMap.put(ScriptEventType.RADIO_EVAL_EVENT, Properties.Radio); // Add the mouse listener IncidentTimelineMouseListener mouseListener = new IncidentTimelineMouseListener(); addMouseMotionListener(mouseListener); addMouseListener(mouseListener); } /** * Constructor. Generates a HashMap of all possible event types. * * @param usesPopup determines whether or not right-clicking on this panel * will display a popup menu. */ public IncidentTimelinePanel(boolean usesPopup) { super(); hasPopupAccessRightClick = usesPopup; requestedEditorFillerTime = 0; // FACILITATOR_EVAL_EVENT, RADIO_EVAL_EVENT eventTypeToPropertyMap = new HashMap(); eventTypeToPropertyMap.put(ScriptEventType.AUDIO_EVENT, Properties.Audio); eventTypeToPropertyMap.put(ScriptEventType.CAD_EVENT, Properties.CADLog); eventTypeToPropertyMap.put(ScriptEventType.CCTV_EVENT, Properties.CCTV); eventTypeToPropertyMap.put(ScriptEventType.CHP_RADIO_EVENT, Properties.CHPRadio); eventTypeToPropertyMap.put(ScriptEventType.PARAMICS_EVENT, Properties.Paramics); eventTypeToPropertyMap.put(ScriptEventType.TOW_EVENT, Properties.Tow); eventTypeToPropertyMap.put(ScriptEventType.UNIT_EVENT, Properties.Unit); eventTypeToPropertyMap.put(ScriptEventType.WITNESS_EVENT, Properties.Witness); eventTypeToPropertyMap.put(ScriptEventType.MAINTENANCE_RADIO_EVENT, Properties.MaintenanceRadio); eventTypeToPropertyMap.put(ScriptEventType.TMT_RADIO_EVENT, Properties.TMTRadio); eventTypeToPropertyMap.put(ScriptEventType.TELEPHONE_EVENT, Properties.Telephone); eventTypeToPropertyMap.put(ScriptEventType.ATMS_EVAL_EVENT, Properties.ATMS); eventTypeToPropertyMap.put(ScriptEventType.ACTIVITY_LOG_EVAL_EVENT, Properties.ActivityLog); eventTypeToPropertyMap.put(ScriptEventType.CAD_EVAL_EVENT, Properties.CAD); eventTypeToPropertyMap.put(ScriptEventType.CMS_EVAL_EVENT, Properties.CMS); eventTypeToPropertyMap.put(ScriptEventType.FACILITATOR_EVAL_EVENT, Properties.Facilitator); eventTypeToPropertyMap.put(ScriptEventType.RADIO_EVAL_EVENT, Properties.Radio); // Add the mouse listener IncidentTimelineMouseListener mouseListener = new IncidentTimelineMouseListener(); addMouseMotionListener(mouseListener); addMouseListener(mouseListener); } /** * Update the panel if it's changed collapsed status. Redraw it with the * correct dimensions for its status. * * @param incident the incident this panel represents */ public void timelinePanelUpdate(ScriptIncident incident) { this.incident = incident; this.visible = (incident != null); Dimension newSize; if (visible) { if (getTopLevelAncestor() instanceof IncidentEditorFrame) { newSize = new Dimension(((incident.length + incident.offset + requestedEditorFillerTime) / ScriptBuilderGuiConstants.HORIZONTAL_TICK_RESOLUTION * ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK) + ScriptBuilderGuiConstants.EVENT_ICON_WIDTH, ScriptBuilderGuiConstants.TIMELINE_COLLAPSED_HEIGHT + ScriptBuilderGuiConstants.SCRIPT_EVENT_ICON_STEP * 2); } else { newSize = new Dimension(((incident.length + incident.offset + requestedScriptBuilderFillerTime) / ScriptBuilderGuiConstants.HORIZONTAL_TICK_RESOLUTION * ScriptBuilderGuiConstants.PIXEL_WIDTH_PER_HORIZONTAL_TICK) + ScriptBuilderGuiConstants.EVENT_ICON_WIDTH, ScriptBuilderGuiConstants.TIMELINE_COLLAPSED_HEIGHT + ScriptBuilderGuiConstants.SCRIPT_EVENT_ICON_STEP * 2); } } else { newSize = new Dimension(0, 0); } this.setSize(newSize); this.setPreferredSize(newSize); invalidate(); } /** * Redraw this panel and all the icons inside it. If user is holding an * event, draw that icon under the mouse. * * @param g the graphics component */ @Override public void paint(Graphics g) { super.paint(g); if (!visible) { return; } Graphics2D g2d = (Graphics2D) g; // jdalbey removed this decision and replaced it with ELSE clause // so it will work with any alternate ancestor. // if (getTopLevelAncestor() instanceof ScriptBuilderFrame) // { // IncidentTimelineDrawer.DrawScriptBuilderTimeline(g2d, incident); // } if (getTopLevelAncestor() instanceof IncidentEditorFrame) { IncidentTimelineDrawer.DrawIncidentTimeline(g2d, incident, false); } else { IncidentTimelineDrawer.DrawScriptBuilderTimeline(g2d, incident); } if (focused) { //System.out.println("Cursor Time: " + cursorTime); CursorDrawer.DrawCursor(g2d, cursorTime, false); if (this.getTopLevelAncestor() instanceof ScriptBuilderFrame) { if (((ScriptBuilderFrame) this.getTopLevelAncestor()).currentEventType != null) { EventIconDrawer.DrawEventIcon(g2d, ((ScriptBuilderFrame) this.getTopLevelAncestor()).currentEventType, x + 5, y + 10); } } if (this.getTopLevelAncestor() instanceof IncidentEditorFrame) { if (((IncidentEditorFrame) this.getTopLevelAncestor()).currentEventType != null) { EventIconDrawer.DrawEventIcon(g2d, ((IncidentEditorFrame) this.getTopLevelAncestor()).currentEventType, x + 5, y + 10); }else { //when we aren't selecting one of the incident adding buttons, this. //this.selectMode = ((IncidentEditorFrame)this.getTopLevelAncestor()).selectMode; if(((IncidentEditorFrame)this.getTopLevelAncestor()).selectMode) { //cursorTime dictates where we are in the x direction //there seems to be no way to tell where we are in the y direction //implement something? } } } } } /** * Local main for viewing this panel only. * * @author jdalbey * @param args not used */ public static void main(String[] args) { JFrame frame = new JFrame("ScriptBuilderTimelinePanel Demo"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); IncidentTimelinePanel pnl = new IncidentTimelinePanel(); // Create a script File inFile = new File("test/scriptbuilder/structures/test_input_file.xml"); SimulationScript script = new SimulationScript(); script.loadScriptFromFile(inFile); // retrieve a single incident from the script ScriptIncident inci = script.incidents.get(2); // update this panel with an incident pnl.timelinePanelUpdate(inci); frame.getContentPane().add(pnl, BorderLayout.CENTER); frame.pack(); frame.setVisible(true); } }