#priority init -200 import tabs.Tab; import tabs.tabbar; import editor.loader; import editor.defs; import editor.fields; import editor.files; import dialogs.InputDialog; import dialogs.QuestionDialog; import editor.hooks; import Tab@ createResearchEditor(const string& fname, const string& ident) from "tabs.ResearchTab"; class FieldInfo { LineDesc@ line; FieldDef@ def; Field@ elem; bool hasOriginal = false; string originalValue; }; Field@ makeField(FieldDef@ def, IGuiElement@ parent, const recti& pos) { auto@ fld = makeField(def.type, parent, pos); if(def.options !is null) { for(uint i = 0, cnt = def.options.length; i < cnt; ++i) fld.addOption(def.options[i]); } return fld; } class BlockEditor : BaseGuiElement, QuestionDialogCallback { FileDesc@ file; BlockDef@ blockType; LineDesc@ headLine; string fname; array fields; array hooks; array blocks; array extraLines; GuiButton@ addHookButton; array addBlockButtons; array addFieldButtons; array addFields; bool expanded = true; bool changed = false; GuiButton@ label; GuiTextbox@ text; GuiButton@ removeButton; GuiButton@ editButton; BlockEditor(IGuiElement@ elem) { super(elem, recti()); @label = GuiButton(this, Alignment(Left+12, Top, Left+400, Top+44)); label.font = FT_Medium; label.horizAlign = 0.0; label.style = SS_IconButton; label.buttonIcon = icons::Minus; @text = GuiTextbox(this, Alignment(Left+400, Top, Right-40, Top+44)); text.font = FT_Medium; text.style = SS_HoverTextbox; text.emptyText = "Enter identifier name for element..."; text.tabIndex = -1; @removeButton = GuiButton(this, Alignment(Right-40, Top+4, Right-4, Top+40)); removeButton.color = colors::Red; setMarkupTooltip(removeButton, "Remove this block and its associated element from the file."); removeButton.setIcon(icons::Remove); StrictBounds = true; } void parse(const string& filename, FileDef@ def) { @file = FileDesc(filename); fname = filename; parseBlock(file, file.lines, -1, def.defaultBlock); } void fillTargets(Field@ sourceField, TargetChooser@ fld, bool selected = false) { if(!selected) { for(uint i = 0, cnt = blocks.length; i < cnt; ++i) { if(blocks[i].hasField(sourceField)) { blocks[i].fillTargets(sourceField, fld, true); return; } } return; } for(uint i = 0, cnt = fields.length; i < cnt; ++i) { auto@ tf = cast(fields[i].elem); if(tf !is null) { auto type = TargetType(tf.type.selected); if(fld.targType == TT_Any || type == fld.targType) fld.addOption(tf.box.text, tf.box.text+" ("+TARGET_TYPE_NAMES[type]+")"); } } } bool hasField(Field@ fld, bool recurse = true) { for(uint i = 0, cnt = fields.length; i < cnt; ++i) { if(fields[i].elem is fld) return true; } for(uint i = 0, cnt = hooks.length; i < cnt; ++i) { if(hooks[i].elem is fld) return true; } if(recurse) { for(uint i = 0, cnt = blocks.length; i < cnt; ++i) { if(blocks[i].hasField(fld)) return true; } } return false; } void clear() { for(uint i = 0, cnt = fields.length; i < cnt; ++i) fields[i].elem.remove(); fields.length = 0; for(uint i = 0, cnt = blocks.length; i < cnt; ++i) blocks[i].remove(); blocks.length = 0; for(uint i = 0, cnt = addBlockButtons.length; i < cnt; ++i) addBlockButtons[i].remove(); addBlockButtons.length = 0; for(uint i = 0, cnt = addFieldButtons.length; i < cnt; ++i) addFieldButtons[i].remove(); addFieldButtons.length = 0; addFields.length = 0; extraLines.length = 0; } void insertLine(LineDesc@ line, int priority = 0) { //Find the right place to insert this line int pos = -1; if(pos == -1 && headLine !is null) pos = file.lines.find(headLine)+1; for(uint i = 0, cnt = fields.length; i < cnt; ++i) { if(fields[i].line !is null && fields[i].def.fieldPriority <= priority) pos = max(pos, file.lines.find(fields[i].line)+1); } if(!line.isKey) { for(uint i = 0, cnt = extraLines.length; i < cnt; ++i) pos = max(pos, file.lines.find(extraLines[i])+1); } if(pos < 0 || pos > int(file.lines.length)) file.lines.insertLast(line); else file.lines.insertAt(pos, line); } void checkDuplicates() { if(blockType is null || !blockType.hasIdentifier) return; bool dup = false; if(blockType.duplicateCheck !is null) { uint count = 0; string ident = text.text; for(uint i = 0, cnt = blockType.duplicateCheck.length; i < cnt; ++i) { if(blockType.duplicateCheck[i].ident == ident) count += 1; } if(count > 1) dup = true; } if(dup) { text.bgColor = colors::Red; text.textColor = colors::Red; setMarkupTooltip(text, "Duplicate identifier, '"+text.text+"' is already in use."); } else { text.bgColor = colors::White; text.textColor = colors::White; setMarkupTooltip(text, ""); } } void setExpanded(bool value) { if(value == expanded) return; expanded = value; for(uint i = 0, cnt = fields.length; i < cnt; ++i) fields[i].elem.visible = expanded; for(uint i = 0, cnt = blocks.length; i < cnt; ++i) blocks[i].visible = expanded; for(uint i = 0, cnt = hooks.length; i < cnt; ++i) hooks[i].elem.visible = expanded; updateAbsolutePosition(); label.buttonIcon = (expanded ? icons::Minus : icons::Plus); if(addHookButton !is null) addHookButton.visible = expanded; if(editButton !is null) editButton.visible = expanded; for(uint i = 0, cnt = addBlockButtons.length; i < cnt; ++i) addBlockButtons[i].visible = expanded; for(uint i = 0, cnt = addFieldButtons.length; i < cnt; ++i) addFieldButtons[i].visible = expanded; emitChanged(); } bool onGuiEvent(const GuiEvent& evt) { if(evt.type == GUI_Clicked) { if(evt.caller is removeButton) { question("Are you sure you want to delete this entire block of fields? This cannot be undone.", this); return true; } else if(evt.caller is addHookButton) { addHook(); setExpanded(true); return true; } else if(evt.caller is editButton) { if(blockType.editMode == "research_grid") { auto@ tab = createResearchEditor(getBasename(fname), text.text); newTab(tab); switchToTab(tab); } return true; } else if(evt.caller is label) { setExpanded(!expanded); return true; } else if(cast(evt.caller.parent) !is null) { auto@ hf = cast(evt.caller.parent); if(evt.caller is hf.resetButton) { removeHook(hf); return true; } } else { for(uint i = 0, cnt = addBlockButtons.length; i < cnt; ++i) { if(evt.caller is addBlockButtons[i]) { addBlock(blockType.blocks[i]); setExpanded(true); return true; } } for(uint i = 0, cnt = addFieldButtons.length; i < cnt; ++i) { if(evt.caller is addFieldButtons[i]) { addField(addFields[i]); setExpanded(true); return true; } } } } if(evt.caller is text) { if(evt.type == GUI_Changed) { changed = true; checkDuplicates(); return true; } if(evt.type == GUI_Focus_Lost) { if(changed) { emitConfirmed(); checkDuplicates(); changed = false; return true; } } } return BaseGuiElement::onGuiEvent(evt); } void addHook() { LineDesc line; line.line = ""; line.isKey = false; insertLine(line); HookField fld(this, recti()); fld.hookType = blockType.hookType; fld.hookModule = blockType.hookModule; fld.visible = expanded; FieldInfo inf; @inf.line = line; @inf.elem = fld; hooks.insertLast(inf); emitConfirmed(); openHookList(fld); } void removeHook(HookField@ fld) { for(uint i = 0, cnt = hooks.length; i < cnt; ++i) { auto@ hook = hooks[i]; if(fld is hook.elem) { if(hook.line !is null) file.lines.remove(hook.line); hook.elem.remove(); hooks.removeAt(i); emitConfirmed(); return; } } } void addField(FieldDef@ fDef) { FieldInfo inf; @inf.line = null; @inf.elem = makeField(fDef, this, recti()); @inf.def = fDef; inf.elem.set(fDef.key, fDef.doc, fDef.defaultValue, fDef.icon); inf.elem.value = fDef.defaultValue; inf.elem.tabIndex = fields.length * 10; inf.elem.visible = expanded; fields.insertLast(inf); emitConfirmed(); inf.elem.focus(); } void addBlock(BlockDef@ type) { BlockEditor@ blk = BlockEditor(cast(this)); blk.setType(type); @blk.file = file; @blk.headLine = LineDesc(); if(headLine !is null) blk.headLine.indent = headLine.indent+1; blk.headLine.key = type.key; blk.headLine.isKey = true; blk.visible = expanded; blk.fname = fname; blk.setExpanded(true); insertLine(blk.headLine); for(uint i = 0, cnt = type.fields.length; i < cnt; ++i) { auto@ fDef = type.fields[i]; if(fDef.repeatable) continue; FieldInfo inf; @inf.line = null; @inf.elem = makeField(fDef, blk, recti()); @inf.def = fDef; inf.elem.set(fDef.key, fDef.doc, fDef.defaultValue, fDef.icon); inf.elem.value = fDef.defaultValue; inf.elem.tabIndex = blk.fields.length * 10; inf.elem.visible = blk.expanded; blk.fields.insertLast(inf); } blocks.insertLast(blk); emitConfirmed(); blk.text.focus(); } void removeBlock() { auto@ upper = cast(parent); if(upper !is null) upper.removeBlock(this); for(uint i = 0, cnt = fields.length; i < cnt; ++i) { if(fields[i].line !is null) file.lines.remove(fields[i].line); } for(uint i = 0, cnt = hooks.length; i < cnt; ++i) { if(hooks[i].line !is null) file.lines.remove(hooks[i].line); } for(uint i = 0, cnt = extraLines.length; i < cnt; ++i) { file.lines.remove(extraLines[i]); } for(uint i = 0, cnt = blocks.length; i < cnt; ++i) { blocks[i].removeBlock(); --i; --cnt; } if(headLine !is null) file.lines.remove(headLine); remove(); emitConfirmed(); } void removeBlock(BlockEditor@ blk) { blocks.remove(blk); } void questionCallback(QuestionDialog@ dialog, int answer) { if(answer == QA_Yes) { auto@ p = cast(parent); removeBlock(); p.emitConfirmed(); } } void commit() { int indent = 0; if(headLine !is null) { indent = headLine.indent; headLine.value = text.text; } for(uint i = 0, cnt = fields.length; i < cnt; ++i) { auto@ inf = fields[i]; string value = inf.elem.value; if(value != inf.def.defaultValue || (inf.hasOriginal && value == inf.originalValue)) { bool newLine = false; if(inf.line is null) { //Create a new line for the field @inf.line = LineDesc(); newLine = true; } inf.line.indent = indent+1; inf.line.isEmpty = false; if(inf.def.fullLine) { inf.line.isKey = false; inf.line.line = value; } else { inf.line.isKey = true; inf.line.key = inf.def.key; inf.line.value = value; } if(newLine) insertLine(inf.line, inf.def.fieldPriority); } else { if(inf.line !is null) { file.lines.remove(inf.line); @inf.line = null; } } } for(uint i = 0, cnt = blocks.length; i < cnt; ++i) blocks[i].commit(); for(uint i = 0, cnt = hooks.length; i < cnt; ++i) { auto@ inf = hooks[i]; if(inf.line !is null) { inf.line.isKey = false; inf.line.isEmpty = false; if(blockType !is null && blockType.hookPrefix.length != 0) inf.line.line = blockType.hookPrefix+inf.elem.value; else inf.line.line = inf.elem.value; inf.line.indent = indent+1; } } } void setType(BlockDef@ def) { @blockType = def; label.visible = blockType.key.length != 0; removeButton.visible = label.visible; text.visible = label.visible && blockType.hasIdentifier; label.text = blockType.key+":"; setMarkupTooltip(label, blockType.doc); if(blockType.identifierLimit) text.setIdentifierLimit(); else text.characterLimit.clear(); if(blockType.hookType.length != 0) { @addHookButton = GuiButton(this, recti(), "Add Hook"); addHookButton.visible = expanded; } if(blockType.editMode.length != 0) { @editButton = GuiButton(this, recti(), "Edit"); editButton.color = Color(0x80ff80ff); editButton.font = FT_Bold; editButton.visible = expanded; } for(uint i = 0, cnt = blockType.blocks.length; i < cnt; ++i) { GuiButton btn(this, recti()); btn.text = "Add "+blockType.blocks[i].key; btn.visible = expanded; setMarkupTooltip(btn, blockType.blocks[i].doc); addBlockButtons.insertLast(btn); } for(uint i = 0, cnt = blockType.fields.length; i < cnt; ++i) { auto@ fDef = blockType.fields[i]; if(!fDef.repeatable) continue; GuiButton btn(this, recti()); btn.text = "Add "+fDef.key; btn.visible = expanded; setMarkupTooltip(btn, fDef.doc); addFieldButtons.insertLast(btn); addFields.insertLast(fDef); } } uint parseBlock(FileDesc@ file, array& lines, int pos, BlockDef@ def) { clear(); @this.file = file; setType(def); int indent = -1; if(pos >= 0) { @headLine = lines[pos]; indent = headLine.indent; text.text = headLine.value; } else { @headLine = null; text.text = ""; } array haveFields(def.fields.length, false); pos += 1; for(int cnt = int(lines.length); pos < cnt; ++pos) { auto@ line = lines[pos]; if(line.isEmpty) { extraLines.insertLast(line); continue; } if(int(line.indent) <= indent) { pos -= 1; break; } //Check if the line is associated with a field auto@ fDef = def.getField(line.key); if(fDef is null) @fDef = def.getField(line); if(fDef !is null) { FieldInfo inf; @inf.line = line; @inf.elem = makeField(fDef, this, recti()); @inf.def = fDef; inf.elem.set(fDef.key, fDef.doc, fDef.defaultValue, fDef.icon); inf.elem.value = fDef.fullLine ? line.line : line.value; inf.elem.tabIndex = fields.length * 10; inf.elem.visible = expanded; inf.hasOriginal = true; inf.originalValue = inf.elem.value; fields.insertLast(inf); haveFields[fDef.index] = true; continue; } //Check if the line is associated with a block auto@ blockDef = def.getBlock(line.key); if(blockDef !is null) { BlockEditor@ blk = BlockEditor(cast(this)); //What the shit angelscript? pos = blk.parseBlock(file, lines, pos, blockDef); blk.visible = expanded; blk.fname = fname; blk.setExpanded(!blockDef.closeDefault); blocks.insertLast(blk); continue; } //Find hooks if(def.hookType.length != 0) { string str = line.line; if(line.isKey) str = line.key+":"+line.value; if(def.hookPrefix.length != 0 && str.trimmed().startswith_nocase(def.hookPrefix.trimmed())) str = str.trimmed().substr(def.hookPrefix.trimmed().length).trimmed(); if(str.findFirst("(") != -1) { line.line = str; line.isKey = false; HookField fld(this, recti()); fld.hookType = def.hookType; fld.hookModule = def.hookModule; fld.value = str; fld.visible = expanded; FieldInfo inf; @inf.line = line; @inf.elem = fld; hooks.insertLast(inf); continue; } } //Remember the line as spurious for this block extraLines.insertLast(line); } for(uint i = 0, cnt = haveFields.length; i < cnt; ++i) { if(haveFields[i]) continue; auto@ fDef = def.fields[i]; if(fDef.repeatable) continue; FieldInfo inf; @inf.line = null; @inf.elem = makeField(fDef, this, recti()); @inf.def = fDef; inf.elem.set(fDef.key, fDef.doc, fDef.defaultValue, fDef.icon); inf.elem.value = fDef.defaultValue; inf.elem.tabIndex = fields.length * 10; inf.elem.visible = expanded; fields.insertLast(inf); } checkDuplicates(); return pos; } uint get_needHeight() { int h = text !is null && label.visible ? 44 : 0; if(expanded) { for(uint i = 0, cnt = fields.length; i < cnt; ++i) h += fields[i].elem.needHeight; for(uint i = 0, cnt = blocks.length; i < cnt; ++i) h += blocks[i].needHeight; for(uint i = 0, cnt = hooks.length; i < cnt; ++i) h += hooks[i].elem.needHeight; if(btnCount != 0) h += 44 * (btnCount/6+1); } return h; } uint get_btnCount() { uint cnt = 0; if(addHookButton !is null) cnt += 1; if(editButton !is null) cnt += 1; cnt += addBlockButtons.length; cnt += addFieldButtons.length; return cnt; } void updateAbsolutePosition() { int blockOff = 0; if(cast(parent) is null) { size = vec2i(parent.size.width-20, needHeight); blockOff = -30; } int y = text !is null && label.visible ? 44 : 0; if(expanded) { for(uint i = 0, cnt = fields.length; i < cnt; ++i) { auto@ fld = fields[i].elem; int h = fld.needHeight; fld.size = vec2i(size.width, h); fld.position = vec2i(0, y); y += h; } for(uint i = 0, cnt = blocks.length; i < cnt; ++i) { auto@ blk = blocks[i]; int h = blk.needHeight; blk.size = vec2i(size.width-blockOff-30, h); blk.position = vec2i(blockOff+30, y); y += h; } for(uint i = 0, cnt = hooks.length; i < cnt; ++i) { auto@ fld = hooks[i].elem; int h = fld.needHeight; fld.size = vec2i(size.width, h); fld.position = vec2i(0, y); y += h; } if(btnCount != 0) { int btnWidth = min(220, size.width/min(btnCount,6)); int btnOff = (size.width-(min(btnCount,6) * btnWidth)) / 2; int btnOrig = btnOff; uint n = 0; if(editButton !is null) { editButton.size = vec2i(btnWidth, 36); editButton.position = vec2i(btnOff, y+4); n += 1; y += 44; btnOff = btnOrig; } if(addHookButton !is null) { addHookButton.size = vec2i(btnWidth, 36); addHookButton.position = vec2i(btnOff, y+4); btnOff += btnWidth + 8; n += 1; if(n % 6 == 0) { btnOff = btnOrig; y += 44; } } for(uint i = 0, cnt = addFieldButtons.length; i < cnt; ++i) { addFieldButtons[i].size = vec2i(btnWidth, 36); addFieldButtons[i].position = vec2i(btnOff, y+4); btnOff += btnWidth + 8; n += 1; if(n % 6 == 0) { btnOff = btnOrig; y += 44; } } for(uint i = 0, cnt = addBlockButtons.length; i < cnt; ++i) { addBlockButtons[i].size = vec2i(btnWidth, 36); addBlockButtons[i].position = vec2i(btnOff, y+4); btnOff += btnWidth + 8; n += 1; if(n % 6 == 0) { btnOff = btnOrig; y += 44; } } } } if(Parent !is null) { if(Alignment !is null) { Position = Alignment.resolve(Parent.updatePosition.size); ClipRect = recti(vec2i(), Position.size); } AbsolutePosition = Position + Parent.updatePosition.topLeft; AbsoluteClipRect = (ClipRect + AbsolutePosition.topLeft).clipAgainst(Parent.absoluteClipRect); } else { if(Alignment !is null) { Position = Alignment.resolve(screenSize); ClipRect = recti(vec2i(), Position.size); } AbsolutePosition = Position; AbsoluteClipRect = ClipRect + Position.topLeft; } if(noClip) AbsoluteClipRect = AbsolutePosition; if(expanded) { uint cCnt = Children.length(); for(uint i = 0; i != cCnt; ++i) Children[i].updateAbsolutePosition(); } else if(removeButton !is null) { label.updateAbsolutePosition(); text.updateAbsolutePosition(); removeButton.updateAbsolutePosition(); } } void draw() { if(text !is null && label.visible) skin.draw(SS_HorizBar, SF_Normal, recti_area(AbsolutePosition.topLeft, vec2i(size.width, 44))); if(btnCount != 0 && expanded) { int bH = (btnCount/6+1)*44; skin.draw(SS_PlainBox, SF_Normal, recti_area(vec2i(AbsolutePosition.topLeft.x, AbsolutePosition.botRight.y-bH), vec2i(size.width, bH))); } BaseGuiElement::draw(); } }; class EditorTab : Tab, IInputDialogCallback, QuestionDialogCallback { GuiPanel@ editPanel; BlockEditor@ editor; FileDesc baseFile; GuiPanel@ filePanel; FileFolder@ files; GuiButton@ newFile; GuiButton@ deleteFile; GuiButton@ duplicateFile; string folder; string loadedFile; FileDef@ type; bool duplicate = false; EditorTab(const string& folder, FileDef@ type) { this.folder = folder; @this.type = type; super(); title = folder; initCompletions(); int y = 12; if(type.defaultBlock.doc.length != 0) { GuiMarkupText txt(this, recti_area(312,12,900,100)); txt.text = type.defaultBlock.doc; txt.updateAbsolutePosition(); y += txt.size.height+8; } @editPanel = GuiPanel(this, Alignment(Left+300, Top+y, Right, Bottom)); @filePanel = GuiPanel(this, Alignment(Left, Top, Left+300, Bottom-36)); @files = FileFolder(filePanel, recti()); files.load(folder, extension=".txt"); @newFile = GuiButton(this, Alignment(Left, Bottom-36, Left+100, Bottom), "New"); newFile.setIcon(icons::Create); newFile.color = colors::Green; @duplicateFile = GuiButton(this, Alignment(Left+100, Bottom-36, Left+200, Bottom), "Duplicate"); duplicateFile.setIcon(icons::Export); duplicateFile.disabled = true; duplicateFile.color = Color(0x0080ffff); @deleteFile = GuiButton(this, Alignment(Left+200, Bottom-36, Left+300, Bottom), "Delete"); deleteFile.setIcon(icons::Delete); deleteFile.color = colors::Red; deleteFile.disabled = true; updateAbsolutePosition(); } Color get_activeColor() { return Color(0xd482ffff); } Color get_inactiveColor() { return Color(0xa800ffff); } Color get_seperatorColor() { return Color(0x75488dff); } Sprite get_icon() { return icons::Paint; } void ensureFile(const string& path) { string dir = path_up(path); array folders; while(!fileExists(dir) && dir.length != 0) { folders.insertLast(dir); dir = path_up(dir); } for(int i = int(folders.length-1); i >= 0; --i) makeDirectory(folders[i]); } void checkEmptyDirectory(const string& path) { string dir = path; while(path_inside(topMod.abspath, dir) && topMod.abspath != dir) { if(isDirectory(dir)) { FileList list(dir, "*", false, false); if(list.length != 0) break; ::deleteFile(dir); } dir = path_up(dir); } } void inputCallback(InputDialog@ dialog, bool accepted) { if(accepted) { loadedFile = path_join(folder, dialog.getTextInput(0)); if(!loadedFile.endswith(".txt")) loadedFile += ".txt"; title = getBasename(loadedFile); { string path = path_join(topMod.abspath, loadedFile); ensureFile(path); if(duplicate) { for(uint i = 0, cnt = editor.blocks.length; i < cnt; ++i) { if(editor.blocks[i].text.text.length != 0) editor.blocks[i].text.text = "DUPLICATE_"+editor.blocks[i].text.text; } editor.commit(); editor.file.save(path); } else { WriteFile file(path); } } files.load(folder, extension=".txt"); files.select(loadedFile); } } void changeCallback(InputDialog@ dialog) {} void questionCallback(QuestionDialog@ dialog, int answer) { if(answer == QA_Yes) { string path = path_join(topMod.abspath, loadedFile); if(fileExists(path)) { ::deleteFile(path); checkEmptyDirectory(path); } deleteFile.disabled = true; duplicateFile.disabled = true; if(editor !is null) { editor.remove(); @editor = null; } type.onChange(); files.load(folder, extension=".txt"); } } void updateRevertButton() { deleteFile.disabled = loadedFile.length == 0 || !fileExists(path_join(topMod.abspath, loadedFile)); if(currentMod !is topMod && fileExists(path_join(currentMod.abspath, loadedFile))) { deleteFile.text = "Revert"; deleteFile.color = Color(0xff8000ff); setMarkupTooltip(deleteFile, "Revert the selected file back to the version from the base game."); } else { deleteFile.text = "Delete"; deleteFile.color = Color(0xff8000ff); setMarkupTooltip(deleteFile, "Delete this file completely from the game."); } } bool onGuiEvent(const GuiEvent& evt) override { if(evt.type == GUI_Confirmed) { if(evt.caller is files) { setGuiFocus(this); loadedFile = files.getSelection().basePath; title = getBasename(loadedFile); if(editor !is null) editor.remove(); @editor = BlockEditor(editPanel); editor.parse(resolve(loadedFile), type); baseFile.load(path_join(currentMod.abspath, loadedFile)); editPanel.updateAbsolutePosition(); duplicateFile.disabled = false; updateRevertButton(); } else if(editor !is null) { editor.commit(); string path = path_join(topMod.abspath, loadedFile); bool existed = fileExists(path); if(baseFile.exists && topMod !is baseMod && baseFile == editor.file) { if(existed) { ::deleteFile(path); checkEmptyDirectory(path); existed = false; } } else { ensureFile(path); editor.file.save(path); } type.onChange(); editPanel.updateAbsolutePosition(); if(!existed) { files.load(folder, extension=".txt"); files.select(loadedFile, bubble=false); } updateRevertButton(); } } if(evt.type == GUI_Changed && cast(evt.caller) !is null) { editPanel.updateAbsolutePosition(); return true; } if(evt.type == GUI_Clicked) { if(evt.caller is newFile) { string defname; auto@ sel = files.getSelection(); if(sel !is null) { defname = path_up(sel.basePath.substr(folder.length+1)); if(defname.length != 0) defname = path_join(defname, ""); } duplicate = false; InputDialog@ dialog = InputDialog(this, this); dialog.addTitle("Create File"); dialog.accept.text = "Create"; dialog.addTextInput("Filename", defname); addDialog(dialog); dialog.focusInput(); return true; } else if(evt.caller is duplicateFile) { string defname; auto@ sel = files.getSelection(); if(sel !is null) { defname = path_up(sel.basePath.substr(folder.length+1)); if(defname.length != 0) defname = path_join(defname, ""); } duplicate = true; InputDialog@ dialog = InputDialog(this, this); dialog.addTitle("Duplicate File"); dialog.accept.text = "Duplicate"; dialog.addTextInput("Filename", defname); addDialog(dialog); dialog.focusInput(); return true; } else if(evt.caller is deleteFile) { question("Are you sure you wish to delete "+loadedFile+" from the mod? This cannot be undone.\n\nIf the file consisted of changes to a base mod file, deleting the file from your mod will revert the changes back to the original version.", this); return true; } } return Tab::onGuiEvent(evt); } void draw() { skin.draw(SS_DesignOverviewBG, SF_Normal, AbsolutePosition); BaseGuiElement::draw(); } }; Tab@ createEditorTab(const string& folder, FileDef@ type) { return EditorTab(folder, type); } Tab@ createEditorTab(const string& file) { string folder; FileDef@ type; for(uint i = 0, cnt = fileFolders.length; i < cnt; ++i) { if(path_inside(fileFolders[i], file)) { folder = fileFolders[i]; @type = cast(getClass(fileTypes[i]).create()); break; } } if(type is null) return null; EditorTab tab(folder, type); tab.files.select(file); return tab; } array fileClasses = {"statuses", "abilities", "anomalies", "artifacts", "influence", "research", "resources", "buildings", "biomes", "creeps", "orbitals", "traits", "subsystems", "effectors", "materials", "sounds", "constructions", "random_events", "cargo", "attitudes"}; array fileFolders = {"data/statuses", "data/abilities", "data/anomalies", "data/artifacts", "data/influence", "data/research", "data/resources", "data/buildings", "data/biomes", "data/creeps", "data/orbitals", "data/traits", "data/subsystems", "data/effectors", "data/materials", "data/sounds", "data/constructions", "data/random_events", "data/cargo", "data/attitudes"}; array fileTypes = {"StatusFile", "AbilityFile", "AnomalyFile", "ArtifactFile", "InfluenceFile", "ResearchFile", "ResourceFile", "BuildingFile", "BiomeFile", "CreepFile", "OrbitalFile", "TraitFile", "SubsystemFile", "EffectorFile", "MaterialFile", "SoundFile", "ConstructionFile", "EventFile", "CargoFile", "AttitudeFile"}; class ModEditorCommand : ConsoleCommand { void execute(const string& args) { int found = -1; string check = args.trimmed(); for(uint i = 0, cnt = fileClasses.length; i < cnt; ++i) { if(fileClasses[i].equals_nocase(check)) { found = int(i); break; } } if(found == -1) { print("Usage: mod_editor \nSpecify one of the following classes of data files to edit:\n"); for(uint i = 0, cnt = fileClasses.length; i < cnt; ++i) { print(" "+fileClasses[i]); } return; } auto@ tab = createEditorTab(fileFolders[found], cast(getClass(fileTypes[found]).create())); newTab(tab); switchToTab(tab); } }; bool canEditFile(const string& basePath) { if(!basePath.endswith(".txt")) return false; for(uint i = 0, cnt = fileFolders.length; i < cnt; ++i) { if(path_inside(fileFolders[i], basePath)) { return true; } } return false; } string getLocaleFile(IGuiElement@ elem) { for(uint i = 0, cnt = tabCount; i < cnt; ++i) { auto@ tab = cast(tabs[i]); if(tab !is null) { if(elem.isChildOf(tab)) { return tab.type.localeFile; } } } auto@ tab = cast(ActiveTab); if(tab !is null) return tab.type.localeFile; return "misc.txt"; } void init() { addConsoleCommand("mod_editor", ModEditorCommand()); /*auto@ tab = createEditorTab("data/subsystems", SubsystemFile());*/ /*newTab(tab);*/ /*switchToTab(tab);*/ } void fillTargetsFromTab(Field@ sourceField, TargetChooser@ fld) { auto@ tab = cast(ActiveTab); if(tab !is null) { if(tab.editor !is null) tab.editor.fillTargets(sourceField, fld); } }