Files
2018-07-17 14:15:37 +02:00

1096 lines
29 KiB
ActionScript

#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<FieldInfo@> fields;
array<FieldInfo@> hooks;
array<BlockEditor@> blocks;
array<LineDesc@> extraLines;
GuiButton@ addHookButton;
array<GuiButton@> addBlockButtons;
array<GuiButton@> addFieldButtons;
array<FieldDef@> 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<TargetField>(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<HookField>(evt.caller.parent) !is null) {
auto@ hf = cast<HookField>(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<IGuiElement>(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<BlockEditor>(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<BaseGuiElement>(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<LineDesc@>& 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<bool> 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<IGuiElement>(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<BlockEditor>(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<string> 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<BlockEditor>(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<FileDef>(getClass(fileTypes[i]).create());
break;
}
}
if(type is null)
return null;
EditorTab tab(folder, type);
tab.files.select(file);
return tab;
}
array<string> fileClasses = {"statuses", "abilities", "anomalies", "artifacts", "influence", "research", "resources", "buildings", "biomes", "creeps", "orbitals", "traits", "subsystems", "effectors", "materials", "sounds", "constructions", "random_events", "cargo", "attitudes"};
array<string> 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<string> 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 <class>\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<FileDef>(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<EditorTab>(tabs[i]);
if(tab !is null) {
if(elem.isChildOf(tab)) {
return tab.type.localeFile;
}
}
}
auto@ tab = cast<EditorTab>(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<EditorTab>(ActiveTab);
if(tab !is null) {
if(tab.editor !is null)
tab.editor.fillTargets(sourceField, fld);
}
}