module window; import std.path; import std.stdio; import std.string; import std.conv : to; import std.array : replace; import gtk.MainWindow; import gtk.HeaderBar; 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; import gdk.Keysyms; import twoda; class Window : MainWindow{ this(in string file=null){ super("2DA-Edit"); getSettings.setLongProperty("gtk-application-prefer-dark-theme", 0, ""); auto accel = new AccelGroup(); addAccelGroup(accel); auto cont = new VBox(false, 0); add(cont); cont.setSizeRequest(300, 200); 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("list-add-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); auto buttonLocale = new Button("accessories-dictionary-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"); buttonLocale.setTooltipText("Set locale table suffix"); buttonOpen.addAccelerator("clicked", accel, GdkKeysyms.GDK_O, GdkModifierType.CONTROL_MASK, GtkAccelFlags.VISIBLE); buttonSave.addAccelerator("clicked", accel, GdkKeysyms.GDK_S, GdkModifierType.CONTROL_MASK, GtkAccelFlags.VISIBLE); buttonSaveAs.addAccelerator("clicked", accel, GdkKeysyms.GDK_S, GdkModifierType.CONTROL_MASK|GdkModifierType.SHIFT_MASK, GtkAccelFlags.VISIBLE); //Header bar import gtk.HeaderBar; header = new HeaderBar(); setTitlebar(header); header.setTitle("2DAEdit"); header.setProperty("show-close-button", true); header.packStart(buttonOpen); header.packStart(buttonLocale); header.packEnd(buttonSaveAs); 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.FileChooserDialog; auto fc = new FileChooserDialog("Save 2DA as", this, FileChooserAction.SAVE); fc.setDoOverwriteConfirmation(true); fc.setCreateFolders(true); fc.setCurrentFolder(openedFile!=""? dirName(openedFile) : getcwd()); auto res = fc.run(); if(res==GtkResponseType.OK){ string filename = fc.getFilename(); save(tree, filename); } fc.destroy(); }); buttonOpen.addOnClicked((Button){ import gtk.FileChooserDialog; auto fc = new FileChooserDialog("Open 2DA", this, FileChooserAction.OPEN); fc.setSelectMultiple(false); fc.setCurrentFolder(openedFile!=""? dirName(openedFile) : getcwd()); auto res = fc.run(); string filename = fc.getFilename(); fc.destroy(); if(res==GtkResponseType.OK) open(filename, tree); }); 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.replace("__", "_"), ")"); } 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.replace("__", "_"); 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"~msg~""); statusbar.packEnd(lbl, false, false, 5); //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; header.setSubtitle(openedFile); } //Detect column sizes size_t colSize[]; colSize.length = tree.getNColumns; foreach(i ; 0..tree.getNColumns) colSize[i] = tree.getColumn(i).getTitle.replace("__", "_").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.replace("__", "_"), 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(); displayMessage("Saved to "~openedFile); } else displayMessage("Nothing to save !"); } void open(string file, ref TreeView tree){ writeln("Opening ",file); TwoDA twoda; try{ twoda = new TwoDA(file); } catch(Exception e){ import gtk.MessageDialog; auto md = new MessageDialog(this, GtkDialogFlags.MODAL, GtkMessageType.ERROR, GtkButtonsType.CLOSE, "Could not open 2da:\n"~e.msg, null); md.run(); md.destroy(); return; } //Open in new window if(openedFile!=null){ new Window(file); return; } //Open in same window openedFile = file; header.setSubtitle(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); } private: HBox statusbar; HeaderBar header; string openedFile; int getColumnStoreIndex(TreeView tree, int colindex){ auto col = tree.getColumn(colindex); if(col !is null) return cast(int)(col.getData("colnumber")); return -1; } 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.5, 0.7, 1, 1))); 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){ displayMessage("Not a number !"); } }); } else{ cr.addOnEdited((string path, string newval, CellRendererText crt){ TreeIter t = new TreeIter(tree.getModel(), path); if(newval.countchars("\"")!=0){ displayMessage("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.replace("_", "__"), 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", this, 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.replace("__", "_")); 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.replace("_", "__")); else displayMessage("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); 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; } }