File chooser system refactor

This commit is contained in:
Anuken
2026-06-11 18:41:25 -04:00
parent e9cb036af1
commit ff3907d6f4
27 changed files with 508 additions and 550 deletions
@@ -5,20 +5,22 @@ import android.app.*;
import android.content.*; import android.content.*;
import android.content.pm.*; import android.content.pm.*;
import android.net.*; import android.net.*;
import android.os.Build.*;
import android.os.*; import android.os.*;
import android.os.Build.*;
import android.telephony.*; import android.telephony.*;
import arc.*; import arc.*;
import arc.backend.android.*; import arc.backend.android.*;
import arc.files.*; import arc.files.*;
import arc.func.*;
import arc.scene.ui.layout.*; import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*; import arc.util.*;
import dalvik.system.*; import dalvik.system.*;
import mindustry.*; import mindustry.*;
import mindustry.game.Saves.*; import mindustry.game.Saves.*;
import mindustry.io.*; import mindustry.io.*;
import mindustry.net.*; import mindustry.net.*;
import mindustry.ui.*;
import mindustry.ui.FileChooser.*;
import mindustry.ui.dialogs.*; import mindustry.ui.dialogs.*;
import java.io.*; import java.io.*;
@@ -30,7 +32,7 @@ import static mindustry.Vars.*;
public class AndroidLauncher extends AndroidApplication{ public class AndroidLauncher extends AndroidApplication{
public static final int PERMISSION_REQUEST_CODE = 1; public static final int PERMISSION_REQUEST_CODE = 1;
boolean doubleScaleTablets = true; boolean doubleScaleTablets = true;
FileChooser chooser; FileChooserDialog chooser;
Runnable permCallback; Runnable permCallback;
@Override @Override
@@ -97,8 +99,7 @@ public class AndroidLauncher extends AndroidApplication{
} }
}; };
}catch(SecurityException e){ }catch(SecurityException e){
//`setReadOnly` to jar file in `/sdcard/Android/data/...` does not work on some Android 14 device //`setReadOnly` to jar file in `/sdcard/Android/data/...` does not work on some Android 14 devices, but in `/data/...`, it does
//But in `/data/...` it works
if(Build.VERSION.SDK_INT < VERSION_CODES.O_MR1){ if(Build.VERSION.SDK_INT < VERSION_CODES.O_MR1){
throw e; throw e;
@@ -127,27 +128,34 @@ public class AndroidLauncher extends AndroidApplication{
} }
@Override @Override
public void showFileChooser(boolean open, String title, String extension, Cons<Fi> cons){ public void showFileChooser(FileChooserParams params){
showFileChooser(open, title, cons, extension);
}
void showFileChooser(boolean open, String title, Cons<Fi> cons, String... extensions){
try{ try{
String extension = extensions[0]; String extension = params.extensions[0];
if(VERSION.SDK_INT >= VERSION_CODES.Q){ if(VERSION.SDK_INT >= VERSION_CODES.Q){
Intent intent = new Intent(open ? Intent.ACTION_OPEN_DOCUMENT : Intent.ACTION_CREATE_DOCUMENT); Intent intent = new Intent(params.open ? Intent.ACTION_OPEN_DOCUMENT : Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE); intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(extension.equals("zip") && !open && extensions.length == 1 ? "application/zip" : "*/*"); intent.setType(extension.equals("zip") && !params.open && params.extensions.length == 1 ? "application/zip" : "*/*");
intent.putExtra(Intent.EXTRA_TITLE, "export." + extension); intent.putExtra(Intent.EXTRA_TITLE, params.fileName);
if(params.allowMultiple){
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
addResultListener(i -> startActivityForResult(intent, i), (code, in) -> { addResultListener(i -> startActivityForResult(intent, i), (code, in) -> {
if(code == Activity.RESULT_OK && in != null && in.getData() != null){ if(code == Activity.RESULT_OK && in != null && in.getData() != null){
Uri uri = in.getData(); Uri[] uris;
if(in.getClipData() != null){
uris = new Uri[in.getClipData().getItemCount()];
for(int i = 0; i < uris.length; i++){
uris[i] = in.getClipData().getItemAt(i).getUri();
}
}else{
uris = new Uri[]{in.getData()};
}
if(uri.getPath().contains("(invalid)")) return; if(uris.length == 0 || uris[0].getPath().contains("(invalid)")) return;
Core.app.post(() -> Core.app.post(() -> cons.get(new Fi(uri.getPath()){ Fi[] files = Seq.with(uris).map(uri -> new Fi(uri.getPath()){
@Override @Override
public InputStream read(){ public InputStream read(){
try{ try{
@@ -165,18 +173,14 @@ public class AndroidLauncher extends AndroidApplication{
throw new ArcRuntimeException(e); throw new ArcRuntimeException(e);
} }
} }
}))); }).toArray(Fi.class);
Core.app.post(() -> Core.app.post(() -> params.handleChooseResult(files)));
} }
}); });
}else if(VERSION.SDK_INT >= VERSION_CODES.M && !(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED && }else if(VERSION.SDK_INT >= VERSION_CODES.M && !(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)){ checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)){
chooser = new FileChooser(title, file -> Structs.contains(extensions, file.extension().toLowerCase()), open, file -> { chooser = FileChooser.createFallbackFileChooser(params);
if(!open){
cons.get(file.parent().child(file.nameWithoutExtension() + "." + extension));
}else{
cons.get(file);
}
});
ArrayList<String> perms = new ArrayList<>(); ArrayList<String> perms = new ArrayList<>();
if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){ if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
@@ -187,22 +191,13 @@ public class AndroidLauncher extends AndroidApplication{
} }
requestPermissions(perms.toArray(new String[0]), PERMISSION_REQUEST_CODE); requestPermissions(perms.toArray(new String[0]), PERMISSION_REQUEST_CODE);
}else{ }else{
if(open){ FileChooser.showFallbackFileChooser(params);
new FileChooser(title, file -> Structs.contains(extensions, file.extension().toLowerCase()), true, cons).show();
}else{
super.showFileChooser(open, "@open", extension, cons);
}
} }
}catch(Throwable error){ }catch(Throwable error){
Core.app.post(() -> Vars.ui.showException(error)); Core.app.post(() -> Vars.ui.showException(error));
} }
} }
@Override
public void showMultiFileChooser(Cons<Fi> cons, String... extensions){
showFileChooser(true, "@open", cons, extensions);
}
@Override @Override
public void beginForceLandscape(){ public void beginForceLandscape(){
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
@@ -235,7 +230,6 @@ public class AndroidLauncher extends AndroidApplication{
Log.err("Failed to delete cached folder", t); Log.err("Failed to delete cached folder", t);
} }
//move to internal storage if there's no file indicating that it moved //move to internal storage if there's no file indicating that it moved
if(!Core.files.local("files_moved").exists()){ if(!Core.files.local("files_moved").exists()){
Log.info("Moving files to external storage..."); Log.info("Moving files to external storage...");
+61 -7
View File
@@ -308,12 +308,6 @@
"discord-logo-white" "discord-logo-white"
] ]
}, },
{
"uid": "dd6df3b754e8d0dbf09d9b24088f0442",
"css": "box",
"code": 59422,
"src": "iconic"
},
{ {
"uid": "f08ba9f0e8f9b426a92efcaa59c1e932", "uid": "f08ba9f0e8f9b426a92efcaa59c1e932",
"css": "reddit-alien", "css": "reddit-alien",
@@ -1203,6 +1197,66 @@
"css": "sitemap", "css": "sitemap",
"code": 61672, "code": 61672,
"src": "fontawesome" "src": "fontawesome"
},
{
"uid": "2c413e78faf1d6631fd7b094d14c2253",
"css": "cloud",
"code": 59441,
"src": "fontawesome"
},
{
"uid": "de2fc7a5c986ab8c622f63455d7cf814",
"css": "upload-cloud",
"code": 61678,
"src": "fontawesome"
},
{
"uid": "f5999a012fc3752386635ec02a858447",
"css": "download-cloud",
"code": 61677,
"src": "fontawesome"
},
{
"uid": "e7a4824e79002c2d8ce3047b2da8b5a2",
"css": "rain",
"code": 59442,
"src": "typicons"
},
{
"uid": "ac10a3a3077eab1b9d66509eb25fb9b4",
"css": "cloud-sun",
"code": 59448,
"src": "typicons"
},
{
"uid": "6f200a5a449259c51cd0ccf56b4c1e93",
"css": "drizzle",
"code": 59452,
"src": "typicons"
},
{
"uid": "3cac7625657b10cf706e7f332749c912",
"css": "cloud-flash",
"code": 59456,
"src": "typicons"
},
{
"uid": "364cc69fb3edb8fb21caa6a2c0489589",
"css": "cloud-wind",
"code": 59457,
"src": "typicons"
},
{
"uid": "cc05df515bebe11df3ada0a5910a8f6d",
"css": "box",
"code": 59422,
"src": "entypo"
},
{
"uid": "d5f287ad8e3a2db8f808bc09980ec3dc",
"css": "chart-alt",
"code": 59459,
"src": "typicons"
} }
] ]
} }
Binary file not shown.
Binary file not shown.
-36
View File
@@ -3,13 +3,11 @@ package mindustry.core;
import arc.*; import arc.*;
import arc.assets.*; import arc.assets.*;
import arc.audio.*; import arc.audio.*;
import arc.graphics.*;
import arc.graphics.g2d.*; import arc.graphics.g2d.*;
import arc.input.*; import arc.input.*;
import arc.math.*; import arc.math.*;
import arc.scene.style.*; import arc.scene.style.*;
import arc.scene.ui.*; import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*; import arc.struct.*;
import arc.util.*; import arc.util.*;
import mindustry.*; import mindustry.*;
@@ -24,7 +22,6 @@ import mindustry.game.Objectives.*;
import mindustry.game.Saves.*; import mindustry.game.Saves.*;
import mindustry.game.Teams.*; import mindustry.game.Teams.*;
import mindustry.gen.*; import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.input.*; import mindustry.input.*;
import mindustry.io.*; import mindustry.io.*;
import mindustry.io.SaveIO.*; import mindustry.io.SaveIO.*;
@@ -113,39 +110,6 @@ public class Control implements ApplicationListener, Loadable{
input.checkUnit(); input.checkUnit();
}); });
Events.on(WorldLoadEvent.class, e -> app.post(() -> {
var errored = state.data.getContent().select(c -> c.errored);
if(!errored.isEmpty()){
new BaseDialog(bundle.get("mod.erroredcontent").replace("[red]", "")){{
setFillParent(true);
setTitleColor(Color.scarlet);
cont.margin(15);
cont.row();
cont.add("@mod.errors.map").wrap().growX().center().labelAlign(Align.center);
cont.row();
cont.pane(d -> {
d.left().marginLeft(15f);
for(var asset : errored){
d.add(asset.path).color(Pal.accent).left().padRight(10).growX().row();
d.table(t -> {
t.left();
t.image(Tex.whiteui).width(3f).growY().color(Color.lightGray).padRight(8f).padTop(2f).padBottom(2f);
t.add(asset.warnings.toString("\n\n")).color(Color.lightGray).labelAlign(Align.left, Align.left).left().growX().wrap().padTop(5f).row();
}).padBottom(10f).grow().row();
}
}).width(Math.min(graphics.getWidth() / Scl.scl(1f) * 0.9f, 700f));
buttons.button("@quit", Icon.exit, () -> {
hide();
ui.paused.runExitSave(false);
}).size(210, 64);
buttons.button("@continue", Icon.warning, this::hide).size(210, 64);
}}.show();
}
}));
Events.on(ResetEvent.class, event -> { Events.on(ResetEvent.class, event -> {
player.reset(); player.reset();
toBePlaced.clear(); toBePlaced.clear();
+4 -67
View File
@@ -2,22 +2,18 @@ package mindustry.core;
import arc.*; import arc.*;
import arc.files.*; import arc.files.*;
import arc.func.*;
import arc.math.*; import arc.math.*;
import arc.struct.*; import arc.struct.*;
import arc.util.*;
import arc.util.serialization.*; import arc.util.serialization.*;
import mindustry.mod.*; import mindustry.mod.*;
import mindustry.net.*; import mindustry.net.*;
import mindustry.net.Net.*; import mindustry.net.Net.*;
import mindustry.type.*; import mindustry.type.*;
import mindustry.ui.dialogs.*; import mindustry.ui.FileChooser.*;
import rhino.*; import rhino.*;
import java.net.*; import java.net.*;
import static mindustry.Vars.*;
public interface Platform{ public interface Platform{
/** Dynamically creates a class loader for a jar file. This loader must be child-first. */ /** Dynamically creates a class loader for a jar file. This loader must be child-first. */
@@ -106,68 +102,9 @@ public interface Platform{
default void shareFile(Fi file){ default void shareFile(Fi file){
} }
default void export(String name, String extension, FileWriter writer){ /** Do not call directly; use the builder pattern in {@link mindustry.ui.FileChooser}. */
if(!ios){ default void showFileChooser(FileChooserParams params){
platform.showFileChooser(false, extension, file -> { throw new IllegalArgumentException("Not implemented on this platform!");
ui.loadAnd(() -> {
try{
writer.write(file);
}catch(Throwable e){
ui.showException(e);
Log.err(e);
}
});
});
}else{
ui.loadAnd(() -> {
try{
Fi result = Core.files.local(name + "." + extension);
writer.write(result);
platform.shareFile(result);
}catch(Throwable e){
ui.showException(e);
Log.err(e);
}
});
}
}
/**
* Show a file chooser.
* @param cons Selection listener
* @param open Whether to open or save files
* @param extension File extension to filter
* @param title The title of the native dialog
*/
default void showFileChooser(boolean open, String title, String extension, Cons<Fi> cons){
defaultFileDialog(open, title, extension, cons);
}
/**
* Show a file chooser for multiple file types.
* @param cons Selection listener
* @param extensions File extensions to filter
*/
default void showMultiFileChooser(Cons<Fi> cons, String... extensions){
defaultMultiFileChooser(cons, extensions);
}
default void showFileChooser(boolean open, String extension, Cons<Fi> cons){
showFileChooser(open, open ? "@open": "@save", extension, cons);
}
static void defaultFileDialog(boolean open, String title, String extension, Cons<Fi> cons){
new FileChooser(title, file -> file.extEquals(extension), open, file -> {
if(!open){
cons.get(file.parent().child(file.nameWithoutExtension() + "." + extension));
}else{
cons.get(file);
}
}).show();
}
static void defaultMultiFileChooser(Cons<Fi> cons, String... extensions){
new FileChooser("@open", file -> Structs.contains(extensions, file.extension().toLowerCase()), true, cons).show();
} }
/** Hide the app. Android only. */ /** Hide the app. Android only. */
@@ -94,7 +94,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
t.button("@editor.import", Icon.download, () -> createDialog("@editor.import", t.button("@editor.import", Icon.download, () -> createDialog("@editor.import",
"@editor.importmap", "@editor.importmap.description", Icon.download, (Runnable)loadDialog::show, "@editor.importmap", "@editor.importmap.description", Icon.download, (Runnable)loadDialog::show,
"@editor.importfile", "@editor.importfile.description", Icon.file, (Runnable)() -> "@editor.importfile", "@editor.importfile.description", Icon.file, (Runnable)() ->
platform.showFileChooser(true, mapExtension, file -> ui.loadAnd(() -> { FileChooser.open(mapExtension).submit(file -> ui.loadAnd(() -> {
maps.tryCatchMapError(() -> { maps.tryCatchMapError(() -> {
if(MapIO.isImage(file)){ if(MapIO.isImage(file)){
ui.showInfo("@editor.errorimage"); ui.showInfo("@editor.errorimage");
@@ -105,7 +105,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
})), })),
"@editor.importimage", "@editor.importimage.description", Icon.fileImage, (Runnable)() -> "@editor.importimage", "@editor.importimage.description", Icon.fileImage, (Runnable)() ->
platform.showFileChooser(true, "png", file -> FileChooser.open("png").submit(file ->
ui.loadAnd(() -> { ui.loadAnd(() -> {
try{ try{
Pixmap pixmap = new Pixmap(file); Pixmap pixmap = new Pixmap(file);
@@ -124,9 +124,9 @@ public class MapEditorDialog extends Dialog implements Disposable{
t.button("@editor.export", Icon.upload, () -> createDialog("@editor.export", t.button("@editor.export", Icon.upload, () -> createDialog("@editor.export",
"@editor.exportfile", "@editor.exportfile.description", Icon.file, "@editor.exportfile", "@editor.exportfile.description", Icon.file,
(Runnable)() -> platform.export(editor.tags.get("name", "unknown"), mapExtension, file -> MapIO.writeMap(file, editor.createMap(file))), (Runnable)() -> FileChooser.export(editor.tags.get("name", "unknown"), mapExtension, file -> MapIO.writeMap(file, editor.createMap(file))),
"@editor.exportimage", "@editor.exportimage.description", Icon.fileImage, "@editor.exportimage", "@editor.exportimage.description", Icon.fileImage,
(Runnable)() -> platform.export(editor.tags.get("name", "unknown"), "png", file -> { (Runnable)() -> FileChooser.export(editor.tags.get("name", "unknown"), "png", file -> {
Pixmap out = MapIO.writeImage(editor.tiles()); Pixmap out = MapIO.writeImage(editor.tiles());
file.writePng(out); file.writePng(out);
out.dispose(); out.dispose();
@@ -16,12 +16,8 @@ import static mindustry.Vars.*;
/* /*
TODO: TODO:
• warn about critical errors for content that prevent content loading
• dividers for content types • dividers for content types
• weather icon in font?
• remove planets from list of content that can be parsed to prevent confusion?
• drop down for import/export zip, clear all • drop down for import/export zip, clear all
• allow multi file selection when importing
*/ */
public class MapAssetsDialog extends BaseDialog{ public class MapAssetsDialog extends BaseDialog{
private static final TextureRegionDrawable[] typeIcons = { private static final TextureRegionDrawable[] typeIcons = {
@@ -71,6 +71,8 @@ public class MapAudioView implements AssetView{
m.setPosition(value); m.setPosition(value);
} }
}); });
//only ogg can seek
slider.touchable = asset.path.endsWith(".ogg") ? Touchable.enabled : Touchable.disabled;
slider.visible(() -> { slider.visible(() -> {
boolean valid = lastPlaying == m && m.isPlaying(); boolean valid = lastPlaying == m && m.isPlaying();
if(valid){ if(valid){
@@ -88,25 +90,7 @@ public class MapAudioView implements AssetView{
} }
}).size(mobile ? 390f : 450f, h).margin(10f); }).size(mobile ? 390f : 450f, h).margin(10f);
list.button(Icon.export, Styles.graySquarei, Vars.iconMed, () -> { list.button(Icon.export, Styles.graySquarei, Vars.iconMed, () -> FileChooser.export(asset.name, Strings.getFileExtension(asset.path), file::copyTo)).size(h).disabled(file == null);
if(ios){
try{
Fi out = tmpDirectory.child(Strings.getFileName(asset.path));
file.copyTo(out);
platform.shareFile(out);
}catch(Exception e){
ui.showException(e);
}
}else{
platform.showFileChooser(false, Strings.getFileExtension(asset.path), result -> {
try{
file.copyTo(result);
}catch(Exception e){
ui.showException(e);
}
});
}
}).size(h).disabled(file == null);
list.button(Icon.trash, Styles.graySquarei, iconMed, () -> { list.button(Icon.trash, Styles.graySquarei, iconMed, () -> {
ui.showConfirm("@asset.delete.confirm", () -> { ui.showConfirm("@asset.delete.confirm", () -> {
@@ -140,32 +124,35 @@ public class MapAudioView implements AssetView{
public void buildButtons(MapAssetsDialog diag, Table buttons){ public void buildButtons(MapAssetsDialog diag, Table buttons){
buttons.button("@add", Icon.add, () -> { buttons.button("@add", Icon.add, () -> {
platform.showMultiFileChooser(result -> { FileChooser.open("ogg", "mp3").submitMulti(result -> {
try{ var assets = state.data.getAssets(type);
//path and name are the same here; there's no path context.
String name = result.nameWithoutExtension(); for(Fi file : result){
String path = name; try{
var other = state.data.getAssets(type).find(p -> (p.path.equalsIgnoreCase(path) || p.name.equalsIgnoreCase(name))); //path and name are the same here; there's no path context.
if(other != null){ String name = file.nameWithoutExtension();
ui.showErrorMessage(Core.bundle.format("asset.exists", other.name + " (" + other.path + ")")); String path = name;
return; var other = state.data.getAssets(type).find(p -> (p.path.equalsIgnoreCase(path) || p.name.equalsIgnoreCase(name)));
if(other != null){
ui.showErrorMessage(Core.bundle.format("asset.exists", other.name + " (" + other.path + ")"));
return;
}
DataAsset asset = (type == DataAssetType.music ? new MusicAsset() : new SoundAsset());
asset.setPath(path);
asset.updateData(file.readBytes());
assets.add(asset);
}catch(Exception e){
ui.showException(e);
} }
var assets = state.data.getAssets(type);
DataAsset asset = (type == DataAssetType.music ? new MusicAsset() : new SoundAsset());
asset.setPath(path);
asset.updateData(result.readBytes());
assets.add(asset);
assets.sort();
state.data.reloadAudio();
diag.rebuild();
lastPlaying = null;
}catch(Exception e){
ui.showException(e);
} }
}, "ogg", "mp3");
assets.sort();
state.data.reloadAudio();
diag.rebuild();
lastPlaying = null;
});
}).size(190f, 64f); }).size(190f, 64f);
} }
@@ -69,23 +69,7 @@ public class MapBundlesView implements AssetView{
}).size(h).disabled(file == null); }).size(h).disabled(file == null);
list.button(Icon.export, Styles.graySquarei, Vars.iconMed, () -> { list.button(Icon.export, Styles.graySquarei, Vars.iconMed, () -> {
if(ios){ FileChooser.export(bundle.name, "properties", file::copyTo);
try{
Fi out = tmpDirectory.child(Strings.getFileName(bundle.path));
file.copyTo(out);
platform.shareFile(out);
}catch(Exception e){
ui.showException(e);
}
}else{
platform.showFileChooser(false, "properties", out -> {
try{
file.copyTo(out);
}catch(Exception e){
ui.showException(e);
}
});
}
}).size(h).disabled(file == null); }).size(h).disabled(file == null);
list.button(Icon.trash, Styles.graySquarei, iconMed, () -> { list.button(Icon.trash, Styles.graySquarei, iconMed, () -> {
@@ -140,10 +124,12 @@ public class MapBundlesView implements AssetView{
handler.get("bundle.properties", Core.app.getClipboardText()); handler.get("bundle.properties", Core.app.getClipboardText());
}).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null); }).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null);
t.row(); t.row();
t.button("@schematic.importfile", Icon.download, style, () -> platform.showMultiFileChooser(file -> { t.button("@schematic.importfile", Icon.download, style, () -> FileChooser.open("properties", "txt").submitMulti(files -> {
dialog.hide(); dialog.hide();
handler.get(file.name(), file.readString()); for(var file : files){
}, "txt", "bundle")).marginLeft(12f); handler.get(file.name(), file.readString());
}
})).marginLeft(12f);
t.row(); t.row();
}); });
}); });
@@ -1,7 +1,6 @@
package mindustry.editor.data; package mindustry.editor.data;
import arc.*; import arc.*;
import arc.files.*;
import arc.scene.event.*; import arc.scene.event.*;
import arc.scene.style.*; import arc.scene.style.*;
import arc.scene.ui.layout.*; import arc.scene.ui.layout.*;
@@ -24,7 +23,7 @@ public class MapContentView implements AssetView{
ContentType.unit, Icon.units, ContentType.unit, Icon.units,
ContentType.block, Icon.distribution, ContentType.block, Icon.distribution,
ContentType.planet, Icon.planet, ContentType.planet, Icon.planet,
ContentType.weather, Icon.power, //TODO cloud icon ContentType.weather, Icon.drizzle,
ContentType.status, Icon.power ContentType.status, Icon.power
); );
@@ -44,7 +43,14 @@ public class MapContentView implements AssetView{
dialog.cont.top().pane(p -> { dialog.cont.top().pane(p -> {
p.top(); p.top();
for(var warning : content.warnings){ var warns = content.warnings;
if(content.errored){
warns = new Seq<>();
warns.add(Iconc.warning + " This content has critical errors, and cannot be used until the errors are fixed.");
warns.addAll(content.warnings);
}
for(var warning : warns){
p.table(Styles.grayPanel, in -> { p.table(Styles.grayPanel, in -> {
in.add(warning.replaceAll("\t", " "), Styles.monoLabel).grow().wrap(); in.add(warning.replaceAll("\t", " "), Styles.monoLabel).grow().wrap();
}).margin(6f).growX().pad(3f).row(); }).margin(6f).growX().pad(3f).row();
@@ -80,23 +86,7 @@ public class MapContentView implements AssetView{
}).size(h); }).size(h);
list.button(Icon.export, Styles.graySquarei, Vars.iconMed, () -> { list.button(Icon.export, Styles.graySquarei, Vars.iconMed, () -> {
if(ios){ FileChooser.export(content.name, "json", file -> file.writeString(content.data));
try{
Fi out = tmpDirectory.child(Strings.getFileName(content.path));
out.writeString(content.data);
platform.shareFile(out);
}catch(Exception e){
ui.showException(e);
}
}else{
platform.showFileChooser(false, "properties", out -> {
try{
out.writeString(content.data);
}catch(Exception e){
ui.showException(e);
}
});
}
}).size(h); }).size(h);
list.button(Icon.trash, Styles.graySquarei, iconMed, () -> { list.button(Icon.trash, Styles.graySquarei, iconMed, () -> {
@@ -158,11 +148,11 @@ public class MapContentView implements AssetView{
choose.buttons.button("@cancel", Icon.cancel, choose::hide).size(170f, 64f); choose.buttons.button("@cancel", Icon.cancel, choose::hide).size(170f, 64f);
choose.buttons.button("@asset.content.import.file", Icon.fileText, () -> platform.showMultiFileChooser(file -> { choose.buttons.button("@asset.content.import.file", Icon.fileText, () -> FileChooser.open("json", "json5", "hjson").submit(file -> {
choose.hide(); choose.hide();
addContent(selected[0], nameField.getText(), file.readString()); addContent(selected[0], nameField.getText(), file.readString());
diag.rebuild(); diag.rebuild();
}, "json", "hjson", "json5")).size(200f, 64f).disabled(b -> nameField.getText().isEmpty() || invalid[0]); })).size(200f, 64f).disabled(b -> nameField.getText().isEmpty() || invalid[0]);
choose.buttons.button("@asset.content.import.clipboard", Icon.copy, () -> { choose.buttons.button("@asset.content.import.clipboard", Icon.copy, () -> {
String text = Core.app.getClipboardText(); String text = Core.app.getClipboardText();
@@ -30,40 +30,43 @@ public class MapImagesView implements AssetView{
public void buildButtons(MapAssetsDialog diag, Table buttons){ public void buildButtons(MapAssetsDialog diag, Table buttons){
buttons.button("@add", Icon.add, () -> { buttons.button("@add", Icon.add, () -> {
platform.showFileChooser(true, "png", result -> { FileChooser.open("png", "jpeg", "jpg").submitMulti(results -> {
try{ var images = getImages();
Pixmap pix = new Pixmap(result);
int width = pix.width;
int height = pix.height;
Pixmaps.bleed(pix);
byte[] bytes = PixmapIO.writePngBytes(pix);
pix.dispose();
if(width > DataPatcher.maxImageSize || height > DataPatcher.maxImageSize){ for(var result : results){
ui.showErrorMessage(Core.bundle.format("asset.image.toolarge", width, height, DataPatcher.maxImageSize, DataPatcher.maxImageSize)); try{
return; Pixmap pix = new Pixmap(result);
int width = pix.width;
int height = pix.height;
Pixmaps.bleed(pix);
byte[] bytes = PixmapIO.writePngBytes(pix);
pix.dispose();
if(width > DataPatcher.maxImageSize || height > DataPatcher.maxImageSize){
ui.showErrorMessage(Core.bundle.format("asset.image.toolarge", width, height, DataPatcher.maxImageSize, DataPatcher.maxImageSize));
return;
}
//path and name are the same here; there's no path context.
String name = result.nameWithoutExtension();
String path = name;
var other = images.find(p -> (p.path.equalsIgnoreCase(path) || p.name.equalsIgnoreCase(name)));
if(other != null){
ui.showErrorMessage(Core.bundle.format("asset.image.exists", other.name + " (" + other.path + ")"));
return;
}
byte[] hash = assetCache.add(bytes);
images.add(new ImageAsset(path, hash));
}catch(Exception e){
ui.showException(e);
} }
var images = getImages();
//path and name are the same here; there's no path context.
String name = result.nameWithoutExtension();
String path = name;
var other = images.find(p -> (p.path.equalsIgnoreCase(path) || p.name.equalsIgnoreCase(name)));
if(other != null){
ui.showErrorMessage(Core.bundle.format("asset.image.exists", other.name + " (" + other.path + ")"));
return;
}
byte[] hash = assetCache.add(bytes);
images.add(new ImageAsset(path, hash));
images.sort();
state.data.reloadImages(images);
diag.rebuild();
}catch(Exception e){
ui.showException(e);
} }
images.sort();
state.data.reloadImages(images);
diag.rebuild();
}); });
}).size(190f, 64f); }).size(190f, 64f);
@@ -74,7 +77,7 @@ public class MapImagesView implements AssetView{
p.table(Tex.button, t -> { p.table(Tex.button, t -> {
TextButtonStyle style = Styles.flatt; TextButtonStyle style = Styles.flatt;
t.defaults().size(280f, 60f).left(); t.defaults().size(280f, 60f).left();
t.button("@asset.image.importzip", Icon.download, style, () -> platform.showFileChooser(true, "zip", file -> { t.button("@asset.image.importzip", Icon.download, style, () -> FileChooser.open("zip").submit(file -> {
//Android doesn't allow accessing the file contents outside the callback; other platforms either copy it already, or don't have dumb permission issues //Android doesn't allow accessing the file contents outside the callback; other platforms either copy it already, or don't have dumb permission issues
Fi targetFile; Fi targetFile;
@@ -171,21 +174,17 @@ public class MapImagesView implements AssetView{
} }
}); });
})).marginLeft(12f).row(); })).marginLeft(12f).row();
t.button("@asset.image.exportzip", Icon.upload, style, () -> platform.showFileChooser(false, "zip", file -> { t.button("@asset.image.exportzip", Icon.upload, style, () -> FileChooser.export("images", "zip", file -> {
dialog.hide(); dialog.hide();
try{ try(OutputStream fos = file.write(false, 4096); ZipOutputStream zos = new ZipOutputStream(fos)){
try(OutputStream fos = file.write(false, 4096); ZipOutputStream zos = new ZipOutputStream(fos)){ for(var image : getImages()){
for(var image : getImages()){ Fi cacheFile = image.getCacheFile();
Fi cacheFile = image.getCacheFile(); if(cacheFile == null) continue;
if(cacheFile == null) continue;
zos.putNextEntry(new ZipEntry(image.path + ".png")); zos.putNextEntry(new ZipEntry(image.path + ".png"));
zos.write(cacheFile.readBytes()); zos.write(cacheFile.readBytes());
zos.closeEntry(); zos.closeEntry();
}
} }
}catch(Throwable e){
ui.showException(e);
} }
})).marginLeft(12f).row(); })).marginLeft(12f).row();
t.button("@asset.image.clearall", Icon.trash, style, () -> { t.button("@asset.image.clearall", Icon.trash, style, () -> {
@@ -279,13 +278,7 @@ public class MapImagesView implements AssetView{
}); });
b.button(Icon.info, istyle, showInfo); b.button(Icon.info, istyle, showInfo);
b.button(Icon.export, istyle, () -> { b.button(Icon.export, istyle, () -> {
platform.showFileChooser(false, "png", out -> { FileChooser.export(image.name, "png", out -> image.getCacheFileNoNull().copyTo(out));
try{
image.getCacheFileNoNull().copyTo(out);
}catch(Throwable e){
ui.showException(e);
}
});
}); });
b.button(Icon.trash, istyle, () -> { b.button(Icon.trash, istyle, () -> {
ui.showConfirm("@asset.image.delete.confirm", () -> { ui.showConfirm("@asset.image.delete.confirm", () -> {
@@ -18,7 +18,10 @@ public class MapPatchesView implements AssetView{
@Override @Override
public void buildButtons(MapAssetsDialog diag, Table buttons){ public void buildButtons(MapAssetsDialog diag, Table buttons){
buttons.button("@add", Icon.add, () -> showImport(this::addPatch)).size(190f, 64f); buttons.button("@add", Icon.add, () -> showImport(s -> {
addPatch(s);
diag.rebuild();
})).size(190f, 64f);
} }
@Override @Override
@@ -106,10 +109,11 @@ public class MapPatchesView implements AssetView{
handler.get(Core.app.getClipboardText()); handler.get(Core.app.getClipboardText());
}).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null); }).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null);
t.row(); t.row();
t.button("@schematic.importfile", Icon.download, style, () -> platform.showMultiFileChooser(file -> { //currently doesn't allow multi import because it could be replacing exactly 1 patch
t.button("@schematic.importfile", Icon.download, style, () -> FileChooser.open("json", "hjson", "json5").submit(file -> {
dialog.hide(); dialog.hide();
handler.get(file.readString()); handler.get(file.readString());
}, "json", "hjson", "json5")).marginLeft(12f); })).marginLeft(12f);
t.row(); t.row();
}); });
}); });
+13 -15
View File
@@ -28,7 +28,7 @@ import java.util.*;
/** The current implementation is awful. Consider it a proof of concept. */ /** The current implementation is awful. Consider it a proof of concept. */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public class DataPatcher{ public class DataPatcher{
public static final int maxImageSize = 1024; public static final int maxImageSize = 2000;
public static final int patchFormatVersion = 2; public static final int patchFormatVersion = 2;
private static boolean needsArrayFix = false; private static boolean needsArrayFix = false;
@@ -109,7 +109,9 @@ public class DataPatcher{
set.error = false; set.error = false;
try{ try{
JsonValue value = parser.getJson().fromJson(null, Jval.read(set.patch).toString(Jformat.plain)); Object someValue = parser.getJson().fromJson(null, Jval.read(set.patch).toString(Jformat.plain));
if(!(someValue instanceof JsonValue value)) throw new SerializationException("Patch must be a JSON object.");
if(Vars.state.rules.planet != null && value.has("requiredPlanets")){ if(Vars.state.rules.planet != null && value.has("requiredPlanets")){
JsonValue req = value.get("requiredPlanets"); JsonValue req = value.get("requiredPlanets");
value.remove("requiredPlanets"); value.remove("requiredPlanets");
@@ -136,6 +138,7 @@ public class DataPatcher{
}catch(Exception e){ }catch(Exception e){
set.error = true; set.error = true;
set.name = "";
set.warnings.add(Strings.getSimpleMessage(e)); set.warnings.add(Strings.getSimpleMessage(e));
currentlyApplyingPatch = null; currentlyApplyingPatch = null;
@@ -146,7 +149,6 @@ public class DataPatcher{
if(!content.isEmpty()){ if(!content.isEmpty()){
content.sort(); content.sort();
ContentLoader preLoad = Vars.content.copy();
dpMod.erroredContent.clear(); dpMod.erroredContent.clear();
for(var asset : content){ for(var asset : content){
@@ -169,7 +171,6 @@ public class DataPatcher{
needsArrayFix = true; needsArrayFix = true;
} }
//TODO: what to do when errors happen in general?
try{ try{
//this binds the content but does not load it entirely //this binds the content but does not load it entirely
asset.content = parser.parse(dpMod, asset.name, asset.data, file, asset.type); asset.content = parser.parse(dpMod, asset.name, asset.data, file, asset.type);
@@ -178,10 +179,11 @@ public class DataPatcher{
asset.warnings.add(Strings.getFinalMessage(e)); asset.warnings.add(Strings.getFinalMessage(e));
asset.errored = true; asset.errored = true;
//TODO: this warning is not very useful, nor is markError in general. What should be done here? var lastAdded = Vars.content.getLastAdded();
if(current != Vars.content.getLastAdded() && Vars.content.getLastAdded() != null){ if(current != lastAdded && lastAdded != null){
Vars.content.remove(lastAdded);
//markError should log it already //markError should log it already
parser.markError(Vars.content.getLastAdded(), dpMod, file, e); parser.markError(lastAdded, dpMod, file, e);
}else{ }else{
Log.err("Error loading content: " + asset.path, e); Log.err("Error loading content: " + asset.path, e);
} }
@@ -207,6 +209,7 @@ public class DataPatcher{
try{ try{
cont.init(); cont.init();
}catch(Throwable t){ }catch(Throwable t){
Vars.content.remove(cont);
if(cont.minfo.asset != null) cont.minfo.asset.errored = true; if(cont.minfo.asset != null) cont.minfo.asset.errored = true;
parser.markError(cont, dpMod, cont.minfo.sourceFile, t); parser.markError(cont, dpMod, cont.minfo.sourceFile, t);
} }
@@ -216,6 +219,7 @@ public class DataPatcher{
try{ try{
cont.postInit(); cont.postInit();
}catch(Throwable t){ }catch(Throwable t){
Vars.content.remove(cont);
if(cont.minfo.asset != null) cont.minfo.asset.errored = true; if(cont.minfo.asset != null) cont.minfo.asset.errored = true;
parser.markError(cont, dpMod, cont.minfo.sourceFile, t); parser.markError(cont, dpMod, cont.minfo.sourceFile, t);
} }
@@ -227,24 +231,18 @@ public class DataPatcher{
cont.loadIcon(); cont.loadIcon();
cont.load(); cont.load();
if(cont.minfo.asset != null && cont instanceof UnlockableContent u){ if(cont.minfo.asset != null && cont instanceof UnlockableContent u){
if(!u.uiIcon.found()){ if(!u.uiIcon.found() && u.getContentType() != ContentType.planet && u.getContentType() != ContentType.weather){
cont.minfo.asset.warnings.add("[" + u.name.substring(u.minfo.mod.name.length() + 1) + "] Could not find an icon. Ensure that you have an image named '" + u.name + "' loaded. Remember that imported images always have the 'dp-' prefix automatically applied."); cont.minfo.asset.warnings.add("[" + u.name.substring(u.minfo.mod.name.length() + 1) + "] Could not find an icon. Ensure that you have an image named '" + u.name + "' loaded. Remember that imported images always have the 'dp-' prefix automatically applied.");
} }
} }
}catch(Throwable t){ }catch(Throwable t){
Vars.content.remove(cont);
if(cont.minfo.asset != null) cont.minfo.asset.errored = true; if(cont.minfo.asset != null) cont.minfo.asset.errored = true;
parser.markError(cont, dpMod, cont.minfo.sourceFile, t); parser.markError(cont, dpMod, cont.minfo.sourceFile, t);
} }
} }
} }
if(all.contains(Content::hasErrored)){
Log.err("Errors were encountered loading content. Content will be unloaded.");
callContentRemove();
Vars.content = preLoad;
needsArrayFix = false;
}
if(reloadContentWorld) fixContentArrays(); if(reloadContentWorld) fixContentArrays();
} }
@@ -8,8 +8,8 @@ import mindustry.ctype.*;
import java.io.*; import java.io.*;
public class ContentAsset extends DataAsset{ public class ContentAsset extends DataAsset{
//Note: sectors can't be loaded at the moment //Note: sectors and planets can't be loaded at the moment, as custom campaigns aren't functional to begin with, and adding them would cause confusion
public static final ContentType[] loadableContent = {ContentType.item, ContentType.block, ContentType.liquid, ContentType.status, ContentType.unit, ContentType.weather, ContentType.planet}; public static final ContentType[] loadableContent = {ContentType.item, ContentType.block, ContentType.liquid, ContentType.status, ContentType.unit, ContentType.weather};
/** Content type to be parsed as. */ /** Content type to be parsed as. */
public ContentType type = ContentType.unit; public ContentType type = ContentType.unit;
+142
View File
@@ -0,0 +1,142 @@
package mindustry.ui;
import arc.*;
import arc.files.*;
import arc.func.*;
import arc.util.*;
import mindustry.*;
import mindustry.core.Platform.*;
import mindustry.ui.dialogs.*;
import static mindustry.Vars.*;
public class FileChooser{
public static void export(String name, String extension, FileWriter writer){
if(!ios){
FileChooser.save(extension).name(name).submit(file -> {
ui.loadAnd(() -> {
try{
writer.write(file);
}catch(Throwable e){
ui.showException(e);
Log.err(e);
}
});
});
}else{
ui.loadAnd(() -> {
try{
Fi result = Core.files.local(name + "." + extension);
writer.write(result);
platform.shareFile(result);
}catch(Throwable e){
ui.showException(e);
Log.err(e);
}
});
}
}
public static FileChooserParams open(String... extensions){
return new FileChooserParams().open(true).extensions(extensions);
}
public static FileChooserParams save(String... extensions){
return new FileChooserParams().open(false).extensions(extensions);
}
public static FileChooserDialog createFallbackFileChooser(FileChooserParams params){
return new FileChooserDialog(params.title, file -> Structs.contains(params.extensions, file::extEquals), params.open, file -> {
if(!params.open){
params.handleChooseResult(file.parent().child(file.nameWithoutExtension() + "." + params.extensions[0]));
}else{
params.handleChooseResult(file);
}
});
}
public static void showFallbackFileChooser(FileChooserParams params){
createFallbackFileChooser(params).show();
}
public static class FileChooserParams{
public boolean open;
public boolean allowMultiple;
public String title;
public String fileName;
public String[] extensions;
public @Nullable Cons<Fi> handler;
public @Nullable Cons<Fi[]> multipleHandler;
private void checkParams(){
if(extensions == null || extensions.length == 0) throw new IllegalArgumentException("Extension types must be defined.");
if(title == null){
title = open ? Core.bundle.get("open") : Core.bundle.get("save");
}else if(title.startsWith("@")){
title = Core.bundle.get(title.substring(1));
}
if(fileName == null){
fileName = "file." + extensions[0];
}
}
/** Submits the request to handle a single file. */
public void submit(Cons<Fi> handler){
checkParams();
this.handler = handler;
Vars.platform.showFileChooser(this);
}
/** Submits the request to handle multiple files. */
public void submitMulti(Cons<Fi[]> multipleHandler){
checkParams();
if(!open) throw new IllegalArgumentException("Saving in a file chooser with multiple choices does not make sense.");
this.multipleHandler = multipleHandler;
this.allowMultiple = true;
Vars.platform.showFileChooser(this);
}
/** Submits the request to handle multiple files as a loop that iterates through the resulting files and invokes the callback repeatedly. */
public void submitMultiLoop(Cons<Fi> multipleHandler){
submitMulti(files -> {
for(var f : files){
multipleHandler.get(f);
}
});
}
public FileChooserParams name(String fileName){
this.fileName = fileName;
return this;
}
public FileChooserParams extensions(String... extensions){
this.extensions = extensions;
return this;
}
public FileChooserParams open(boolean open){
this.open = open;
return this;
}
public FileChooserParams title(String title){
this.title = title;
return this;
}
public void handleChooseResult(Fi... files){
if(files.length == 0) return;
if(handler != null){
handler.get(files[0]);
}else if(multipleHandler != null){
multipleHandler.get(files);
}
}
}
}
@@ -43,9 +43,9 @@ public class CanvasEditDialog extends BaseDialog{
addCloseButton(160f); addCloseButton(160f);
buttons.button("@import", Icon.image, () -> platform.showFileChooser(true, "png", this::importFrom)); buttons.button("@import", Icon.image, () -> FileChooser.open("png", "jpeg").submit(this::importFrom));
buttons.button("@export", Icon.export, () -> platform.showFileChooser(false, "png", this::exportTo)); buttons.button("@export", Icon.export, () -> FileChooser.export("canvas", "png", this::exportTo));
hidden(() -> { hidden(() -> {
save(); save();
@@ -1,6 +1,7 @@
package mindustry.ui.dialogs; package mindustry.ui.dialogs;
import arc.*; import arc.*;
import arc.files.*;
import arc.graphics.*; import arc.graphics.*;
import arc.scene.ui.*; import arc.scene.ui.*;
import arc.scene.ui.layout.*; import arc.scene.ui.layout.*;
@@ -40,48 +41,49 @@ public class EditorMapsDialog extends MapListDialog{
}).size(210f, 64f); }).size(210f, 64f);
buttons.button("@editor.importmap", Icon.upload, () -> { buttons.button("@editor.importmap", Icon.upload, () -> {
platform.showFileChooser(true, mapExtension, file -> { FileChooser.open(mapExtension).submitMulti(files -> {
ui.loadAnd(() -> { ui.loadAnd(() -> {
maps.tryCatchMapError(() -> { for(Fi file : files){
if(MapIO.isImage(file)){ maps.tryCatchMapError(() -> {
ui.showErrorMessage("@editor.errorimage"); if(MapIO.isImage(file)){
return; ui.showErrorMessage("@editor.errorimage");
} return;
}
Map map = MapIO.createMap(file, true); Map map = MapIO.createMap(file, true);
//when you attempt to import a save, it will have no name, so generate one //when you attempt to import a save, it will have no name, so generate one
String name = map.tags.get("name", () -> { String name = map.tags.get("name", () -> {
String result = "unknown"; String result = "unknown";
int number = 0; int number = 0;
while(maps.byName(result + number++) != null) ; while(maps.byName(result + number++) != null) ;
return result + number; return result + number;
});
//this will never actually get called, but it remains just in case
if(name == null){
ui.showErrorMessage("@editor.errorname");
return;
}
Map conflict = maps.all().find(m -> m.name().equalsIgnoreCase(name));
if(conflict != null && !conflict.custom){
ui.showInfo(Core.bundle.format("editor.import.exists", name));
}else if(conflict != null){
ui.showConfirm("@confirm", Core.bundle.format("editor.overwrite.confirm", map.name()), () -> {
maps.tryCatchMapError(() -> {
maps.removeMap(conflict);
maps.importMap(map.file);
setup();
});
}); });
}else{
maps.importMap(map.file);
setup();
}
}); //this will never actually get called, but it remains just in case
if(name == null){
ui.showErrorMessage("@editor.errorname");
return;
}
Map conflict = maps.all().find(m -> m.name().equalsIgnoreCase(name));
if(conflict != null && !conflict.custom){
ui.showInfo(Core.bundle.format("editor.import.exists", name));
}else if(conflict != null){
ui.showConfirm("@confirm", Core.bundle.format("editor.overwrite.confirm", map.name()), () -> {
maps.tryCatchMapError(() -> {
maps.removeMap(conflict);
maps.importMap(map.file);
setup();
});
});
}else{
maps.importMap(map.file);
setup();
}
});
}
}); });
}); });
}).size(210f, 64f); }).size(210f, 64f);
@@ -16,7 +16,7 @@ import mindustry.ui.*;
import java.util.*; import java.util.*;
public class FileChooser extends BaseDialog{ public class FileChooserDialog extends BaseDialog{
private static final Fi homeDirectory = Core.files.absolute(Core.files.getExternalStoragePath()); private static final Fi homeDirectory = Core.files.absolute(Core.files.getExternalStoragePath());
static Fi lastDirectory = Core.files.absolute(Core.settings.getString("lastDirectory", homeDirectory.absolutePath())); static Fi lastDirectory = Core.files.absolute(Core.settings.getString("lastDirectory", homeDirectory.absolutePath()));
@@ -30,7 +30,7 @@ public class FileChooser extends BaseDialog{
private Cons<Fi> selectListener; private Cons<Fi> selectListener;
private boolean open; private boolean open;
public FileChooser(String title, Boolf<Fi> filter, boolean open, Cons<Fi> result){ public FileChooserDialog(String title, Boolf<Fi> filter, boolean open, Cons<Fi> result){
super(title); super(title);
setFillParent(true); setFillParent(true);
this.open = open; this.open = open;
+19 -15
View File
@@ -1,6 +1,7 @@
package mindustry.ui.dialogs; package mindustry.ui.dialogs;
import arc.*; import arc.*;
import arc.files.*;
import arc.graphics.*; import arc.graphics.*;
import arc.graphics.g2d.*; import arc.graphics.g2d.*;
import arc.scene.style.*; import arc.scene.style.*;
@@ -139,7 +140,7 @@ public class LoadDialog extends BaseDialog{
if(slot.hasExternalAssets() && !slot.isBeingPlayed()){ if(slot.hasExternalAssets() && !slot.isBeingPlayed()){
ui.showInfo("@save.export.needsload"); ui.showInfo("@save.export.needsload");
}else{ }else{
platform.export("save-" + slot.getName(), saveExtension, slot::exportFile); FileChooser.export(slot.getName(), saveExtension, slot::exportFile);
} }
}).right(); }).right();
@@ -196,24 +197,27 @@ public class LoadDialog extends BaseDialog{
public void addSetup(){ public void addSetup(){
buttons.button("@save.import", Icon.add, () -> { buttons.button("@save.import", Icon.add, () -> {
platform.showFileChooser(true, saveExtension, file -> { FileChooser.open("msav").submitMulti(files -> {
if(SaveIO.isSaveValid(file)){ for(Fi file : files){
var meta = SaveIO.getMeta(file); if(SaveIO.isSaveValid(file)){
var meta = SaveIO.getMeta(file);
if(meta.rules.sector != null){ if(meta.rules.sector != null){
ui.showErrorMessage("@save.nocampaign"); ui.showErrorMessage("@save.nocampaign");
}else{ }else{
try{ try{
control.saves.importSave(file); control.saves.importSave(file);
rebuild(); }catch(Exception e){
}catch(IOException e){ Log.err(e);
e.printStackTrace(); ui.showException("@save.import.fail", e);
ui.showException("@save.import.fail", e); }
} }
}else{
ui.showErrorMessage("@save.import.invalid");
} }
}else{
ui.showErrorMessage("@save.import.invalid");
} }
rebuild();
}); });
}).fillX().margin(10f); }).fillX().margin(10f);
} }
+11 -8
View File
@@ -191,15 +191,18 @@ public class ModsDialog extends BaseDialog{
t.button("@mod.import.file", Icon.file, bstyle, () -> { t.button("@mod.import.file", Icon.file, bstyle, () -> {
dialog.hide(); dialog.hide();
platform.showMultiFileChooser(file -> { FileChooser.open("zip", "jar").submitMulti(files -> {
try{ for(var file : files){
mods.importMod(file); try{
setup(); mods.importMod(file);
}catch(Exception e){ }catch(Exception e){
ui.showException(e.getMessage() != null && e.getMessage().toLowerCase(Locale.ROOT).contains("writable dex") ? "@error.moddex" : "", e); ui.showException(e.getMessage() != null && e.getMessage().toLowerCase(Locale.ROOT).contains("writable dex") ? "@error.moddex" : "", e);
Log.err(e); Log.err(e);
}
} }
}, "zip", "jar");
setup();
});
}).margin(12f); }).margin(12f);
t.row(); t.row();
@@ -1,6 +1,7 @@
package mindustry.ui.dialogs; package mindustry.ui.dialogs;
import arc.*; import arc.*;
import arc.files.*;
import arc.func.*; import arc.func.*;
import arc.graphics.*; import arc.graphics.*;
import arc.graphics.Texture.*; import arc.graphics.Texture.*;
@@ -241,19 +242,28 @@ public class SchematicsDialog extends BaseDialog{
} }
}).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null || !Core.app.getClipboardText().startsWith(schematicBaseStart)); }).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null || !Core.app.getClipboardText().startsWith(schematicBaseStart));
t.row(); t.row();
t.button("@schematic.importfile", Icon.download, style, () -> platform.showFileChooser(true, schematicExtension, file -> { t.button("@schematic.importfile", Icon.download, style, () -> FileChooser.open(schematicExtension).submitMulti(files -> {
dialog.hide(); dialog.hide();
try{ Schematic last = null;
Schematic s = Schematics.read(file);
s.removeSteamID(); for(Fi file : files){
schematics.add(s); try{
setup(); Schematic s = Schematics.read(file);
showInfo(s); s.removeSteamID();
checkTags(s); schematics.add(s);
}catch(Exception e){ checkTags(s);
ui.showException(e); last = s;
}catch(Exception e){
ui.showException(e);
}
} }
if(last != null){
showInfo(last);
}
setup();
})).marginLeft(12f); })).marginLeft(12f);
t.row(); t.row();
if(steam){ if(steam){
@@ -290,7 +300,7 @@ public class SchematicsDialog extends BaseDialog{
t.row(); t.row();
t.button("@schematic.exportfile", Icon.export, style, () -> { t.button("@schematic.exportfile", Icon.export, style, () -> {
dialog.hide(); dialog.hide();
platform.export(s.name(), schematicExtension, file -> Schematics.write(s, file)); FileChooser.export(s.name(), schematicExtension, file -> Schematics.write(s, file));
}).marginLeft(12f); }).marginLeft(12f);
}); });
}); });
@@ -249,30 +249,12 @@ public class SettingsMenuDialog extends BaseDialog{
t.row(); t.row();
t.button("@data.export", Icon.upload, style, () -> { t.button("@data.export", Icon.upload, style, () -> {
if(ios){ FileChooser.export("mindustry-data-export", "zip", this::exportData);
Fi file = Core.files.local("mindustry-data-export.zip");
try{
exportData(file);
}catch(Exception e){
ui.showException(e);
}
platform.shareFile(file);
}else{
platform.showFileChooser(false, "zip", file -> {
try{
exportData(file);
ui.showInfo("@data.exported");
}catch(Exception e){
e.printStackTrace();
ui.showException(e);
}
});
}
}).marginLeft(4); }).marginLeft(4);
t.row(); t.row();
t.button("@data.import", Icon.download, style, () -> ui.showConfirm("@confirm", "@data.import.confirm", () -> platform.showFileChooser(true, "zip", file -> { t.button("@data.import", Icon.download, style, () -> ui.showConfirm("@confirm", "@data.import.confirm", () -> FileChooser.open("zip").submit(file -> {
try{ try{
importData(file); importData(file);
control.saves.resetSave(); control.saves.resetSave();
@@ -281,7 +263,7 @@ public class SettingsMenuDialog extends BaseDialog{
}catch(IllegalArgumentException e){ }catch(IllegalArgumentException e){
ui.showErrorMessage("@data.invalid"); ui.showErrorMessage("@data.invalid");
}catch(Exception e){ }catch(Exception e){
e.printStackTrace(); Log.err(e);
if(e.getMessage() == null || !e.getMessage().contains("too short")){ if(e.getMessage() == null || !e.getMessage().contains("too short")){
ui.showException(e); ui.showException(e);
}else{ }else{
@@ -301,20 +283,7 @@ public class SettingsMenuDialog extends BaseDialog{
if(settings.getDataDirectory().child("crashes").list().length == 0 && !settings.getDataDirectory().child("last_log.txt").exists()){ if(settings.getDataDirectory().child("crashes").list().length == 0 && !settings.getDataDirectory().child("last_log.txt").exists()){
ui.showInfo("@crash.none"); ui.showInfo("@crash.none");
}else{ }else{
if(ios){ FileChooser.export("logs", "txt", file -> file.writeString(getLogs()));
Fi logs = tmpDirectory.child("logs.txt");
logs.writeString(getLogs());
platform.shareFile(logs);
}else{
platform.showFileChooser(false, "txt", file -> {
try{
file.writeBytes(getLogs().getBytes(Strings.utf8));
app.post(() -> ui.showInfo("@crash.exported"));
}catch(Throwable e){
ui.showException(e);
}
});
}
} }
}).marginLeft(4); }).marginLeft(4);
}); });
@@ -122,7 +122,7 @@ public class ConsoleFragment extends Table{
button(Icon.downOpen, Styles.cleari, () -> scrollPos = Mathf.clamp(scrollPos - 1, 0, Math.max(0, messages.size))).disabled(b -> scrollPos <= 0).size(s).padLeft(4f); button(Icon.downOpen, Styles.cleari, () -> scrollPos = Mathf.clamp(scrollPos - 1, 0, Math.max(0, messages.size))).disabled(b -> scrollPos <= 0).size(s).padLeft(4f);
button(Icon.fileText, Styles.cleari, () -> platform.showFileChooser(true, "js", file -> { button(Icon.fileText, Styles.cleari, () -> FileChooser.open("js").submit(file -> {
try{ try{
mods.getScripts().runConsole(file.readString()); mods.getScripts().runConsole(file.readString());
}catch(Exception e){ }catch(Exception e){
@@ -8,7 +8,6 @@ import arc.discord.*;
import arc.discord.DiscordRPC.*; import arc.discord.DiscordRPC.*;
import arc.filedialogs.*; import arc.filedialogs.*;
import arc.files.*; import arc.files.*;
import arc.func.*;
import arc.math.*; import arc.math.*;
import arc.profiling.*; import arc.profiling.*;
import arc.struct.*; import arc.struct.*;
@@ -27,6 +26,8 @@ import mindustry.net.*;
import mindustry.net.Net.*; import mindustry.net.Net.*;
import mindustry.service.*; import mindustry.service.*;
import mindustry.type.*; import mindustry.type.*;
import mindustry.ui.*;
import mindustry.ui.FileChooser.*;
import mindustry.ui.dialogs.*; import mindustry.ui.dialogs.*;
import java.io.*; import java.io.*;
@@ -327,30 +328,11 @@ public class DesktopLauncher extends ClientLauncher{
} }
@Override @Override
public void showFileChooser(boolean open, String title, String extension, Cons<Fi> cons){ public void showFileChooser(FileChooserParams params){
showNativeFileChooser(title, open, cons, extension);
}
@Override
public void showMultiFileChooser(Cons<Fi> cons, String... extensions){
showNativeFileChooser("@open", true, cons, extensions);
}
void showNativeFileChooser(String title, boolean open, Cons<Fi> cons, String... shownExtensions){
String formatted = (title.startsWith("@") ? Core.bundle.get(title.substring(1)) : title).replaceAll("\"", "'");
//this should never happen unless someone is being dumb with the parameters
String[] ext = shownExtensions == null || shownExtensions.length == 0 ? new String[]{""} : shownExtensions;
if(OS.isLinux){
showZenity(open, formatted, shownExtensions, cons, () -> Platform.defaultFileDialog(open, title, ext[0], cons));
return;
}
//native file dialog
Threads.daemon(() -> { Threads.daemon(() -> {
try{ try{
FileDialogs.loadNatives(); FileDialogs.loadNatives();
var ext = params.extensions;
String result; String result;
String[] patterns = new String[ext.length]; String[] patterns = new String[ext.length];
@@ -359,14 +341,14 @@ public class DesktopLauncher extends ClientLauncher{
} }
//on MacOS, .msav is not properly recognized until I put garbage into the array? //on MacOS, .msav is not properly recognized until I put garbage into the array?
if(patterns.length == 1 && OS.isMac && open){ if(patterns.length == 1 && OS.isMac && params.open){
patterns = new String[]{"", "*." + ext[0]}; patterns = new String[]{"", "*." + ext[0]};
} }
if(open){ if(params.open){
result = FileDialogs.openFileDialog(formatted, FileChooser.getLastDirectory().absolutePath(), patterns, "." + ext[0] + " files", false); result = FileDialogs.openFileDialog(params.title, FileChooserDialog.getLastDirectory().absolutePath() + "/", patterns, "." + ext[0] + " files", params.allowMultiple);
}else{ }else{
result = FileDialogs.saveFileDialog(formatted, FileChooser.getLastDirectory().child("file." + ext[0]).absolutePath(), patterns, "." + ext[0] + " files"); result = FileDialogs.saveFileDialog(params.title, FileChooserDialog.getLastDirectory().child(params.fileName).absolutePath(), patterns, "." + ext[0] + " files");
} }
if(result == null) return; if(result == null) return;
@@ -378,86 +360,32 @@ public class DesktopLauncher extends ClientLauncher{
//cancelled selection, ignore result //cancelled selection, ignore result
if(result.isEmpty() || result.equals("\n")) return; if(result.isEmpty() || result.equals("\n")) return;
if(result.endsWith("\n")) result = result.substring(0, result.length() - 1); if(result.endsWith("\n")) result = result.substring(0, result.length() - 1);
if(result.contains("\n")) throw new IOException("invalid input: \"" + result + "\"");
Fi file = Core.files.absolute(result); Fi[] resultFiles = Seq.with(result.split("\\|")).map(Core.files::absolute).toArray(Fi.class);
if(result.isEmpty()) return;
Core.app.post(() -> { Core.app.post(() -> {
FileChooser.setLastDirectory(file.isDirectory() ? file : file.parent()); FileChooserDialog.setLastDirectory(resultFiles[0].isDirectory() ? resultFiles[0] : resultFiles[0].parent());
if(!open){ if(!params.open){
cons.get(file.parent().child(file.nameWithoutExtension() + "." + ext[0])); Fi single = resultFiles[0];
//fix extension to match filters
if(!Structs.contains(params.extensions, single::extEquals)){
single = single.parent().child(single.nameWithoutExtension() + "." + ext[0]);
}
params.handleChooseResult(single);
}else{ }else{
cons.get(file); params.handleChooseResult(resultFiles);
} }
}); });
}catch(Throwable error){ }catch(Throwable error){
Log.err("Failure to execute native file chooser", error); Log.err("Failed to execute native file chooser", error);
Core.app.post(() -> { Core.app.post(() -> FileChooser.showFallbackFileChooser(params));
if(ext.length > 1){
showMultiFileChooser(cons, ext);
}else{
Platform.defaultFileDialog(open, formatted, ext[0], cons);
}
});
} }
}); });
} }
/** attempt to use the native file picker with zenity, or runs the fallback Runnable if the operation fails */
static void showZenity(boolean open, String title, String[] extensions, Cons<Fi> cons, Runnable fallback){
Threads.daemon(() -> {
try{
String formatted = (title.startsWith("@") ? Core.bundle.get(title.substring(1)) : title).replaceAll("\"", "'");
String last = FileChooser.getLastDirectory().absolutePath();
if(!last.endsWith("/")) last += "/";
//zenity doesn't support filtering by extension
Seq<String> args = Seq.with("zenity",
"--file-selection",
"--title=" + formatted,
"--filename=" + last,
"--confirm-overwrite",
"--file-filter=" + Seq.with(extensions).toString(" ", s -> "*." + s),
"--file-filter=All files | *" //allow anything if the user wants
);
if(!open){
args.add("--save");
}
String result = OS.exec(args.toArray(String.class));
//first line.
if(result.length() > 1 && result.contains("\n")){
result = result.split("\n")[0];
}
//cancelled selection, ignore result
if(result.isEmpty() || result.equals("\n")) return;
if(result.endsWith("\n")) result = result.substring(0, result.length() - 1);
if(result.contains("\n")) throw new IOException("invalid input: \"" + result + "\"");
Fi file = Core.files.absolute(result);
Core.app.post(() -> {
FileChooser.setLastDirectory(file.isDirectory() ? file : file.parent());
if(!open){
cons.get(file.parent().child(file.nameWithoutExtension() + "." + extensions[0]));
}else{
cons.get(file);
}
});
}catch(Exception e){
Log.err(e);
Log.warn("zenity not found, using non-native file dialog. Consider installing `zenity` for native file dialogs.");
Core.app.post(fallback);
}
});
}
@Override @Override
public Seq<Fi> getWorkshopContent(Class<? extends Publishable> type){ public Seq<Fi> getWorkshopContent(Class<? extends Publishable> type){
return !steam ? super.getWorkshopContent(type) : SVars.workshop.getWorkshopFiles(type); return !steam ? super.getWorkshopContent(type) : SVars.workshop.getWorkshopFiles(type);
+1 -1
View File
@@ -26,4 +26,4 @@ org.gradle.caching=true
org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.socketTimeout=100000
org.gradle.internal.http.connectionTimeout=100000 org.gradle.internal.http.connectionTimeout=100000
android.enableR8.fullMode=false android.enableR8.fullMode=false
archash=aaa6d755e7 archash=62887bdcae
+30 -33
View File
@@ -4,8 +4,8 @@ import arc.*;
import arc.Input.*; import arc.Input.*;
import arc.backend.robovm.*; import arc.backend.robovm.*;
import arc.files.*; import arc.files.*;
import arc.func.*;
import arc.scene.ui.layout.*; import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*; import arc.util.*;
import arc.util.io.*; import arc.util.io.*;
import mindustry.*; import mindustry.*;
@@ -13,6 +13,7 @@ import mindustry.game.EventType.*;
import mindustry.game.Saves.*; import mindustry.game.Saves.*;
import mindustry.io.*; import mindustry.io.*;
import mindustry.net.*; import mindustry.net.*;
import mindustry.ui.FileChooser.*;
import mindustry.ui.*; import mindustry.ui.*;
import org.robovm.apple.coregraphics.*; import org.robovm.apple.coregraphics.*;
import org.robovm.apple.foundation.*; import org.robovm.apple.foundation.*;
@@ -44,16 +45,16 @@ public class IOSLauncher extends IOSApplication.Delegate{
return new IOSApplication(new ClientLauncher(){ return new IOSApplication(new ClientLauncher(){
@Override @Override
public void showFileChooser(boolean open, String titleIgn, String extension, Cons<Fi> cons){ public void showFileChooser(FileChooserParams params){
if(!open){ //when exporting, just share it. if(!params.open){ //when exporting, just share it.
//ask for export name //ask for export name
Core.input.getTextInput(new TextInput(){{ Core.input.getTextInput(new TextInput(){{
title = Core.bundle.get("filename"); title = Core.bundle.get("filename");
accepted = name -> { accepted = name -> {
try{ try{
//write result //write result
Fi result = tmpDirectory.child(name + "." + extension); Fi result = tmpDirectory.child(name + "." + params.extensions[0]);
cons.get(result); params.handleChooseResult(result);
//import the document //import the document
shareFile(result); shareFile(result);
@@ -80,33 +81,35 @@ public class IOSLauncher extends IOSApplication.Delegate{
@Override @Override
public void didPickDocumentsAtURLs(UIDocumentBrowserViewController controller, NSArray<NSURL> documentURLs){ public void didPickDocumentsAtURLs(UIDocumentBrowserViewController controller, NSArray<NSURL> documentURLs){
if(documentURLs.size() < 1) return; if(documentURLs.isEmpty()) return;
NSURL url = documentURLs.first(); Seq<Fi> results = new Seq<>();
NSFileCoordinator coord = new NSFileCoordinator(null); NSFileCoordinator coord = new NSFileCoordinator(null);
url.startAccessingSecurityScopedResource(); for(NSURL url : documentURLs){
try{ url.startAccessingSecurityScopedResource();
coord.coordinateReadingItem(url, NSFileCoordinatorReadingOptions.ForUploading, result -> { try{
coord.coordinateReadingItem(url, NSFileCoordinatorReadingOptions.ForUploading, result -> {
Fi src = Core.files.absolute(result.getAbsoluteURL().getPath()); Fi src = Core.files.absolute(result.getAbsoluteURL().getPath());
Fi dst = Core.files.absolute(getDocumentsDirectory()).child(src.name()); Fi dst = Core.files.absolute(getDocumentsDirectory()).child(src.name());
src.copyTo(dst); src.copyTo(dst);
results.add(dst);
Core.app.post(() -> {
try{
cons.get(dst);
}catch(Throwable t){
ui.showException(t);
}
}); });
}); }catch(Throwable e){
}catch(Throwable e){ ui.showException(e);
ui.showException(e); }
url.stopAccessingSecurityScopedResource();
cont.dismissViewController(true, () -> {});
} }
url.stopAccessingSecurityScopedResource(); Core.app.post(() -> {
try{
cont.dismissViewController(true, () -> {}); params.handleChooseResult(results.toArray(Fi.class));
}catch(Throwable t){
ui.showException(t);
}
});
} }
@Override @Override
@@ -137,12 +140,6 @@ public class IOSLauncher extends IOSApplication.Delegate{
UIApplication.getSharedApplication().getKeyWindow().getRootViewController().presentViewController(cont, true, () -> {}); UIApplication.getSharedApplication().getKeyWindow().getRootViewController().presentViewController(cont, true, () -> {});
} }
@Override
public void showMultiFileChooser(Cons<Fi> cons, String... extensions){
showFileChooser(true, extensions[0], cons);
}
@Override @Override
public Context getScriptContext(){ public Context getScriptContext(){
Context context = Context.getCurrentContext(); Context context = Context.getCurrentContext();