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]);
}
}