/* * 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.app; import java.awt.BorderLayout; import java.awt.Component; import java.awt.FileDialog; import java.awt.Font; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; import javax.swing.Action; import javax.swing.Box; import javax.swing.JCheckBoxMenuItem; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JSeparator; import javax.swing.KeyStroke; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; import taskblocks.graph.GraphActionListener; import taskblocks.graph.TaskGraphComponent; import taskblocks.io.BugzillaExportDialog; import taskblocks.io.ProjectSaveLoad; import taskblocks.io.WrongDataException; import taskblocks.modelimpl.ManImpl; import taskblocks.modelimpl.TaskImpl; import taskblocks.modelimpl.TaskModelImpl; import taskblocks.modelimpl.TaskPainterImpl; import taskblocks.modelimpl.UndoActionManModify; import taskblocks.modelimpl.UndoActionTaskModify; import taskblocks.modelimpl.UndoManager; public class ProjectFrame extends JFrame implements WindowListener, GraphActionListener { static int _numWindows; static List<JMenuItem> _windowMenuItems = new ArrayList<JMenuItem>(); TaskModelImpl _taskModel; TaskGraphComponent _graph; URL _file; boolean _newCleanProject; JCheckBoxMenuItem _myWindowMenuItem; Preferences _prefs = Preferences.userNodeForPackage(this.getClass()); Action _shrinkAction = new MyAction("Shrink", TaskBlocks.getImage("shrink.png"), "Shrink tasks as near as possible") { public void actionPerformed(ActionEvent e) { _graph.getGraphRepresentation().shrinkTasks(); _graph.repaint(); } }; Action _scaleDownAction = new MyAction("Zoom Out", TaskBlocks.getImage("zoomOut.png")) { public void actionPerformed(ActionEvent arg0) { _graph.scaleDown(); } }; Action _scaleUpAction = new MyAction("Zoom In", TaskBlocks.getImage("zoomIn.png")) { public void actionPerformed(ActionEvent arg0) { _graph.scaleUp(); } }; Action _closeFileAction = new MyAction("Close") { public void actionPerformed(ActionEvent arg0) { tryClose(); } }; Action _loadFileAction = new MyAction("Open...", TaskBlocks.getImage("folder.gif"), "Lets you open an existing project") { public void actionPerformed(ActionEvent e) { File f = null; if(TaskBlocks.RUNNING_ON_MAC || TaskBlocks.RUNNING_ON_WINDOWS) { // MacOS & Windows user feeling FileDialog fd = new FileDialog(ProjectFrame.this, "blabla", FileDialog.LOAD); fd.setVisible(true); if(fd.getFile() != null) { f = new File(fd.getDirectory(), fd.getFile()); } } else { JFileChooser fc = new JFileChooser(); fc.setFileSelectionMode(JFileChooser.FILES_ONLY); fc.showOpenDialog(ProjectFrame.this); f = fc.getSelectedFile(); } if(f != null) { openFile(f); } } }; class OpenRecentFileAction extends MyAction { String _path; public OpenRecentFileAction(String path) { super(path); _path = path; } public void actionPerformed(ActionEvent e) { openFile(new File(_path)); } } Action _newFileAction = new MyAction("New Project") { public void actionPerformed(ActionEvent e) { new ProjectFrame(); } }; Action _saveAction = new MyAction("Save", TaskBlocks.getImage("save.png")) { public void actionPerformed(ActionEvent e) { save(); } }; MyAction _undoAction = new MyAction("Undo") { public void actionPerformed(ActionEvent e) { final UndoManager um = _taskModel.getUndoManager(); // We try to update the action's "enabled" state after undo/redo, but // if user changes tasks in GUI, the undo/redo action is not updated. So we must check here too. if(um.canUndo()) { _graph.getGraphRepresentation().updateModel(); // GUI -> model update um.undo(); _graph.setModel(_taskModel); // model -> GUI _graph.getGraphRepresentation().setDirty(); // the model->GUI resetted the dirty flag _graph.repaint(); } } public void setEnabled(boolean enabled) { super.setEnabled(enabled); } }; MyAction _redoAction = new MyAction("Redo") { public void actionPerformed(ActionEvent e) { final UndoManager um = _taskModel.getUndoManager(); // We try to update the action's "enabled" state after undo/redo, but // if user changes tasks in GUI, the undo/redo action is not updated. So we must check here too. if(um.canRedo()) { _graph.getGraphRepresentation().updateModel(); // GUI -> model update um.redo(); _graph.setModel(_taskModel); // model -> GUI _graph.getGraphRepresentation().setDirty(); // the model->GUI resetted the dirty flag _graph.repaint(); } } }; Action _saveAsAction = new MyAction("Save As...") { public void actionPerformed(ActionEvent e) { URL oldFile = _file; _file = null; if(!save()) { _file = oldFile; } } }; Action _leftAction = new MyAction("Left", TaskBlocks.getImage("left.gif"), "Scrolls left") { public void actionPerformed(ActionEvent e) { _graph.moveLeft(); } }; Action _focusTodayAction = new MyAction("Focus on today", TaskBlocks.getImage("down.gif"), "Scrolls to current day") { public void actionPerformed(ActionEvent e) { _graph.focusOnToday(); } }; Action _rightAction = new MyAction("Right", TaskBlocks.getImage("right.gif"), "Scrolls right") { public void actionPerformed(ActionEvent e) { _graph.moveRight(); } }; ChangeListener _graphChangeListener = new ChangeListener(){ public void stateChanged(ChangeEvent e) { _newCleanProject = false; updateActionsEnableState(); } }; Action _minimizeAction = new MyAction("Minimize"){ public void actionPerformed(ActionEvent e) { ProjectFrame.this.setExtendedState(JFrame.ICONIFIED); } }; Action _newTaskAction = new MyAction("New Task...", TaskBlocks.getImage("newtask.png"), "Opens the New Task Wizard"){ public void actionPerformed(ActionEvent e) { TaskConfigDialog.openDialog(ProjectFrame.this, null, _taskModel, _graph, true); } }; Action _newManAction = new MyAction("New Worker...", TaskBlocks.getImage("newman.png"), "Opens the New Worker Wizard"){ public void actionPerformed(ActionEvent e) { ManConfigDialog.openDialog(ProjectFrame.this, null, _taskModel, _graph, true); } }; Action _aboutAction = new MyAction("About..."){ public void actionPerformed(ActionEvent e) { AboutDialog.showAbout(ProjectFrame.this); } }; Action _bugzillaSubmit = new MyAction("Export to Bugzilla...", TaskBlocks.getImage("bugzilla.png"), "Opens the Bugzilla Export dialog"){ public void actionPerformed(ActionEvent e) { BugzillaExportDialog.openDialog(ProjectFrame.this, _taskModel._tasks, new ChangeListener() { public void stateChanged(ChangeEvent arg0) { _graph.getGraphRepresentation().setDirty(); } }); }}; Action _deleteSel = new MyAction("Delete Selection", TaskBlocks.getImage("delete.gif"), "Deletes selected objects") { public void actionPerformed(ActionEvent e) { _graph.deleteSelection(); } }; ChangeListener _undoRedoChangeListener = new ChangeListener() { public void stateChanged(ChangeEvent e) { updateUndoRedoMenu(); } }; /** * creates window with empty project. */ public ProjectFrame() { this(TaskModelImpl.createEmptyModel()); setTitle("New project"); _newCleanProject = true; updateActionsEnableState(); updateUndoRedoMenu(); } // public ProjectFrame(File f) throws WrongDataException { // this(new ProjectSaveLoad().loadProject(f)); // setFile(f); // updateActionsEnableState(); // updateUndoRedoMenu(); // } private ProjectFrame(TaskModelImpl model) { // there are some issues with transparent icon in frame, so we use icon // with filled background. this.setIconImage(TaskBlocks.getImage("frameicon32.png").getImage()); _taskModel = model; _taskModel.getUndoManager().setChangeListener(_undoRedoChangeListener); buildGui(); pack(); setSize(800,400); fillMenu(); _graph.setGraphChangeListener(_graphChangeListener); this.addWindowListener(this); setLocationRelativeTo(null); setVisible(true); _numWindows++; } /** * Opens the specified file. If this frame contains empty project, opens the file in this frame. * If project in this frame is modified, opens new window with it. * * @param f */ public void openFile(File f) { try { openURL(f.toURI().toURL()); } catch (MalformedURLException e) { // NEVER GET HERE throw new RuntimeException(e); } } public void openURL(URL url) { try { // check if the project is empty _graph.getGraphRepresentation().updateModel(); if(_taskModel._tasks.length == 0) { _taskModel = new ProjectSaveLoad().loadProject(url); _taskModel.getUndoManager().setChangeListener(_undoRedoChangeListener); _graph.setModel(_taskModel); setFile(url); updateActionsEnableState(); } else { new ProjectFrame().openURL(url); } } catch (WrongDataException e) { JOptionPane.showMessageDialog(null, "<html><b>Couldn't Open File</b><br><br><font size=\"-2\">" + e.getMessage() + "<br><br>"); } catch(Exception e) { JOptionPane.showMessageDialog(null, "<html><b>Couldn't Open File</b><br><br><font size=\"-2\">" + e.getMessage() + "<br><br>"); } } public void setTitle(String title) { super.setTitle(title + " - Task Blocks"); _myWindowMenuItem.setText(title); } public void windowActivated(WindowEvent arg0) { } public void windowClosed(WindowEvent arg0) { _numWindows--; _windowMenuItems.remove(_myWindowMenuItem); if(_numWindows <= 0) { System.exit(0); } } public void windowClosing(WindowEvent arg0) { tryClose(); } public void windowDeactivated(WindowEvent arg0) {} public void windowDeiconified(WindowEvent arg0) {} public void windowIconified(WindowEvent arg0) {} public void windowOpened(WindowEvent arg0) {} private void buildGui() { // create components JPanel mainP = new JPanel(new BorderLayout()); MyToolBar toolB = new MyToolBar(); _graph = new TaskGraphComponent(_taskModel, new TaskPainterImpl()); // setup toolbar actions toolB.add(_loadFileAction); toolB.add(_saveAction); toolB.add(Box.createHorizontalStrut(4)); toolB.add(new FixedSeparator(FixedSeparator.VERTICAL)); toolB.add(Box.createHorizontalStrut(4)); toolB.add(_newTaskAction); toolB.add(_newManAction); toolB.add(_shrinkAction); toolB.add(Box.createHorizontalStrut(4)); toolB.add(new FixedSeparator(FixedSeparator.VERTICAL)); toolB.add(Box.createHorizontalStrut(4)); toolB.add(_scaleUpAction); toolB.add(_scaleDownAction); toolB.add(Box.createHorizontalStrut(8)); toolB.add(_leftAction); toolB.add(_rightAction); toolB.add(_focusTodayAction); toolB.add(Box.createHorizontalStrut(4)); toolB.add(new FixedSeparator(FixedSeparator.VERTICAL)); toolB.add(Box.createHorizontalStrut(4)); toolB.add(_deleteSel); // set component's properties this.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); mainP.add(toolB, BorderLayout.NORTH); mainP.add(_graph, BorderLayout.CENTER); getContentPane().setLayout(new BorderLayout()); getContentPane().add(mainP); //toolB.setRollover(true); toolB.setFloatable(false); _graph.setGraphActionListener(this); } private void fillMenu() { JMenuBar menu = getJMenuBar(); if(menu == null) { menu = new JMenuBar(); this.setJMenuBar(menu); } JMenu menuFile = new JMenu("File"); menuFile.add(_newFileAction).setAccelerator(getAcceleratorStroke('N')); menuFile.add(_loadFileAction).setAccelerator(getAcceleratorStroke('O')); menuFile.add(new JSeparator()); menuFile.add(_saveAction).setAccelerator(getAcceleratorStroke('S')); menuFile.add(_saveAsAction); menuFile.add(new JSeparator()); fillRecentFilesMenu(menuFile); menuFile.add(new JSeparator()); menuFile.add(_closeFileAction).setAccelerator(getAcceleratorStroke('W')); menu.add(menuFile); JMenu menuEdit = new JMenu("Edit"); menuEdit.add(_undoAction).setAccelerator(getAcceleratorStroke('Z')); menuEdit.add(_redoAction).setAccelerator(getAcceleratorStroke('Y')); menu.add(menuEdit); JMenu menuProject = new JMenu("Project"); menuProject.add(_newTaskAction).setAccelerator(getAcceleratorStroke('T')); menuProject.add(_newManAction).setAccelerator(getAcceleratorStroke('U')); menuProject.add(new JSeparator()); JMenuItem mi = menuProject.add(_deleteSel); if(TaskBlocks.RUNNING_ON_MAC) { mi.setAccelerator(getAcceleratorStroke(KeyEvent.VK_BACK_SPACE)); } else { mi.setAccelerator(getAcceleratorStroke(KeyEvent.VK_DELETE)); } menuProject.add(new JSeparator()); menuProject.add(_shrinkAction).setAccelerator(getAcceleratorStroke('R')); menuProject.add(_bugzillaSubmit); menu.add(menuProject); final JMenu menuWindow = new JMenu("Window"); _myWindowMenuItem = new JCheckBoxMenuItem("???"); _myWindowMenuItem.setFont(_myWindowMenuItem.getFont().deriveFont(Font.PLAIN)); _myWindowMenuItem.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { ProjectFrame.this.toFront(); }}); _windowMenuItems.add(_myWindowMenuItem); menuWindow.addMenuListener(new MenuListener(){ public void menuCanceled(MenuEvent e) { } public void menuDeselected(MenuEvent e) { } public void menuSelected(MenuEvent e) { menuWindow.removeAll(); JMenuItem minItem = menuWindow.add(new JMenuItem(_minimizeAction)); minItem.setAccelerator(getAcceleratorStroke('M')); minItem.setFont(minItem.getFont().deriveFont(Font.PLAIN)); menuWindow.add(new JSeparator()); for(JMenuItem winItem: _windowMenuItems) { menuWindow.add(winItem); winItem.setSelected(winItem == _myWindowMenuItem); } }}); menu.add(menuWindow); if(!TaskBlocks.RUNNING_ON_MAC) { JMenu menuHelp = new JMenu("Help"); menuHelp.add(_aboutAction); menu.add(menuHelp); } // not bold menu items and on mac without icons for(int i = 0; i < menu.getMenuCount(); i++) { JMenu subMenu = menu.getMenu(i); subMenu.setFont(subMenu.getFont().deriveFont(Font.PLAIN)); for(Component c: subMenu.getMenuComponents()) { if(c instanceof JMenuItem) { // MacOS user feeling if(TaskBlocks.RUNNING_ON_MAC) { // clear icons from all menus. It is convention on Mac ((JMenuItem)c).setIcon(null); } ((JMenuItem)c).setFont(c.getFont().deriveFont(Font.PLAIN)); } } } } private void fillRecentFilesMenu(JMenu menuFile) { try { Preferences p = _prefs.node("recentFiles"); for(String child: p.childrenNames()) { String path = p.node(child).get("path", null); if(path != null) { menuFile.add(new OpenRecentFileAction(path)); } } } catch (BackingStoreException e) { // NOTHING TO DO } } private void addToRecentFiles(File f) { try { Preferences p = _prefs.node("recentFiles"); String[] childs = p.childrenNames(); for(String child: childs) { String path = p.node(child).get("path", null); if(path != null) { if(path.equals(f.getAbsolutePath())) { // is already in recent list - do nothing return; } } } if(childs.length >= 5) { p.node(childs[0]).removeNode(); } Preferences newNode = p.node(String.valueOf(System.currentTimeMillis())); newNode.put("path", f.getAbsolutePath()); } catch (BackingStoreException e) { // NOTHING TO DO } } private KeyStroke getAcceleratorStroke(char key) { return KeyStroke.getKeyStroke(key, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()); } private KeyStroke getAcceleratorStroke(int keyCode) { return KeyStroke.getKeyStroke(keyCode, 0); } private void setFile(URL url) { _file = url; setTitle(getShortName(_file)); // TODO: //addToRecentFiles(f); } private void setFile(File f) { try { _file = f.toURI().toURL(); } catch (MalformedURLException e) { // NEVER GET HERE throw new RuntimeException(e); } setTitle(f.getName()); addToRecentFiles(f); } private String getShortName(URL url) { if(url == null) { return null; } String path = url.getPath(); if(path == null) { return null; } int idx = path.lastIndexOf("/"); if(idx >= 0) { return path.substring(idx+1); } return path; } private void tryClose() { if(_graph.getGraphRepresentation().isSaveDirty()) { // not saved - ask for save String SAVE = "Save"; String DONT_SAVE = "Don't Save"; String CANCEL = "Cancel"; Object[] options; // from unknown reasons the appearing order is reversed on Mac if(TaskBlocks.RUNNING_ON_MAC) { options = new Object[] {SAVE, CANCEL, DONT_SAVE}; } else { options = new Object[] {DONT_SAVE, SAVE, CANCEL}; } JLabel l = new JLabel("<html><b>Do you want to save changes to this document<br>before closing?</b><br><br><font size=\"-2\">If you don't save, your changes will be lost.<br></font><br>"); l.setFont(l.getFont().deriveFont(Font.PLAIN)); JOptionPane op = new JOptionPane(l, JOptionPane.QUESTION_MESSAGE, 0, null, options); op.createDialog(this, _file == null ? "Unsaved project" : getShortName(_file)).setVisible(true); op.setInitialSelectionValue(CANCEL); Object choice = op.getValue(); if(choice == null) { return; // cancel; } if(choice.equals(SAVE)) { // save if(!save()) { // save didn't success, don't close return; } else { this.dispose(); } } else if(choice.equals(DONT_SAVE)) { this.dispose(); } } else { this.dispose(); } } private void updateActionsEnableState() { boolean unsaved = _newCleanProject || _graph.getGraphRepresentation().isSaveDirty(); _saveAction.setEnabled(unsaved); // MacOS user feeling getRootPane().putClientProperty("windowModified", unsaved?Boolean.TRUE : Boolean.FALSE); _newTaskAction.setEnabled(_graph.getGraphRepresentation().getManCount() > 0); } private boolean save() { _graph.getGraphRepresentation().updateModel(); try { URL f = _file; if(f == null) { // ask for file if(TaskBlocks.RUNNING_ON_MAC || TaskBlocks.RUNNING_ON_WINDOWS) { // MacOS user feeling FileDialog fd = new FileDialog(ProjectFrame.this, "Save", FileDialog.SAVE); fd.setVisible(true); if(fd.getFile() != null) { File f2 = new File(fd.getDirectory(), fd.getFile()); if(f2 != null) { f = f2.toURI().toURL();} } } else { JFileChooser fc = new JFileChooser(); fc.setFileSelectionMode(JFileChooser.FILES_ONLY); fc.showSaveDialog(ProjectFrame.this); File file = fc.getSelectedFile(); if(file != null) {f = file.toURI().toURL();} } } if(f != null) { new ProjectSaveLoad().saveProject(f, _taskModel); setFile(f); _graph.getGraphRepresentation().clearSaveDirtyFlag(); return true; } } catch (Exception e1) { JOptionPane.showMessageDialog(null, "<html><b>Couldn't Save</b><br><br><font size=\"-2\">" + e1.getMessage() + "<br><br>"); e1.printStackTrace(); } return false; } private void configureTask(TaskImpl t) { _graph.getGraphRepresentation().updateModel(); // GUI -> model update TaskImpl before = t.clone(); if(TaskConfigDialog.openDialog(this, t, _taskModel, _graph, false)) { _graph.setModel(_taskModel); // model -> GUI udate _graph.getGraphRepresentation().setDirty(); // the model->GUI resetted the dirty flag _graph.repaint(); _taskModel.getUndoManager().addAction(new UndoActionTaskModify(_taskModel, before, t)); } } private void configureMan(ManImpl man) { _graph.getGraphRepresentation().updateModel(); // GUI -> model update ManImpl before = man.clone(); if(ManConfigDialog.openDialog(this, man, _taskModel, _graph, false)) { _graph.setModel(_taskModel); // model -> GUI udate _graph.getGraphRepresentation().setDirty(); // the model->GUI resetted the dirty flag _graph.repaint(); _taskModel.getUndoManager().addAction(new UndoActionManModify(_taskModel, before, man)); } } public void graphClicked(MouseEvent e) { } public void manClicked(Object man, MouseEvent e) { if(man != null && e.getClickCount() >= 2) { configureMan((ManImpl)man); } } public void taskClicked(Object task, MouseEvent e) { if(task != null && e.getClickCount() >= 2) { configureTask((TaskImpl)task); } } private void updateUndoRedoMenu() { final UndoManager um = _taskModel.getUndoManager(); if(um.canUndo()) { String name = um.getFirstUndoActionLabel(); if(name.length() > 23) { name = name.substring(0, 20) + "..."; } _undoAction.putValue(Action.NAME, "Undo - " + name); _undoAction.setEnabled(true); } else { _undoAction.putValue(Action.NAME, "Undo"); _undoAction.setEnabled(false); } if(um.canRedo()) { String name = um.getFirstRedoActionLabel(); if(name.length() > 23) { name = name.substring(0, 20) + "..."; } _redoAction.putValue(Action.NAME, "Redo - " + name); _redoAction.setEnabled(true); } else { _redoAction.putValue(Action.NAME, "Redo"); _redoAction.setEnabled(false); } } }