package timeflow.app; import timeflow.app.ui.*; import timeflow.app.actions.*; import timeflow.app.ui.filter.*; import timeflow.data.db.*; import timeflow.data.time.RoughTime; import timeflow.format.field.*; import timeflow.format.file.*; import timeflow.model.*; import timeflow.views.*; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import timeflow.util.Pad; import java.awt.*; import java.awt.event.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.*; import java.util.ArrayList; public class TimeflowApp extends JFrame { public TFModel model=new TFModel(); public JFileChooser fileChooser; AboutWindow splash; String[][] examples; String[] templates; AppState state=new AppState(); JMenu openRecent=new JMenu("Open Recent"); public JMenu filterMenu; JMenuItem save=new JMenuItem("Save"); FilterControlPanel filterControlPanel; LinkTabPane leftPanel; TFListener filterMenuMaker=new TFListener() { @Override public void note(TFEvent e) { if (e.affectsSchema()) { filterMenu.removeAll(); for (Field f: model.getDB().getFields()) { if (f.getType()==String.class || f.getType()==String[].class || f.getType()==Double.class || f.getType()==RoughTime.class) { final JCheckBoxMenuItem item=new JCheckBoxMenuItem(f.getName()); final Field field=f; filterMenu.add(item); item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { filterControlPanel.setFacet(field, item.getState()); leftPanel.setSelectedIndex(1); }}); } } } } }; void splash(boolean visible) { splash.setVisible(visible); } public void init() throws Exception { Dimension d=Toolkit.getDefaultToolkit().getScreenSize(); setBounds(0,0,Math.min(d.width, 1200), Math.min(d.height, 900)); setTitle(Display.version()); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); final QuitAction quitAction=new QuitAction(this, model); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { quitAction.quit(); } public void windowStateChanged(WindowEvent e) { repaint(); } }); Image icon = Toolkit.getDefaultToolkit().getImage("images/icon.gif"); setIconImage(icon); // read example directory String[] ex=getVisibleFiles("settings/examples"); int n=ex.length; examples=new String[n][2]; for (int i=0; i<n; i++) { String s=ex[i]; int dot=s.lastIndexOf('.'); if (dot>=0 && dot<s.length()-1); s=s.substring(0,dot); examples[i][0]=s; examples[i][1]="settings/examples/"+ex[i]; } templates=getVisibleFiles("settings/templates"); fileChooser=new JFileChooser(state.getCurrentFile()); getContentPane().setLayout(new BorderLayout()); // left tab area, with vertical gray divider. JPanel leftHolder=new JPanel(); getContentPane().add(leftHolder, BorderLayout.WEST); leftHolder.setLayout(new BorderLayout()); JPanel pad=new Pad(3,3); pad.setBackground(Color.gray); leftHolder.add(pad, BorderLayout.EAST); leftPanel=new LinkTabPane();//JTabbedPane(); leftHolder.add(leftPanel, BorderLayout.CENTER); JPanel configPanel=new JPanel(); configPanel.setLayout(new BorderLayout()); filterMenu=new JMenu("Filters"); filterControlPanel=new FilterControlPanel(model, filterMenu); final GlobalDisplayPanel displayPanel=new GlobalDisplayPanel(model, filterControlPanel); configPanel.add(displayPanel, BorderLayout.NORTH); JPanel legend=new JPanel(); legend.setLayout(new BorderLayout()); configPanel.add(legend, BorderLayout.CENTER); legend.add(new SizeLegendPanel(model), BorderLayout.NORTH); legend.add(new ColorLegendPanel(model), BorderLayout.CENTER); leftPanel.addTab(configPanel, "Display", true); leftPanel.addTab(filterControlPanel, "Filter", true); // center tab area final LinkTabPane center=new LinkTabPane(); getContentPane().add(center, BorderLayout.CENTER); center.addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { displayPanel.showLocalControl(center.getCurrentName()); } }); final IntroView intro=new IntroView(model); // we refer to this a bit later. final TimelineView timeline=new TimelineView(model); AbstractView[] views={ timeline, new CalendarView(model), new ListView(model), new TableView(model), new BarGraphView(model), intro, new DescriptionView(model), new SummaryView(model), }; for (int i=0; i<views.length; i++) { center.addTab(views[i], views[i].getName(), i<5); displayPanel.addLocalControl(views[i].getName(), views[i].getControls()); } // start off with intro screen center.setCurrentName(intro.getName()); displayPanel.showLocalControl(intro.getName()); // but then, once data is loaded, switch directly to the timeline view. model.addListener(new TFListener() { @Override public void note(TFEvent e) { if (e.type==e.type.DATABASE_CHANGE) { if (center.getCurrentName().equals(intro.getName())) { center.setCurrentName(timeline.getName()); displayPanel.showLocalControl(timeline.getName()); } } }}); JMenuBar menubar=new JMenuBar(); setJMenuBar(menubar); JMenu fileMenu=new JMenu("File"); menubar.add(fileMenu); fileMenu.add(new NewDataAction(this)); fileMenu.add(new CopySchemaAction(this)); JMenu templateMenu=new JMenu("New From Template"); fileMenu.add(templateMenu); for (int i=0; i<templates.length; i++) { JMenuItem t=new JMenuItem(templates[i]); final String fileName="settings/templates/"+templates[i]; templateMenu.add(t); t.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { load(fileName, FileExtensionCatalog.get(fileName), true); }}); } fileMenu.addSeparator(); JMenuItem open=new JMenuItem("Open..."); fileMenu.add(open); open.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { load(new TimeflowFormat(), false); }}); fileMenu.add(openRecent); makeRecentFileMenu(); fileMenu.addSeparator(); fileMenu.add(new ImportFromPasteAction(this)); JMenuItem impDel=new JMenuItem("Import CSV/TSV..."); fileMenu.add(impDel); impDel.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (checkSaveStatus()) importDelimited(); }}); fileMenu.addSeparator(); fileMenu.add(save); save.setAccelerator(KeyStroke.getKeyStroke('S', Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); save.setEnabled(false); save.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { save(model.getDbFile()); }}); model.addListener(new TFListener() { @Override public void note(TFEvent e) { save.setEnabled(!model.getReadOnly()); }}); JMenuItem saveAs=new JMenuItem("Save As..."); fileMenu.add(saveAs); saveAs.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { saveAs(); }}); fileMenu.addSeparator(); JMenuItem exportTSV=new JMenuItem("Export TSV..."); fileMenu.add(exportTSV); exportTSV.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { exportDelimited('\t'); }}); JMenuItem exportCSV=new JMenuItem("Export CSV..."); fileMenu.add(exportCSV); exportCSV.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { exportDelimited(','); }}); JMenuItem exportHTML=new JMenuItem("Export HTML..."); fileMenu.add(exportHTML); exportHTML.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { exportHtml(); }}); fileMenu.addSeparator(); fileMenu.add(quitAction); JMenu editMenu=new JMenu("Edit"); menubar.add(editMenu); editMenu.add(new AddRecordAction(this)); editMenu.addSeparator(); editMenu.add(new DateFieldAction(this)); editMenu.add(new AddFieldAction(this)); editMenu.add(new RenameFieldAction(this)); editMenu.add(new DeleteFieldAction(this)); editMenu.add(new ReorderFieldsAction(this)); editMenu.addSeparator(); editMenu.add(new EditSourceAction(this)); editMenu.addSeparator(); editMenu.add(new DeleteSelectedAction(this)); editMenu.add(new DeleteUnselectedAction(this)); menubar.add(filterMenu); model.addListener(filterMenuMaker); JMenu exampleMenu=new JMenu("Examples"); menubar.add(exampleMenu); for (int i=0; i<examples.length; i++) { JMenuItem example=new JMenuItem(examples[i][0]); exampleMenu.add(example); final String file=examples[i][1]; example.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { load(file, FileExtensionCatalog.get(file), true); }}); } JMenu helpMenu=new JMenu("Help"); menubar.add(helpMenu); helpMenu.add(new WebDocAction(this)); JMenuItem about=new JMenuItem("About TimeFlow"); helpMenu.add(about); about.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { splash(true); }}); model.addListener(new TFListener() { @Override public void note(TFEvent e) { if (e.type==TFEvent.Type.DATABASE_CHANGE) { String name=model.getDbFile(); int n=Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\')); if (n>0) name=name.substring(n+1); setTitle(name); } }}); } void makeRecentFileMenu() { openRecent.removeAll(); try { for (File f:state.getRecentFiles()) { final String file=f.getAbsolutePath(); JMenuItem m=new JMenuItem(f.getName()); openRecent.add(m); m.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { load(file, FileExtensionCatalog.get(file), false); }}); } } catch (Exception e) { e.printStackTrace(System.out); } } void exportHtml() { int retval = fileChooser.showSaveDialog(this); if (retval == fileChooser.APPROVE_OPTION) { String fileName=fileChooser.getSelectedFile().getAbsolutePath(); try { FileWriter fw=new FileWriter(fileName); BufferedWriter out=new BufferedWriter(fw); new HtmlFormat().export(model, out); out.close(); fw.close(); } catch (Exception e) { System.out.println(e); showUserError("Couldn't save file: "+e); } } } void exportDelimited(char delimiter) { int retval = fileChooser.showSaveDialog(this); if (retval == fileChooser.APPROVE_OPTION) { String fileName=fileChooser.getSelectedFile().getAbsolutePath(); try { new DelimitedFormat(delimiter).write(model.getDB(), new File(fileName)); } catch (Exception e) { System.out.println(e); showUserError("Couldn't save file: "+e); } } } void load(Import importer, boolean readOnly) { if (!checkSaveStatus()) return; try { int retval = fileChooser.showOpenDialog(this); if (retval == fileChooser.APPROVE_OPTION) { load(fileChooser.getSelectedFile().getAbsolutePath(), importer, readOnly); noteFileUse(fileChooser.getSelectedFile().getAbsolutePath()); } } catch (Exception e) { showUserError("Couldn't read file."); System.out.println(e); } } public void showImportEditor(String fileName, String[][] data) { final ImportDelimitedPanel editor=new ImportDelimitedPanel(model); editor.setFileName(fileName); editor.setData(data); editor.setBounds(0,0,1024,768); editor.setVisible(true); SwingUtilities.invokeLater(new Runnable() { public void run() { editor.scrollToTop(); } }); } void importDelimited() { if (!checkSaveStatus()) return; try { int result = fileChooser.showOpenDialog(this); if (result == JFileChooser.APPROVE_OPTION) { File file = fileChooser.getSelectedFile(); String fileName=file.getAbsolutePath(); noteFileUse(fileName); String[][] data=DelimitedFormat.readArrayGuessDelim(fileName, System.out); showImportEditor(fileName, data); } else { System.out.println("OK, canceling import."); } } catch (Exception e) { showUserError("Couldn't read file format."); e.printStackTrace(System.out); } } void load(final String fileName, final Import importer, boolean readOnly) { if (!checkSaveStatus()) return; try { final File f=new File(fileName); ActDB db=importer.importFile(f); model.setDB(db, fileName, readOnly, TimeflowApp.this); if (!readOnly) noteFileUse(fileName); } catch (Exception e) { e.printStackTrace(System.out); showUserError("Couldn't read file."); model.noteError(this); } } public boolean save(String fileName) { try { FileWriter fw=new FileWriter(fileName); BufferedWriter out=new BufferedWriter(fw); new TimeflowFormat().export(model, out); out.close(); fw.close(); noteFileUse(fileName); if (!fileName.equals(model.getDbFile())) model.setDbFile(fileName, false, this); model.setChangedSinceSave(false); model.setReadOnly(false); save.setEnabled(true); return true; } catch (Exception e) { e.printStackTrace(System.out); showUserError("Couldn't save file: "+e); return false; } } public boolean checkSaveStatus() { boolean needSave=model.isChangedSinceSave(); if (!needSave) return true; Object[] options=null; if (model.isReadOnly()) options= new Object[] {"Save As", "Discard Changes", "Cancel"}; else options= new Object[] {"Save", "Save As", "Discard Changes", "Cancel"}; int n = JOptionPane.showOptionDialog( this, "The current data set has unsaved changes that will be lost.\n"+ "Would you like to save them before continuing?", "Save Before Closing?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, model.isReadOnly() ? "Save As" : "Save"); Object result=options[n]; if ("Discard Changes".equals(result)) return true; if ("Cancel".equals(result)) return false; if ("Save".equals(result)) { return save(model.getDbFile()); } // we are now at "save as..." return saveAs(); } public boolean saveAs() { File current=fileChooser.getSelectedFile(); if (current!=null) fileChooser.setSelectedFile(new File(current.getAbsolutePath()+" (copy)")); int retval = fileChooser.showSaveDialog(this); if (retval == fileChooser.APPROVE_OPTION) { String fileName=fileChooser.getSelectedFile().getAbsolutePath(); model.setReadOnly(false); save.setEnabled(true); return save(fileName); } else return false; } public void showUserError(Object o) { JOptionPane.showMessageDialog(this, o, "A problem occurred", JOptionPane.ERROR_MESSAGE); if (o instanceof Exception) { ((Exception)o).printStackTrace(System.out); } } public void noteFileUse(String file) { state.setCurrentFile(new File(file)); state.save(); makeRecentFileMenu(); } public void clearFilters() { filterControlPanel.clearFilters(); } static String[] getVisibleFiles(String dir) { String[] s=new File(dir).list(); ArrayList<String> real=new ArrayList<String>(); for (int i=0; i<s.length; i++) if (!s[i].startsWith(".")) real.add(s[i]); return (String[])real.toArray(new String[0]); } }