2daedit/source/app.d

574 lines
14 KiB
D
Executable File

import std.stdio;
import std.file;
import std.string;
import std.conv : to;
import gtk.Main;
import gtk.MainWindow;
import gtk.Widget;
import gtk.TreeView;
import gtk.ListStore;
import gtk.TreeViewColumn;
import gtk.TreeIter;
import gtk.Label;
import gtk.Entry;
import gtk.CellRenderer;
import gtk.CellRendererText;
import gtk.VBox;
import gtk.Statusbar;
import gtk.Button;
import gtk.ScrolledWindow;
import gtk.AccelGroup;
import gtk.HBox;
__gshared MainWindow window;
__gshared HBox statusbar;
__gshared Object header;
void main(string[] args)
{
Main.init(args);
//Window
window = new MainWindow("2DA-Edit");
auto accel = new AccelGroup();
window.addAccelGroup(accel);
auto cont = new VBox(false, 0);
window.add(cont);
cont.setSizeRequest(300, 200);
version(Windows){
auto buttonSave = new Button(StockID.SAVE, true);
auto buttonSaveAs = new Button(StockID.SAVE_AS, true);
auto buttonOpen = new Button(StockID.OPEN, true);
auto buttonInsert = new Button(StockID.JUMP_TO, true);
auto buttonDelete = new Button(StockID.DELETE, true);
auto buttonRenumber = new Button(StockID.INDEX, true);
auto buttonNewCol = new Button(StockID.NEW, true);
}
else{
auto buttonSave = new Button("document-save-symbolic", GtkIconSize.MENU);
auto buttonSaveAs = new Button("document-save-as-symbolic", GtkIconSize.MENU);
auto buttonOpen = new Button("document-open-symbolic", GtkIconSize.MENU);
auto buttonInsert = new Button("format-text-direction-ltr-symbolic", GtkIconSize.SMALL_TOOLBAR);
auto buttonDelete = new Button("user-trash-symbolic", GtkIconSize.SMALL_TOOLBAR);
auto buttonRenumber = new Button("view-list-symbolic", GtkIconSize.SMALL_TOOLBAR);
auto buttonNewCol = new Button("tab-new-symbolic", GtkIconSize.SMALL_TOOLBAR);
}
buttonSave.setTooltipText("Save");
buttonSaveAs.setTooltipText("Save as");
buttonOpen.setTooltipText("Open 2DA");
buttonInsert.setTooltipText("Insert row after");
buttonDelete.setTooltipText("Delete row");
buttonRenumber.setTooltipText("Renumber all rows");
buttonNewCol.setTooltipText("Add new column");
enum GDK_KEY_S = 0x053;
buttonSave.addAccelerator("clicked", accel, GDK_KEY_S, GdkModifierType.CONTROL_MASK, GtkAccelFlags.VISIBLE);
buttonSaveAs.addAccelerator("clicked", accel, GDK_KEY_S, GdkModifierType.CONTROL_MASK|GdkModifierType.SHIFT_MASK, GtkAccelFlags.VISIBLE);
version(Windows){
//Menu bar
import gtk.HBox;
header = new HBox(false, 0);
cont.packStart(cast(HBox)header, false, false, 0);
(cast(HBox)header).packStart(buttonOpen, false, false, 0);
(cast(HBox)header).packEnd(buttonSaveAs, false, false, 0);
(cast(HBox)header).packEnd(buttonSave, false, false, 0);
}
else{
//Header bar
import gtk.HeaderBar;
header = new HeaderBar();
window.setTitlebar(cast(HeaderBar)header);
(cast(HeaderBar)header).setTitle("2DAEdit");
(cast(HeaderBar)header).setProperty("show-close-button", true);
(cast(HeaderBar)header).packStart(buttonOpen);
(cast(HeaderBar)header).packEnd(buttonSaveAs);
(cast(HeaderBar)header).packEnd(buttonSave);
}
//Status bar
statusbar = new HBox(false, 0);
cont.packEnd(statusbar, false, false, 5);
statusbar.packStart(buttonRenumber, false, false, 5);
statusbar.packStart(buttonInsert, false, false, 5);
statusbar.packStart(buttonDelete, false, false, 5);
statusbar.packEnd(buttonNewCol, false, false, 5);
//TreeView to display database
auto scroll = new ScrolledWindow(PolicyType.AUTOMATIC, PolicyType.AUTOMATIC);
cont.packEnd(scroll, true, true, 0);
auto tree = new TreeView();
scroll.add(tree);
tree.setHeadersVisible(true);
tree.setEnableSearch(true);
tree.setProperty("enable-grid-lines", GtkTreeViewGridLines.VERTICAL);
tree.setProperty("tooltip-column", 0);
tree.setProperty("reorderable", true);
tree.setProperty("headers-clickable", true);
tree.addOnColumnsChanged((TreeView tree){
auto store = cast(ListStore)tree.getModel();
if(store is null) return;
int si = GetColumnStoreIndex(tree, 0);
if(si>=0 && store.getColumnType(si) != GType.INT){
foreach(i ; 0..store.getNColumns()){
si = GetColumnStoreIndex(tree, i);
if(si>=0 && store.getColumnType(si)==GType.INT){
tree.moveColumnAfter(tree.getColumn(i), null);
break;
}
}
}
});
//Configure button callbacks
buttonSave.addOnClicked((Button){
Save(tree);
});
buttonSaveAs.addOnClicked((Button){
import gtk.Dialog;
import gtk.FileChooserDialog;
auto fc = new FileChooserDialog("Save 2DA as", window, FileChooserAction.SAVE);
auto res = fc.run();
if(res==GtkResponseType.OK){
string filename = fc.getFilename();
Save(tree, filename);
}
fc.destroy();
});
buttonOpen.addOnClicked((Button){
import gtk.Dialog;
import gtk.FileChooserDialog;
auto fc = new FileChooserDialog("Open 2DA", window, FileChooserAction.OPEN);
auto res = fc.run();
if(res==GtkResponseType.OK){
string filename = fc.getFilename();
Open(filename, tree);
}
fc.destroy();
});
buttonInsert.addOnClicked((Button){
TreeIter it = tree.getSelectedIter();
auto store = cast(ListStore)tree.getModel();
if(store !is null){
store.insertAfter(it, it);
store.setValue(it, 0, 0);
foreach(i ; 1..store.getNColumns()){
store.setValue(it, cast(int)i, "_");
}
}
});
buttonDelete.addOnClicked((Button){
TreeIter it = tree.getSelectedIter();
auto store = cast(ListStore)tree.getModel();
if(store !is null)
store.remove(it);
});
buttonRenumber.addOnClicked((Button){
auto store = cast(ListStore)tree.getModel();
if(store !is null){
TreeIter it = new TreeIter();
if(store.getIterFirst(it)){
int id = 0;
do{
store.setValue(it, 0, id++);
}while(store.iterNext(it));
}
}
});
buttonNewCol.addOnClicked((Button){
auto oldstore = cast(ListStore)tree.getModel();
if(oldstore is null) return;
int newColIndex = oldstore.getNColumns();
foreach(i ; 0..tree.getNColumns){
auto col = tree.getColumn(i);
writeln("col",i,"=",col, "(", col is null? "" : col.getTitle, ")");
}
GType[] types;
string[] titles;
int[] storeIndex;
foreach(i ; 0..tree.getNColumns){
writeln("Saving ",i);
if(i==0)types~= GType.INT;
else types~= GType.STRING;
storeIndex~= GetColumnStoreIndex(tree, 0)>=0?GetColumnStoreIndex(tree, 0):0;
auto col = tree.getColumn(0);
if(col !is null) titles~= tree.getColumn(0).getTitle;
else titles~= "ERR";
tree.removeColumn(tree.getColumn(0));
}
types~=GType.STRING;
titles~="new_col";
auto store = new ListStore(types);
tree.setModel(store);
//Fill them
TreeIter oldit = new TreeIter();
TreeIter newit = new TreeIter();
if(oldstore.getIterFirst(oldit)){
do{
store.append(newit);
foreach(i ; 0..newColIndex+1){
if(i<newColIndex){
if(types[i]==GType.INT) store.setValue(newit, i, oldstore.getValueInt(oldit, storeIndex[i]));
else store.setValue(newit, i, oldstore.getValueString(oldit, storeIndex[i]));
}
else
store.setValue(newit, cast(int)i, "_");
}
}while(oldstore.iterNext(oldit));
}
//setup cols
foreach(i ; 0..newColIndex+1){
tree.appendColumn(SetupColumn(tree, titles[i], i));
}
oldstore.destroy();
});
//Open if exists
if(args.length>=2 && exists(args[1])){
Open(args[1], tree);
}
window.showAll();
Main.run();
}
int GetColumnStoreIndex(TreeView tree, int colindex){
auto col = tree.getColumn(colindex);
if(col !is null)
return cast(int)(col.getData("colnumber"));
return -1;
}
void SaySomething(string msg){
import core.thread;
new Thread({
Thread.getThis.sleep(dur!"msecs"(100));
auto lbl = new Label("");
lbl.setMarkup("<i>"~msg~"</i>");
statusbar.packEnd(lbl, false, false, 5);
version(Windows){
lbl.show();
Thread.getThis.sleep(dur!"msecs"(1500));
}
else{
//Wow, much animation, very badass
lbl.setOpacity(0.0);
lbl.show();
foreach(i ; 0..20){
lbl.setOpacity(i/20.0);
Thread.getThis.sleep(dur!"msecs"(10));
}
Thread.getThis.sleep(dur!"msecs"(1500));
foreach(i ; 1..20){
lbl.setOpacity(1.0-i/20.0);
Thread.getThis.sleep(dur!"msecs"(10));
}
}
//Destroy
lbl.destroy();
}).start();
}
void Save(ref TreeView tree, string newpath=""){
auto store = cast(ListStore)tree.getModel();
if(store !is null){
if(newpath!=""){
openedFile = newpath;
SetTitle(openedFile);
}
//Detect column sizes
ulong colSize[];
colSize.length = tree.getNColumns;
foreach(i ; 0..tree.getNColumns)
colSize[i] = tree.getColumn(i).getTitle.length +1;//+1 space
TreeIter it = new TreeIter();
if(store.getIterFirst(it)){
do{
import std.math : log10;
int size0 = log10(store.getValueInt(it, 0)+1).to!int +1;//+1 space
if(size0>colSize[0])
colSize[0] = size0;
foreach(i ; 1..store.getNColumns()){
int size = store.getValueString(it, GetColumnStoreIndex(tree, i)).length.to!int +3;//+2 double quotes added, +1 space
if(size>colSize[i])
colSize[i] = size;
}
}while(store.iterNext(it));
}
//Write file
auto file = File(openedFile, "w");
foreach(i ; 0..tree.getNColumns)
file.write(leftJustify(tree.getColumn(i).getTitle, colSize[i]));
file.write("\n");
it = new TreeIter();
if(store.getIterFirst(it)){
do{
file.write(leftJustify(store.getValueInt(it, 0).to!string, colSize[0]));
foreach(i ; 1..store.getNColumns()){
file.write(leftJustify("\""~store.getValueString(it, GetColumnStoreIndex(tree, i))~"\"", colSize[i]));
}
file.write("\n");
}while(store.iterNext(it));
}
file.flush();
file.close();
SaySomething("Saved to "~openedFile);
}
else
SaySomething("Nothing to save !");
}
void SetTitle(string title){
version(Windows) window.setTitle(title);
else{
import gtk.HeaderBar;
(cast(HeaderBar)header).setSubtitle(title);
}
}
__gshared string openedFile;
void Open(string file, ref TreeView tree){
auto twoda = new TwoDA(file);
openedFile = file;
SetTitle(openedFile);
//Delete old store
auto oldstore = cast(ListStore)tree.getModel();
if(oldstore !is null)
oldstore.destroy();
//Remove columns from TreeView
foreach(i ; 0..tree.getNColumns)
tree.removeColumn(tree.getColumn(0));
//Set store types
GType type[];
type~=GType.INT;
foreach(i;1..twoda.header.length)type~=GType.STRING;
//Create new store
auto store = new ListStore(type);
tree.setModel(store);
//Setup TreeView columns
foreach(index, s ; twoda.header){
auto col = SetupColumn(tree, s, index);
tree.appendColumn(col);
}
//Fill database
TreeIter iter = new TreeIter();
for(int i=0 ; i<=twoda.lastLine ; i++){
store.append(iter);
store.setValue(iter, 0, i);
if(i in twoda.values){
foreach(index, v ; twoda.values[i]){
store.setValue(iter, cast(int)index+1, v);
}
}
else{
foreach(index ; 1..twoda.header.length){
store.setValue(iter, cast(int)index+1, "_");
}
}
}
//Autosize columns
tree.columnsAutosize();
tree.setSizeRequest(50, 50);
}
//Note: tree is only used on events (click)
auto ref SetupColumn(TreeView tree, string sName, size_t index){
auto store = cast(ListStore)tree.getModel();
CellRendererText cr = new CellRendererText();
cr.setProperty("editable", true);
if(index==0){
cr.setProperty("background-rgba", cast(ulong)(new GdkRGBA(0.36, 0.13, 0.4, 0.5)));
cr.setProperty("background-set", true);
cr.addOnEdited((string path, string newval, CellRendererText crt){
try{
int n = newval.to!int;
TreeIter t = new TreeIter(tree.getModel(), path);
store.setValue(t, cast(int)crt.getData("colnumber"), n);
}
catch(Exception e){
SaySomething("Not a number !");
}
});
}
else{
cr.addOnEdited((string path, string newval, CellRendererText crt){
TreeIter t = new TreeIter(tree.getModel(), path);
if(newval.countchars("\"")!=0){
SaySomething("Double quotes are forbidden !");
}
else
store.setValue(t, cast(int)crt.getData("colnumber"), newval);
});
}
cr.setData("colnumber", cast(void*)cast(int)index);
auto col = new TreeViewColumn(sName, cr, "text", cast(int)index);
col.setData("colnumber", cast(void*)cast(int)index);
col.setResizable(true);
col.setMinWidth(10);
col.setClickable(true);
col.setReorderable(true);
col.addOnClicked((TreeViewColumn col){
import gtk.Dialog;
auto dlg = new Dialog("Column options", window, GtkDialogFlags.MODAL, ["Close"], [ResponseType.CANCEL]);
//Rename
dlg.getContentArea.packStart(new Label("Rename:"), false, false, 0);
auto renamebox = new HBox(false, 0);
dlg.getContentArea.packStart(renamebox, false, false, 0);
auto renameentry = new Entry(col.getTitle);
version(Windows) auto renamebutton = new Button(StockID.APPLY, true);
else auto renamebutton = new Button("object-select-symbolic", GtkIconSize.SMALL_TOOLBAR);
renamebutton.addOnClicked((Button){
auto newname = renameentry.getText.strip;
if(newname.countchars(" \t\n\r")==0)
col.setTitle(newname);
else
SaySomething("Spaces are forbidden in column name");
});
renamebox.packStart(renameentry, true, true, 0);
renamebox.packEnd(renamebutton, false, false, 0);
//Delete
auto deletebox = new HBox(false, 0);
dlg.getContentArea.packStart(deletebox, false, false, 0);
version(Windows) auto deletebutton = new Button(StockID.DELETE, true);
else auto deletebutton = new Button("user-trash-symbolic", GtkIconSize.SMALL_TOOLBAR);
deletebutton.addOnClicked((Button){
tree.removeColumn(col);
dlg.destroy();
});
deletebox.packStart(new Label("Delete column"), true, true, 0);
deletebox.packEnd(deletebutton, false, false, 0);
dlg.showAll();
dlg.run();
dlg.destroy();
});
return col;
}
class TwoDA{
import std.regex;
import std.file;
this(string filepath){
lastLine = 0;
foreach(index, line ; readText(filepath).splitLines()){
string data[];
auto results = matchAll(line, rgxField);
foreach(res ; results){
string s;
if(res[0][0]=='"') data~= res[2];
else data~= res[1];
}
if(index==0){
header = data;
//writeln(header);
}
else{
int nLine = data[0].to!int;
values[nLine] = data[1..$];
//writeln(values[nLine]);
if(nLine > lastLine)
lastLine = nLine;
}
}
}
string[] header;
string[][uint] values;
uint lastLine;
enum rgxField = ctRegex!"(?:\\b([^\\s]+?)\\b|\"([^\"]+?)\")";
}