full fakefx & game and setting module

This commit is contained in:
Tungstend 2022-11-03 16:09:42 +08:00
parent ca72e310eb
commit 1898530f2f
396 changed files with 49321 additions and 485 deletions

View File

@ -33,9 +33,10 @@ dependencies {
implementation project(path: ':FCLCore') implementation project(path: ':FCLCore')
implementation project(path: ':FCLLibrary') implementation project(path: ':FCLLibrary')
implementation project(path: ':FCLauncher') implementation project(path: ':FCLauncher')
implementation 'org.nanohttpd:nanohttpd:2.3.1'
implementation 'org.apache.commons:commons-compress:1.21' implementation 'org.apache.commons:commons-compress:1.21'
implementation 'org.tukaani:xz:1.8' implementation 'org.tukaani:xz:1.8'
implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.google.code.gson:gson:2.10'
implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.7.0' implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,30 @@
package com.tungsten.fcl.game;
import com.tungsten.fclcore.download.DefaultCacheRepository;
import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty;
import com.tungsten.fclcore.fakefx.beans.property.StringProperty;
import java.nio.file.Paths;
public class FCLCacheRepository extends DefaultCacheRepository {
private final StringProperty directory = new SimpleStringProperty();
public FCLCacheRepository() {
directory.addListener((a, b, t) -> changeDirectory(Paths.get(t)));
}
public String getDirectory() {
return directory.get();
}
public StringProperty directoryProperty() {
return directory;
}
public void setDirectory(String directory) {
this.directory.set(directory);
}
public static final FCLCacheRepository REPOSITORY = new FCLCacheRepository();
}

View File

@ -0,0 +1,80 @@
package com.tungsten.fcl.game;
import android.content.Context;
import android.view.Surface;
import com.tungsten.fcl.R;
import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclauncher.bridge.FCLBridge;
import com.tungsten.fclauncher.bridge.FCLBridgeCallback;
import com.tungsten.fclcore.auth.AuthInfo;
import com.tungsten.fclcore.game.GameRepository;
import com.tungsten.fclcore.game.LaunchOptions;
import com.tungsten.fclcore.game.Version;
import com.tungsten.fclcore.launch.DefaultLauncher;
import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.io.FileUtils;
import com.tungsten.fcllibrary.component.LocaleUtils;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
public final class FCLGameLauncher extends DefaultLauncher {
public FCLGameLauncher(Context context, Surface surface, GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options) {
super(context, surface, repository, version, authInfo, options);
}
public FCLGameLauncher(Context context, Surface surface, GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, FCLBridgeCallback callback) {
super(context, surface, repository, version, authInfo, options, callback);
}
@Override
protected Map<String, String> getConfigurations() {
Map<String, String> res = super.getConfigurations();
res.put("${launcher_name}", FCLPath.CONTEXT.getString(R.string.app_name));
res.put("${launcher_version}", FCLPath.CONTEXT.getString(R.string.app_version));
return res;
}
private void generateOptionsTxt() {
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
File configFolder = new File(repository.getRunDirectory(version.getId()), "config");
if (optionsFile.exists())
return;
if (configFolder.isDirectory())
if (findFiles(configFolder, "options.txt"))
return;
try {
// TODO: Dirty implementation here
if (LocaleUtils.getSystemLocale() == Locale.CHINA)
FileUtils.writeText(optionsFile, "lang:zh_CN\nforceUnicodeFont:true\n");
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to generate options.txt", e);
}
}
private boolean findFiles(File folder, String fileName) {
File[] fs = folder.listFiles();
if (fs != null) {
for (File f : fs) {
if (f.isDirectory())
if (f.listFiles((dir, name) -> name.equals(fileName)) != null)
return true;
if (f.getName().equals(fileName))
return true;
}
}
return false;
}
@Override
public FCLBridge launch() throws IOException, InterruptedException {
generateOptionsTxt();
return super.launch();
}
}

View File

@ -0,0 +1,427 @@
package com.tungsten.fcl.game;
import static com.tungsten.fclcore.util.Logging.LOG;
import android.annotation.SuppressLint;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.tungsten.fcl.R;
import com.tungsten.fcl.setting.Profile;
import com.tungsten.fcl.setting.VersionSetting;
import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.download.LibraryAnalyzer;
import com.tungsten.fclcore.event.Event;
import com.tungsten.fclcore.event.EventManager;
import com.tungsten.fclcore.game.DefaultGameRepository;
import com.tungsten.fclcore.game.GameDirectoryType;
import com.tungsten.fclcore.game.JavaVersion;
import com.tungsten.fclcore.game.LaunchOptions;
import com.tungsten.fclcore.game.Version;
import com.tungsten.fclcore.mod.Modpack;
import com.tungsten.fclcore.mod.ModpackConfiguration;
import com.tungsten.fclcore.mod.ModpackProvider;
import com.tungsten.fclcore.util.Lang;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.FileUtils;
import com.tungsten.fclcore.util.platform.MemoryUtils;
import com.tungsten.fclcore.util.platform.OperatingSystem;
import com.tungsten.fclcore.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Stream;
public class FCLGameRepository extends DefaultGameRepository {
private final Profile profile;
// local version settings
private final Map<String, VersionSetting> localVersionSettings = new HashMap<>();
private final Set<String> beingModpackVersions = new HashSet<>();
public final EventManager<Event> onVersionIconChanged = new EventManager<>();
public FCLGameRepository(Profile profile, File baseDirectory) {
super(baseDirectory);
this.profile = profile;
}
public Profile getProfile() {
return profile;
}
@Override
public GameDirectoryType getGameDirectoryType(String id) {
if (beingModpackVersions.contains(id) || isModpack(id)) {
return GameDirectoryType.VERSION_FOLDER;
} else {
return getVersionSetting(id).getGameDirType();
}
}
@Override
public File getRunDirectory(String id) {
switch (getGameDirectoryType(id)) {
case VERSION_FOLDER:
return getVersionRoot(id);
case ROOT_FOLDER:
return super.getRunDirectory(id);
default:
throw new Error();
}
}
public Stream<Version> getDisplayVersions() {
return getVersions().stream()
.filter(v -> !v.isHidden())
.sorted(Comparator.comparing((Version v) -> v.getReleaseTime() == null ? new Date(0L) : v.getReleaseTime())
.thenComparing(v -> VersionNumber.asVersion(v.getId())));
}
@Override
protected void refreshVersionsImpl() {
localVersionSettings.clear();
super.refreshVersionsImpl();
versions.keySet().forEach(this::loadLocalVersionSetting);
versions.keySet().forEach(version -> {
if (isModpack(version)) {
specializeVersionSetting(version);
}
});
try {
File file = new File(getBaseDirectory(), "launcher_profiles.json");
if (!file.exists() && !versions.isEmpty())
FileUtils.writeText(file, PROFILE);
} catch (IOException ex) {
LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
}
// https://github.com/huanghongxun/HMCL/issues/938
System.gc();
}
public void changeDirectory(File newDirectory) {
setBaseDirectory(newDirectory);
refreshVersionsAsync().start();
}
private void clean(File directory) throws IOException {
FileUtils.deleteDirectory(new File(directory, "crash-reports"));
FileUtils.deleteDirectory(new File(directory, "logs"));
}
public void clean(String id) throws IOException {
clean(getBaseDirectory());
clean(getRunDirectory(id));
}
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
Path srcDir = getVersionRoot(srcId).toPath();
Path dstDir = getVersionRoot(dstId).toPath();
Version fromVersion = getVersion(srcId);
if (Files.exists(dstDir)) throw new IOException("Version exists");
FileUtils.copyDirectory(srcDir, dstDir);
Path fromJson = dstDir.resolve(srcId + ".json");
Path fromJar = dstDir.resolve(srcId + ".jar");
Path toJson = dstDir.resolve(dstId + ".json");
Path toJar = dstDir.resolve(dstId + ".jar");
if (Files.exists(fromJar)) {
Files.move(fromJar, toJar);
}
Files.move(fromJson, toJson);
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
oldVersionSetting.setUsesGlobal(false);
oldVersionSetting.setGameDirType(GameDirectoryType.VERSION_FOLDER);
VersionSetting newVersionSetting = initLocalVersionSetting(dstId, oldVersionSetting);
saveVersionSetting(dstId);
File srcGameDir = getRunDirectory(srcId);
File dstGameDir = getRunDirectory(dstId);
List<String> blackList = new ArrayList<>(Arrays.asList(
"regex:(.*?)\\.log",
"usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
"backup", "pack.json", "launcher.jar", "cache", // HMCL
".curseclient", // Curse
".fabric", ".mixin.out", // Fabric
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
"downloads", // Curse
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data", "CustomSkinLoader/caches" // Mods
));
blackList.add(srcId + ".jar");
blackList.add(srcId + ".json");
if (!copySaves)
blackList.add("saves");
if (originalGameDirType != GameDirectoryType.VERSION_FOLDER)
FileUtils.copyDirectory(srcGameDir.toPath(), dstGameDir.toPath(), path -> Modpack.acceptFile(path, blackList, null));
}
private File getLocalVersionSettingFile(String id) {
return new File(getVersionRoot(id), "hmclversion.cfg");
}
private void loadLocalVersionSetting(String id) {
File file = getLocalVersionSettingFile(id);
if (file.exists())
try {
VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class);
initLocalVersionSetting(id, versionSetting);
} catch (Exception ex) {
// If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
initLocalVersionSetting(id, new VersionSetting());
}
}
/**
* Create new version setting if version id has no version setting.
* @param id the version id.
* @return new version setting, null if given version does not exist.
*/
public VersionSetting createLocalVersionSetting(String id) {
if (!hasVersion(id))
return null;
if (localVersionSettings.containsKey(id))
return getLocalVersionSetting(id);
else
return initLocalVersionSetting(id, new VersionSetting());
}
private VersionSetting initLocalVersionSetting(String id, VersionSetting vs) {
localVersionSettings.put(id, vs);
vs.addPropertyChangedListener(a -> saveVersionSetting(id));
return vs;
}
/**
* Get the version setting for version id.
*
* @param id version id
*
* @return corresponding version setting, null if the version has no its own version setting.
*/
@Nullable
public VersionSetting getLocalVersionSetting(String id) {
if (!localVersionSettings.containsKey(id))
loadLocalVersionSetting(id);
VersionSetting setting = localVersionSettings.get(id);
if (setting != null && isModpack(id))
setting.setGameDirType(GameDirectoryType.VERSION_FOLDER);
return setting;
}
@Nullable
public VersionSetting getLocalVersionSettingOrCreate(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null) {
vs = createLocalVersionSetting(id);
}
return vs;
}
public VersionSetting getVersionSetting(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
profile.getGlobal().setGlobal(true); // always keep global.isGlobal = true
profile.getGlobal().setUsesGlobal(true);
return profile.getGlobal();
} else
return vs;
}
public File getVersionIconFile(String id) {
return new File(getVersionRoot(id), "icon.png");
}
@SuppressLint("UseCompatLoadingForDrawables")
public Drawable getVersionIconImage(String id) {
if (id == null || !isLoaded())
return FCLPath.CONTEXT.getDrawable(R.drawable.img_grass);
VersionSetting vs = getLocalVersionSettingOrCreate(id);
Version version = getVersion(id).resolve(this);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return BitmapDrawable.createFromPath(iconFile.getAbsolutePath());
else if (LibraryAnalyzer.analyze(version).has(LibraryAnalyzer.LibraryType.FORGE) || LibraryAnalyzer.analyze(version).has(LibraryAnalyzer.LibraryType.BOOTSTRAP_LAUNCHER))
return FCLPath.CONTEXT.getDrawable(R.drawable.img_forge);
else if (LibraryAnalyzer.analyze(version).has(LibraryAnalyzer.LibraryType.LITELOADER))
return FCLPath.CONTEXT.getDrawable(R.drawable.img_chicken);
else if (LibraryAnalyzer.analyze(version).has(LibraryAnalyzer.LibraryType.OPTIFINE))
return FCLPath.CONTEXT.getDrawable(R.drawable.img_command);
else if (LibraryAnalyzer.analyze(version).has(LibraryAnalyzer.LibraryType.FABRIC))
return FCLPath.CONTEXT.getDrawable(R.drawable.img_fabric);
else if (LibraryAnalyzer.analyze(version).has(LibraryAnalyzer.LibraryType.QUILT))
return FCLPath.CONTEXT.getDrawable(R.drawable.img_quilt);
else
return FCLPath.CONTEXT.getDrawable(R.drawable.img_grass);
}
public boolean saveVersionSetting(String id) {
if (!localVersionSettings.containsKey(id))
return false;
File file = getLocalVersionSettingFile(id);
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
return false;
try {
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
return true;
} catch (IOException e) {
LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
return false;
}
}
/**
* Make version use self version settings instead of the global one.
* @param id the version id.
* @return specialized version setting, null if given version does not exist.
*/
public VersionSetting specializeVersionSetting(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null)
vs = createLocalVersionSetting(id);
if (vs == null)
return null;
vs.setUsesGlobal(false);
return vs;
}
public void globalizeVersionSetting(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs != null)
vs.setUsesGlobal(true);
}
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir, List<String> javaAgents, boolean makeLaunchScript) {
VersionSetting vs = getVersionSetting(version);
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionType(FCLPath.CONTEXT.getString(R.string.app_name))
.setVersionName(version)
.setProfileName(FCLPath.CONTEXT.getString(R.string.app_name))
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory((int)(getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
MemoryUtils.getFreeDeviceMemory(FCLPath.CONTEXT),
vs.isAutoMemory()
) / 1024 / 1024))
.setMinMemory(vs.getMinMemory())
.setMetaspace(Lang.toIntOrNull(vs.getPermSize()))
.setWidth(vs.getWidth())
.setHeight(vs.getHeight())
.setFullscreen(vs.isFullscreen())
.setServerIp(vs.getServerIp())
.setProcessPriority(vs.getProcessPriority())
.setJavaAgents(javaAgents);
File json = getModpackConfiguration(version);
if (json.exists()) {
try {
String jsonText = FileUtils.readText(json);
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
if (provider != null) provider.injectLaunchOptions(jsonText, builder);
} catch (IOException | JsonParseException e) {
e.printStackTrace();
}
}
return builder.create();
}
@Override
public File getModpackConfiguration(String version) {
return new File(getVersionRoot(version), "modpack.cfg");
}
public void markVersionAsModpack(String id) {
beingModpackVersions.add(id);
}
public void undoMark(String id) {
beingModpackVersions.remove(id);
}
public void markVersionLaunchedAbnormally(String id) {
try {
Files.createFile(getVersionRoot(id).toPath().resolve(".abnormal"));
} catch (IOException ignored) {
}
}
public boolean unmarkVersionLaunchedAbnormally(String id) {
File file = new File(getVersionRoot(id), ".abnormal");
boolean result = file.isFile();
file.delete();
return result;
}
private static final Gson GSON = new GsonBuilder()
.setPrettyPrinting()
.create();
private static final String PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}";
// These version ids are forbidden because they may conflict with modpack configuration filenames
private static final Set<String> FORBIDDEN_VERSION_IDS = new HashSet<>(Arrays.asList(
"modpack", "minecraftinstance", "manifest"));
public static boolean isValidVersionId(String id) {
if (FORBIDDEN_VERSION_IDS.contains(id))
return false;
return OperatingSystem.isNameValid(id);
}
/**
* Returns true if the given version id conflicts with an existing version.
*/
public boolean versionIdConflicts(String id) {
return versions.containsKey(id);
}
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
if (auto) {
available -= 256 * 1024 * 1024; // Reserve 256MB memory for off-heap memory and HMCL itself
if (available <= 0) {
return minimum;
}
final long threshold = 8L * 1024 * 1024 * 1024;
final long suggested = Math.min(available <= threshold
? (long) (available * 0.8)
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
32736L * 1024 * 1024); // Limit the maximum suggested memory to ensure that compressed oops are available
return Math.max(minimum, suggested);
} else {
return minimum;
}
}
}

View File

@ -0,0 +1,92 @@
package com.tungsten.fcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import com.tungsten.fcl.setting.Profile;
import com.tungsten.fclcore.download.DefaultDependencyManager;
import com.tungsten.fclcore.download.LibraryAnalyzer;
import com.tungsten.fclcore.game.Version;
import com.tungsten.fclcore.mod.MinecraftInstanceTask;
import com.tungsten.fclcore.mod.Modpack;
import com.tungsten.fclcore.mod.ModpackConfiguration;
import com.tungsten.fclcore.mod.ModpackInstallTask;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.CompressingUtils;
import com.tungsten.fclcore.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class HMCLModpackInstallTask extends Task<Void> {
private final File zipFile;
private final String name;
private final FCLGameRepository repository;
private final DefaultDependencyManager dependency;
private final Modpack modpack;
private final List<Task<?>> dependencies = new ArrayList<>(1);
private final List<Task<?>> dependents = new ArrayList<>(4);
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
dependency = profile.getDependency();
repository = profile.getRepository();
this.zipFile = zipFile;
this.name = name;
this.modpack = modpack;
File run = repository.getRunDirectory(name);
File json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && !json.exists())
throw new IllegalArgumentException("Version " + name + " already exists");
dependents.add(dependency.gameBuilder().name(name).gameVersion(modpack.getGameVersion()).buildAsync());
onDone().register(event -> {
if (event.isFailed()) repository.removeVersionFromDisk(name);
});
ModpackConfiguration<Modpack> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() {
}.getType());
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
}
} catch (JsonParseException | IOException ignore) {
}
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/minecraft"), it -> !"pack.json".equals(it), config));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
}
@Override
public List<Task<?>> getDependencies() {
return dependencies;
}
@Override
public List<Task<?>> getDependents() {
return dependents;
}
@Override
public void execute() throws Exception {
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
Version originalVersion = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion);
Task<Version> libraryTask = Task.supplyAsync(() -> originalVersion);
// reinstall libraries
// libraries of Forge and OptiFine should be obtained by installation.
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
if (LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(mark.getLibraryId()))
continue;
libraryTask = libraryTask.thenComposeAsync(version -> dependency.installLibraryAsync(modpack.getGameVersion(), version, mark.getLibraryId(), mark.getLibraryVersion()));
}
dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync));
}
}

View File

@ -0,0 +1,15 @@
package com.tungsten.fcl.game;
import com.tungsten.fclcore.mod.ModpackManifest;
import com.tungsten.fclcore.mod.ModpackProvider;
public final class HMCLModpackManifest implements ModpackManifest {
public static final HMCLModpackManifest INSTANCE = new HMCLModpackManifest();
private HMCLModpackManifest() {}
@Override
public ModpackProvider getProvider() {
return HMCLModpackProvider.INSTANCE;
}
}

View File

@ -0,0 +1,74 @@
package com.tungsten.fcl.game;
import com.google.gson.JsonParseException;
import com.tungsten.fcl.setting.Profile;
import com.tungsten.fclcore.download.DefaultDependencyManager;
import com.tungsten.fclcore.game.Version;
import com.tungsten.fclcore.mod.MismatchedModpackTypeException;
import com.tungsten.fclcore.mod.Modpack;
import com.tungsten.fclcore.mod.ModpackProvider;
import com.tungsten.fclcore.mod.ModpackUpdateTask;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.CompressingUtils;
import org.apache.commons.compress.archivers.zip.ZipFile;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
public final class HMCLModpackProvider implements ModpackProvider {
public static final HMCLModpackProvider INSTANCE = new HMCLModpackProvider();
@Override
public String getName() {
return "HMCL";
}
@Override
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
return null;
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
if (!(dependencyManager.getGameRepository() instanceof FCLGameRepository)) {
throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository");
}
FCLGameRepository repository = (FCLGameRepository) dependencyManager.getGameRepository();
Profile profile = repository.getProfile();
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
}
@Override
public Modpack readManifest(ZipFile file, Path path, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json");
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
if (game.getJar() == null)
if (StringUtils.isBlank(manifest.getVersion()))
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
else
manifest.setManifest(HMCLModpackManifest.INSTANCE);
else
manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar());
return manifest;
}
private static class HMCLModpack extends Modpack {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new HMCLModpackInstallTask(((FCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
}
}
}

View File

@ -0,0 +1,23 @@
package com.tungsten.fcl.game;
import android.content.Context;
import com.tungsten.fcl.R;
public enum LoadingState {
DEPENDENCIES(R.string.launch_state_dependencies),
MODS(R.string.launch_state_modpack),
LOGGING_IN(R.string.launch_state_logging_in),
LAUNCHING(R.string.launch_state_waiting_launching),
DONE(R.string.launch_state_done);
private final int id;
LoadingState(int id) {
this.id = id;
}
public String getLocalizedMessage(Context context) {
return context.getString(id);
}
}

View File

@ -0,0 +1,71 @@
package com.tungsten.fcl.game;
import com.tungsten.fcl.util.ModTranslations;
import com.tungsten.fclcore.mod.LocalModFile;
import com.tungsten.fclcore.mod.RemoteMod;
import com.tungsten.fclcore.mod.RemoteModRepository;
import com.tungsten.fclcore.util.StringUtils;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
public abstract class LocalizedRemoteModRepository implements RemoteModRepository {
protected abstract RemoteModRepository getBackedRemoteModRepository();
@Override
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
String newSearchFilter;
if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) {
ModTranslations modTranslations = ModTranslations.getTranslationsByRepositoryType(getType());
List<ModTranslations.Mod> mods = modTranslations.searchMod(searchFilter);
List<String> searchFilters = new ArrayList<>();
int count = 0;
for (ModTranslations.Mod mod : mods) {
String englishName = mod.getName();
if (StringUtils.isNotBlank(mod.getSubname())) {
englishName = mod.getSubname();
}
searchFilters.add(englishName);
count++;
if (count >= 3) break;
}
newSearchFilter = String.join(" ", searchFilters);
} else {
newSearchFilter = searchFilter;
}
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort, sortOrder);
}
@Override
public Stream<Category> getCategories() throws IOException {
return getBackedRemoteModRepository().getCategories();
}
@Override
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionByLocalFile(localModFile, file);
}
@Override
public RemoteMod getModById(String id) throws IOException {
return getBackedRemoteModRepository().getModById(id);
}
@Override
public RemoteMod.File getModFile(String modId, String fileId) throws IOException {
return getBackedRemoteModRepository().getModFile(modId, fileId);
}
@Override
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionsById(id);
}
}

View File

@ -0,0 +1,64 @@
package com.tungsten.fcl.game;
import com.tungsten.fclcore.game.DefaultGameRepository;
import com.tungsten.fclcore.game.Version;
import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.io.Zipper;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public final class LogExporter {
private LogExporter() {
}
public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) {
Path runDirectory = gameRepository.getRunDirectory(versionId).toPath();
Path baseDirectory = gameRepository.getBaseDirectory().toPath();
List<String> versions = new ArrayList<>();
String currentVersionId = versionId;
HashSet<String> resolvedSoFar = new HashSet<>();
while (true) {
if (resolvedSoFar.contains(currentVersionId)) break;
resolvedSoFar.add(currentVersionId);
Version currentVersion = gameRepository.getVersion(currentVersionId);
versions.add(currentVersionId);
if (StringUtils.isNotBlank(currentVersion.getInheritsFrom())) {
currentVersionId = currentVersion.getInheritsFrom();
} else {
break;
}
}
return CompletableFuture.runAsync(() -> {
try (Zipper zipper = new Zipper(zipFile)) {
if (Files.exists(runDirectory.resolve("logs").resolve("debug.log"))) {
zipper.putFile(runDirectory.resolve("logs").resolve("debug.log"), "debug.log");
}
if (Files.exists(runDirectory.resolve("logs").resolve("latest.log"))) {
zipper.putFile(runDirectory.resolve("logs").resolve("latest.log"), "latest.log");
}
zipper.putTextFile(Logging.getLogs(), "fcl.log");
zipper.putTextFile(logs, "minecraft.log");
for (String id : versions) {
Path versionJson = baseDirectory.resolve("versions").resolve(id).resolve(id + ".json");
if (Files.exists(versionJson)) {
zipper.putFile(versionJson, id + ".json");
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}

View File

@ -0,0 +1,15 @@
package com.tungsten.fcl.game;
import java.nio.file.Path;
public class ManuallyCreatedModpackException extends Exception {
private final Path path;
public ManuallyCreatedModpackException(Path path) {
this.path = path;
}
public Path getPath() {
return path;
}
}

View File

@ -0,0 +1,44 @@
package com.tungsten.fcl.game;
import com.tungsten.fcl.setting.Profile;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.io.CompressingUtils;
import com.tungsten.fclcore.util.io.Unzipper;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
public class ManuallyCreatedModpackInstallTask extends Task<Path> {
private final Profile profile;
private final Path zipFile;
private final Charset charset;
private final String name;
public ManuallyCreatedModpackInstallTask(Profile profile, Path zipFile, Charset charset, String name) {
this.profile = profile;
this.zipFile = zipFile;
this.charset = charset;
this.name = name;
}
@Override
public void execute() throws Exception {
Path subdirectory;
try (FileSystem fs = CompressingUtils.readonly(zipFile).setEncoding(charset).build()) {
subdirectory = ModpackHelper.findMinecraftDirectoryInManuallyCreatedModpack(zipFile.toString(), fs);
}
Path dest = Paths.get("externalgames").resolve(name);
setResult(dest);
new Unzipper(zipFile, dest)
.setSubDirectory(subdirectory.toString())
.setTerminateIfSubDirectoryNotExists()
.setEncoding(charset)
.unzip();
}
}

View File

@ -0,0 +1,272 @@
package com.tungsten.fcl.game;
import static com.tungsten.fclcore.util.Lang.mapOf;
import static com.tungsten.fclcore.util.Lang.toIterable;
import static com.tungsten.fclcore.util.Pair.pair;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import com.tungsten.fcl.setting.Profile;
import com.tungsten.fcl.setting.Profiles;
import com.tungsten.fcl.setting.VersionSetting;
import com.tungsten.fclcore.game.GameDirectoryType;
import com.tungsten.fclcore.mod.MismatchedModpackTypeException;
import com.tungsten.fclcore.mod.Modpack;
import com.tungsten.fclcore.mod.ModpackCompletionException;
import com.tungsten.fclcore.mod.ModpackConfiguration;
import com.tungsten.fclcore.mod.ModpackProvider;
import com.tungsten.fclcore.mod.ModpackUpdateTask;
import com.tungsten.fclcore.mod.UnsupportedModpackException;
import com.tungsten.fclcore.mod.curse.CurseModpackProvider;
import com.tungsten.fclcore.mod.mcbbs.McbbsModpackManifest;
import com.tungsten.fclcore.mod.mcbbs.McbbsModpackProvider;
import com.tungsten.fclcore.mod.modrinth.ModrinthModpackProvider;
import com.tungsten.fclcore.mod.multimc.MultiMCInstanceConfiguration;
import com.tungsten.fclcore.mod.multimc.MultiMCModpackProvider;
import com.tungsten.fclcore.mod.server.ServerModpackManifest;
import com.tungsten.fclcore.mod.server.ServerModpackProvider;
import com.tungsten.fclcore.mod.server.ServerModpackRemoteInstallTask;
import com.tungsten.fclcore.task.Schedulers;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.Lang;
import com.tungsten.fclcore.util.function.ExceptionalConsumer;
import com.tungsten.fclcore.util.function.ExceptionalRunnable;
import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.CompressingUtils;
import com.tungsten.fclcore.util.io.FileUtils;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
public final class ModpackHelper {
private ModpackHelper() {}
private static final Map<String, ModpackProvider> providers = mapOf(
pair(CurseModpackProvider.INSTANCE.getName(), CurseModpackProvider.INSTANCE),
pair(McbbsModpackProvider.INSTANCE.getName(), McbbsModpackProvider.INSTANCE),
pair(ModrinthModpackProvider.INSTANCE.getName(), ModrinthModpackProvider.INSTANCE),
pair(MultiMCModpackProvider.INSTANCE.getName(), MultiMCModpackProvider.INSTANCE),
pair(ServerModpackProvider.INSTANCE.getName(), ServerModpackProvider.INSTANCE),
pair(HMCLModpackProvider.INSTANCE.getName(), HMCLModpackProvider.INSTANCE)
);
@Nullable
public static ModpackProvider getProviderByType(String type) {
return providers.get(type);
}
public static boolean isFileModpackByExtension(File file) {
String ext = FileUtils.getExtension(file);
return "zip".equals(ext) || "mrpack".equals(ext);
}
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
try (ZipFile zipFile = CompressingUtils.openZipFile(file, charset)) {
// Order for trying detecting manifest is necessary here.
// Do not change to iterating providers.
for (ModpackProvider provider : new ModpackProvider[]{
McbbsModpackProvider.INSTANCE,
CurseModpackProvider.INSTANCE,
ModrinthModpackProvider.INSTANCE,
HMCLModpackProvider.INSTANCE,
MultiMCModpackProvider.INSTANCE,
ServerModpackProvider.INSTANCE}) {
try {
return provider.readManifest(zipFile, file, charset);
} catch (Exception ignored) {
}
}
} catch (IOException ignored) {
}
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
findMinecraftDirectoryInManuallyCreatedModpack(file.toString(), fs);
throw new ManuallyCreatedModpackException(file);
} catch (IOException e) {
// ignore it
}
throw new UnsupportedModpackException(file.toString());
}
public static Path findMinecraftDirectoryInManuallyCreatedModpack(String modpackName, FileSystem fs) throws IOException, UnsupportedModpackException {
Path root = fs.getPath("/");
if (isMinecraftDirectory(root)) return root;
try (Stream<Path> firstLayer = Files.list(root)) {
for (Path dir : toIterable(firstLayer)) {
if (isMinecraftDirectory(dir)) return dir;
try (Stream<Path> secondLayer = Files.list(dir)) {
for (Path subdir : toIterable(secondLayer)) {
if (isMinecraftDirectory(subdir)) return subdir;
}
} catch (IOException ignored) {
}
}
} catch (IOException ignored) {
}
throw new UnsupportedModpackException(modpackName);
}
private static boolean isMinecraftDirectory(Path path) {
return Files.isDirectory(path.resolve("versions")) &&
(path.getFileName() == null || ".minecraft".equals(FileUtils.getName(path)));
}
public static ModpackConfiguration<?> readModpackConfiguration(File file) throws IOException {
if (!file.exists())
throw new FileNotFoundException(file.getPath());
else
try {
return JsonUtils.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<?>>() {
}.getType());
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
}
}
public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
FCLGameRepository repository = profile.getRepository();
repository.refreshVersions();
VersionSetting vs = repository.specializeVersionSetting(name);
repository.undoMark(name);
if (vs != null)
vs.setGameDirType(GameDirectoryType.VERSION_FOLDER);
};
ExceptionalConsumer<Exception, ?> failure = ex -> {
if (ex instanceof ModpackCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
success.run();
// This is tolerable and we will not delete the game
}
};
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStagesHint(Arrays.asList("fcl.modpack", "fcl.modpack.download"));
}
public static boolean isExternalGameNameConflicts(String name) {
return Files.exists(Paths.get("externalgames").resolve(name));
}
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, File zipFile, String name, Charset charset) {
if (isExternalGameNameConflicts(name)) {
throw new IllegalArgumentException("name existing");
}
return new ManuallyCreatedModpackInstallTask(profile, zipFile.toPath(), charset, name)
.thenAcceptAsync(Schedulers.androidUIThread(), location -> {
Profile newProfile = new Profile(name, location.toFile());
Profiles.getProfiles().add(newProfile);
Profiles.setSelectedProfile(newProfile);
});
}
public static Task<?> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
FCLGameRepository repository = profile.getRepository();
repository.refreshVersions();
VersionSetting vs = repository.specializeVersionSetting(name);
repository.undoMark(name);
if (vs != null)
vs.setGameDirType(GameDirectoryType.VERSION_FOLDER);
};
ExceptionalConsumer<Exception, ?> failure = ex -> {
if (ex instanceof ModpackCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
success.run();
// This is tolerable and we will not delete the game
}
};
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
else
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.androidUIThread(), success, failure);
}
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
switch (configuration.getType()) {
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
.withStagesHint(Arrays.asList("fcl.modpack", "fcl.modpack.download"));
default:
throw new UnsupportedModpackException();
}
}
public static Task<?> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
ModpackProvider provider = getProviderByType(configuration.getType());
if (provider == null) {
throw new UnsupportedModpackException();
}
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
}
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
vs.setUsesGlobal(false);
vs.setGameDirType(GameDirectoryType.VERSION_FOLDER);
if (c.isOverrideMemory()) {
vs.setPermSize(Optional.ofNullable(c.getPermGen()).map(Object::toString).orElse(""));
if (c.getMaxMemory() != null)
vs.setMaxMemory(c.getMaxMemory());
vs.setMinMemory(c.getMinMemory());
}
if (c.isOverrideJavaArgs()) {
vs.setJavaArgs(Lang.nonNull(c.getJvmArgs(), ""));
}
if (c.isOverrideWindow()) {
vs.setFullscreen(c.isFullscreen());
if (c.getWidth() != null)
vs.setWidth(c.getWidth());
if (c.getHeight() != null)
vs.setHeight(c.getHeight());
}
}
private static Task<Void> createMultiMCPostInstallTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
return Task.runAsync(Schedulers.androidUIThread(), () -> {
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
ModpackHelper.toVersionSetting(manifest, vs);
});
}
private static Task<Void> createMcbbsPostInstallTask(Profile profile, McbbsModpackManifest manifest, String version) {
return Task.runAsync(Schedulers.androidUIThread(), () -> {
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
if (manifest.getLaunchInfo().getMinMemory() > vs.getMaxMemory())
vs.setMaxMemory(manifest.getLaunchInfo().getMinMemory());
});
}
}

View File

@ -0,0 +1,199 @@
package com.tungsten.fcl.game;
import static com.tungsten.fclcore.util.Lang.mapOf;
import static com.tungsten.fclcore.util.Lang.thread;
import com.tungsten.fcl.R;
import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.auth.AuthenticationException;
import com.tungsten.fclcore.auth.OAuth;
import com.tungsten.fclcore.event.Event;
import com.tungsten.fclcore.event.EventManager;
import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.io.IOUtils;
import com.tungsten.fclcore.util.io.JarUtils;
import com.tungsten.fclcore.util.io.NetworkUtils;
import fi.iki.elonen.NanoHTTPD;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
private final int port;
private final CompletableFuture<String> future = new CompletableFuture<>();
public static String lastlyOpenedURL;
private String idToken;
private OAuthServer(int port) {
super(port);
this.port = port;
}
@Override
public String getRedirectURI() {
return String.format("http://localhost:%d/auth-response", port);
}
@Override
public String waitFor() throws InterruptedException, ExecutionException {
return future.get();
}
@Override
public String getIdToken() {
return idToken;
}
@Override
public Response serve(IHTTPSession session) {
if (!"/auth-response".equals(session.getUri())) {
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "");
}
if (session.getMethod() == Method.POST) {
Map<String, String> files = new HashMap<>();
try {
session.parseBody(files);
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to read post data", e);
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
} catch (ResponseException re) {
return newFixedLengthResponse(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
}
} else if (session.getMethod() == Method.GET) {
// do nothing
} else {
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "");
}
String parameters = session.getQueryParameterString();
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
if (query.containsKey("code")) {
idToken = query.get("id_token");
future.complete(query.get("code"));
} else {
Logging.LOG.warning("Error: " + parameters);
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
}
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
.replace("%close-page%", FCLPath.CONTEXT.getString(R.string.account_methods_microsoft_close_page));
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to load html");
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
}
thread(() -> {
try {
Thread.sleep(1000);
stop();
} catch (InterruptedException e) {
Logging.LOG.log(Level.SEVERE, "Failed to sleep for 1 second");
}
});
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
}
public static class Factory implements OAuth.Callback {
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
@Override
public OAuth.Session startServer() throws IOException, AuthenticationException {
if (StringUtils.isBlank(getClientId())) {
throw new MicrosoftAuthenticationNotSupportedException();
}
IOException exception = null;
for (int port : new int[]{29111, 29112, 29113, 29114, 29115}) {
try {
OAuthServer server = new OAuthServer(port);
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
return server;
} catch (IOException e) {
exception = e;
}
}
throw exception;
}
@Override
public void grantDeviceCode(String userCode, String verificationURI) {
onGrantDeviceCode.fireEvent(new GrantDeviceCodeEvent(this, userCode, verificationURI));
}
@Override
public void openBrowser(String url) throws IOException {
lastlyOpenedURL = url;
// TODO: fix
//FXUtils.openLink(url);
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
}
// TODO: fix
@Override
public String getClientId() {
return System.getProperty("hmcl.microsoft.auth.id",
JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Id")).orElse(""));
}
// TODO: fix
@Override
public String getClientSecret() {
return System.getProperty("hmcl.microsoft.auth.secret",
JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Secret")).orElse(""));
}
@Override
public boolean isPublicClient() {
return true; // We have turned on the device auth flow.
}
}
public static class GrantDeviceCodeEvent extends Event {
private final String userCode;
private final String verificationUri;
public GrantDeviceCodeEvent(Object source, String userCode, String verificationUri) {
super(source);
this.userCode = userCode;
this.verificationUri = verificationUri;
}
public String getUserCode() {
return userCode;
}
public String getVerificationUri() {
return verificationUri;
}
}
public static class OpenBrowserEvent extends Event {
private final String url;
public OpenBrowserEvent(Object source, String url) {
super(source);
this.url = url;
}
public String getUrl() {
return url;
}
}
public static class MicrosoftAuthenticationNotSupportedException extends AuthenticationException {
}
}

View File

@ -0,0 +1,233 @@
package com.tungsten.fcl.game;
import static com.tungsten.fclcore.util.Lang.threadPool;
import static com.tungsten.fclcore.util.Logging.LOG;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static java.util.Objects.requireNonNull;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.drawable.BitmapDrawable;
import com.tungsten.fcl.util.ResourceNotFoundError;
import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.auth.Account;
import com.tungsten.fclcore.auth.ServerResponseMalformedException;
import com.tungsten.fclcore.auth.microsoft.MicrosoftAccount;
import com.tungsten.fclcore.auth.yggdrasil.Texture;
import com.tungsten.fclcore.auth.yggdrasil.TextureModel;
import com.tungsten.fclcore.auth.yggdrasil.TextureType;
import com.tungsten.fclcore.auth.yggdrasil.YggdrasilAccount;
import com.tungsten.fclcore.auth.yggdrasil.YggdrasilService;
import com.tungsten.fclcore.fakefx.beans.binding.Bindings;
import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding;
import com.tungsten.fclcore.task.FileDownloadTask;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.fakefx.BindingMapping;
public final class TexturesLoader {
private TexturesLoader() {
}
// ==== Texture Loading ====
public static class LoadedTexture {
private final Bitmap image;
private final Map<String, String> metadata;
public LoadedTexture(Bitmap image, Map<String, String> metadata) {
this.image = requireNonNull(image);
this.metadata = requireNonNull(metadata);
}
public Bitmap getImage() {
return image;
}
public Map<String, String> getMetadata() {
return metadata;
}
}
private static final ThreadPoolExecutor POOL = threadPool("TexturesDownload", true, 2, 10, TimeUnit.SECONDS);
private static final Path TEXTURES_DIR = new File(FCLPath.FILES_DIR).toPath().resolve("assets").resolve("skins");
private static Path getTexturePath(Texture texture) {
String url = texture.getUrl();
int slash = url.lastIndexOf('/');
int dot = url.lastIndexOf('.');
if (dot < slash) {
dot = url.length();
}
String hash = url.substring(slash + 1, dot);
String prefix = hash.length() > 2 ? hash.substring(0, 2) : "xx";
return TEXTURES_DIR.resolve(prefix).resolve(hash);
}
public static LoadedTexture loadTexture(Texture texture) throws IOException {
if (StringUtils.isBlank(texture.getUrl())) {
throw new IOException("Texture url is empty");
}
Path file = getTexturePath(texture);
if (!Files.isRegularFile(file)) {
// download it
try {
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run();
LOG.info("Texture downloaded: " + texture.getUrl());
} catch (Exception e) {
if (Files.isRegularFile(file)) {
// concurrency conflict?
LOG.log(Level.WARNING, "Failed to download texture " + texture.getUrl() + ", but the file is available", e);
} else {
throw new IOException("Failed to download texture " + texture.getUrl());
}
}
}
Bitmap img = BitmapFactory.decodeStream(Files.newInputStream(file));
if (img == null)
throw new IOException("Texture is malformed");
Map<String, String> metadata = texture.getMetadata();
if (metadata == null) {
metadata = emptyMap();
}
return new LoadedTexture(img, metadata);
}
// ====
// ==== Skins ====
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
static {
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
}
private static void loadDefaultSkin(String path, TextureModel model) {
try (InputStream in = ResourceNotFoundError.getResourceAsStream(path)) {
DEFAULT_SKINS.put(model, new LoadedTexture(BitmapFactory.decodeStream(in), singletonMap("model", model.modelName)));
} catch (Throwable e) {
throw new ResourceNotFoundError("Cannoot load default skin from " + path, e);
}
}
public static LoadedTexture getDefaultSkin(TextureModel model) {
return DEFAULT_SKINS.get(model);
}
public static ObjectBinding<LoadedTexture> skinBinding(YggdrasilService service, UUID uuid) {
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(uuid));
return BindingMapping.of(service.getProfileRepository().binding(uuid))
.map(profile -> profile
.flatMap(it -> {
try {
return YggdrasilService.getTextures(it);
} catch (ServerResponseMalformedException e) {
LOG.log(Level.WARNING, "Failed to parse texture payload", e);
return Optional.empty();
}
})
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
.asyncMap(it -> {
if (it.isPresent()) {
Texture texture = it.get();
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
} else {
return CompletableFuture.completedFuture(uuidFallback);
}
}, uuidFallback);
}
public static ObjectBinding<LoadedTexture> skinBinding(Account account) {
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(account.getUUID()));
return BindingMapping.of(account.getTextures())
.map(textures -> textures
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
.asyncMap(it -> {
if (it.isPresent()) {
Texture texture = it.get();
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
} else {
return CompletableFuture.completedFuture(uuidFallback);
}
}, uuidFallback);
}
// ====
// ==== Avatar ====
public static Bitmap toAvatar(Bitmap skin, int size) {
int faceOffset = (int) Math.round(size / 18.0);
Bitmap faceBitmap = Bitmap.createBitmap(skin, 8, 8, 8, 8, (Matrix) null, false);
Bitmap hatBitmap = Bitmap.createBitmap(skin, 40, 8, 8, 8, (Matrix) null, false);
Bitmap avatar = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(avatar);
Matrix matrix;
float faceScale = ((size - 2 * faceOffset) / 8f);
float hatScale = (size / 8f);
matrix = new Matrix();
matrix.postScale(faceScale, faceScale);
Bitmap newFaceBitmap = Bitmap.createBitmap(faceBitmap, 0, 0 , 8, 8, matrix, false);
matrix = new Matrix();
matrix.postScale(hatScale, hatScale);
Bitmap newHatBitmap = Bitmap.createBitmap(hatBitmap, 0, 0, 8, 8, matrix, false);
canvas.drawBitmap(newFaceBitmap, faceOffset, faceOffset, null);
canvas.drawBitmap(newHatBitmap, 0, 0, null);
return avatar;
}
public static ObjectBinding<BitmapDrawable> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
return BindingMapping.of(skinBinding(service, uuid))
.map(it -> toAvatar(it.image, size))
.map(BitmapDrawable::new);
}
public static ObjectBinding<BitmapDrawable> fxAvatarBinding(Account account, int size) {
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
return BindingMapping.of(skinBinding(account))
.map(it -> toAvatar(it.image, size))
.map(BitmapDrawable::new);
} else {
return Bindings.createObjectBinding(
() -> new BitmapDrawable(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
}
}
// ====
}

View File

@ -0,0 +1,402 @@
package com.tungsten.fcl.setting;
import static com.tungsten.fcl.setting.ConfigHolder.config;
import static com.tungsten.fcl.util.FXUtils.onInvalidating;
import static com.tungsten.fclcore.fakefx.collections.FXCollections.observableArrayList;
import static com.tungsten.fclcore.util.Lang.immutableListOf;
import static com.tungsten.fclcore.util.Lang.mapOf;
import static com.tungsten.fclcore.util.Logging.LOG;
import static com.tungsten.fclcore.util.Pair.pair;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import static java.util.stream.Collectors.toList;
import android.content.Context;
import com.tungsten.fcl.R;
import com.tungsten.fcl.game.OAuthServer;
import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.auth.Account;
import com.tungsten.fclcore.auth.AccountFactory;
import com.tungsten.fclcore.auth.AuthenticationException;
import com.tungsten.fclcore.auth.CharacterDeletedException;
import com.tungsten.fclcore.auth.NoCharacterException;
import com.tungsten.fclcore.auth.OAuthAccount;
import com.tungsten.fclcore.auth.ServerDisconnectException;
import com.tungsten.fclcore.auth.ServerResponseMalformedException;
import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorAccount;
import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorAccountFactory;
import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorArtifactInfo;
import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorArtifactProvider;
import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorDownloadException;
import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorDownloader;
import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorServer;
import com.tungsten.fclcore.auth.authlibinjector.BoundAuthlibInjectorAccountFactory;
import com.tungsten.fclcore.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider;
import com.tungsten.fclcore.auth.microsoft.MicrosoftAccount;
import com.tungsten.fclcore.auth.microsoft.MicrosoftAccountFactory;
import com.tungsten.fclcore.auth.microsoft.MicrosoftService;
import com.tungsten.fclcore.auth.offline.OfflineAccount;
import com.tungsten.fclcore.auth.offline.OfflineAccountFactory;
import com.tungsten.fclcore.auth.yggdrasil.RemoteAuthenticationException;
import com.tungsten.fclcore.auth.yggdrasil.YggdrasilAccount;
import com.tungsten.fclcore.auth.yggdrasil.YggdrasilAccountFactory;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.property.ObjectProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyListProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyListWrapper;
import com.tungsten.fclcore.fakefx.beans.property.SimpleObjectProperty;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
import com.tungsten.fclcore.task.Schedulers;
import com.tungsten.fclcore.util.skin.InvalidSkinException;
public final class Accounts {
private Accounts() {}
private static final AuthlibInjectorArtifactProvider AUTHLIB_INJECTOR_DOWNLOADER = createAuthlibInjectorArtifactProvider();
private static void triggerAuthlibInjectorUpdateCheck() {
if (AUTHLIB_INJECTOR_DOWNLOADER instanceof AuthlibInjectorDownloader) {
Schedulers.io().execute(() -> {
try {
((AuthlibInjectorDownloader) AUTHLIB_INJECTOR_DOWNLOADER).checkUpdate();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to check update for authlib-injector", e);
}
});
}
}
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
public static final BoundAuthlibInjectorAccountFactory FACTORY_LITTLE_SKIN = getAccountFactoryByAuthlibInjectorServer(new AuthlibInjectorServer("https://littleskin.cn/api/yggdrasil/"));
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
// ==== login type / account factory mapping ====
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
private static final Map<AccountFactory<?>, String> factory2type = new HashMap<>();
static {
type2factory.put("offline", FACTORY_OFFLINE);
type2factory.put("yggdrasil", FACTORY_MOJANG);
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
type2factory.put("microsoft", FACTORY_MICROSOFT);
type2factory.forEach((type, factory) -> factory2type.put(factory, type));
}
public static String getLoginType(AccountFactory<?> factory) {
String type = factory2type.get(factory);
if (type != null) return type;
if (factory instanceof BoundAuthlibInjectorAccountFactory) {
return factory2type.get(FACTORY_AUTHLIB_INJECTOR);
}
throw new IllegalArgumentException("Unrecognized account factory");
}
public static AccountFactory<?> getAccountFactory(String loginType) {
return Optional.ofNullable(type2factory.get(loginType))
.orElseThrow(() -> new IllegalArgumentException("Unrecognized login type"));
}
public static BoundAuthlibInjectorAccountFactory getAccountFactoryByAuthlibInjectorServer(AuthlibInjectorServer server) {
return new BoundAuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, server);
}
// ====
public static AccountFactory<?> getAccountFactory(Account account) {
if (account instanceof OfflineAccount)
return FACTORY_OFFLINE;
else if (account instanceof AuthlibInjectorAccount)
return FACTORY_AUTHLIB_INJECTOR;
else if (account instanceof YggdrasilAccount)
return FACTORY_MOJANG;
else if (account instanceof MicrosoftAccount)
return FACTORY_MICROSOFT;
else
throw new IllegalArgumentException("Failed to determine account type: " + account);
}
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static final ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
{
accounts.addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
// this methods first checks whether the current selection is valid
// if it's valid, the underlying storage will be updated
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
Account selected = get();
if (accounts.isEmpty()) {
if (selected == null) {
// valid
} else {
// the previously selected account is gone, we can only set it to null here
set(null);
return;
}
} else {
if (accounts.contains(selected)) {
// valid
} else {
// the previously selected account is gone
set(accounts.get(0));
return;
}
}
// selection is valid, store it
if (!initialized)
return;
updateAccountStorages();
}
};
/**
* True if {@link #init()} hasn't been called.
*/
private static boolean initialized = false;
static {
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
}
private static Map<Object, Object> getAccountStorage(Account account) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", getLoginType(getAccountFactory(account)));
if (account == selectedAccount.get()) {
storage.put("selected", true);
}
return storage;
}
private static void updateAccountStorages() {
// don't update the underlying storage before data loading is completed
// otherwise it might cause data loss
if (!initialized)
return;
// update storage
config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList()));
}
/**
* Called when it's ready to load accounts from {@link ConfigHolder#config()}.
*/
static void init() {
if (initialized)
throw new IllegalStateException("Already initialized");
// load accounts
config().getAccountStorages().forEach(storage -> {
AccountFactory<?> factory = type2factory.get(storage.get("type"));
if (factory == null) {
LOG.warning("Unrecognized account type: " + storage);
return;
}
Account account;
try {
account = factory.fromStorage(storage);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
return;
}
accounts.add(account);
if (Boolean.TRUE.equals(storage.get("selected"))) {
selectedAccount.set(account);
}
});
initialized = true;
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
Account selected = selectedAccount.get();
if (selected != null) {
Schedulers.io().execute(() -> {
try {
selected.logIn();
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
}
});
}
if (!config().getAuthlibInjectorServers().isEmpty()) {
triggerAuthlibInjectorUpdateCheck();
}
Schedulers.io().execute(() -> {
try {
FACTORY_LITTLE_SKIN.getServer().fetchMetadataResponse();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + FACTORY_LITTLE_SKIN.getServer(), e);
}
});
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
continue;
Schedulers.io().execute(() -> {
try {
server.fetchMetadataResponse();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + server, e);
}
});
}
}
public static ObservableList<Account> getAccounts() {
return accounts;
}
public static ReadOnlyListProperty<Account> accountsProperty() {
return accountsWrapper.getReadOnlyProperty();
}
public static Account getSelectedAccount() {
return selectedAccount.get();
}
public static void setSelectedAccount(Account selectedAccount) {
Accounts.selectedAccount.set(selectedAccount);
}
public static ObjectProperty<Account> selectedAccountProperty() {
return selectedAccount;
}
// ==== authlib-injector ====
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
String authlibinjectorLocation = FCLPath.AUTHLIB_INJECTOR_PATH;
if (authlibinjectorLocation == null) {
return new AuthlibInjectorDownloader(
new File(FCLPath.AUTHLIB_INJECTOR_PATH).toPath(),
DownloadProviders::getDownloadProvider) {
@Override
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
Optional<AuthlibInjectorArtifactInfo> local = super.getArtifactInfoImmediately();
if (local.isPresent()) {
return local;
}
// search authlib-injector.jar in current directory, it's used as a fallback
return parseArtifact(Paths.get("authlib-injector.jar"));
}
};
} else {
LOG.info("Using specified authlib-injector: " + authlibinjectorLocation);
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
}
}
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
return config().getAuthlibInjectorServers().stream()
.filter(server -> url.equals(server.getUrl()))
.findFirst()
.orElseGet(() -> {
AuthlibInjectorServer server = new AuthlibInjectorServer(url);
config().getAuthlibInjectorServers().add(server);
return server;
});
}
/**
* After an {@link AuthlibInjectorServer} is removed, the associated accounts should also be removed.
* This method performs a check and removes the dangling accounts.
*/
private static void removeDanglingAuthlibInjectorAccounts() {
accounts.stream()
.filter(AuthlibInjectorAccount.class::isInstance)
.map(AuthlibInjectorAccount.class::cast)
.filter(it -> !config().getAuthlibInjectorServers().contains(it.getServer()))
.collect(toList())
.forEach(accounts::remove);
}
// ====
// ==== Login type name i18n ===
private static final Map<AccountFactory<?>, Integer> unlocalizedLoginTypeNames = mapOf(
pair(Accounts.FACTORY_OFFLINE, R.string.account_methods_offline),
pair(Accounts.FACTORY_MOJANG, R.string.account_methods_yggdrasil),
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, R.string.account_methods_authlib_injector),
pair(Accounts.FACTORY_MICROSOFT, R.string.account_methods_microsoft));
public static String getLocalizedLoginTypeName(Context context, AccountFactory<?> factory) {
return context.getString(unlocalizedLoginTypeNames.get(factory));
}
// ====
public static String localizeErrorMessage(Context context, Exception exception) {
if (exception instanceof NoCharacterException) {
return context.getString(R.string.account_failed_no_character);
} else if (exception instanceof ServerDisconnectException) {
return context.getString(R.string.account_failed_connect_authentication_server);
} else if (exception instanceof ServerResponseMalformedException) {
return context.getString(R.string.account_failed_server_response_malformed);
} else if (exception instanceof RemoteAuthenticationException) {
RemoteAuthenticationException remoteException = (RemoteAuthenticationException) exception;
String remoteMessage = remoteException.getRemoteMessage();
if ("ForbiddenOperationException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
if (remoteMessage.contains("Invalid credentials")) {
return context.getString(R.string.account_failed_invalid_credentials);
} else if (remoteMessage.contains("Invalid token")) {
return context.getString(R.string.account_failed_invalid_token);
} else if (remoteMessage.contains("Invalid username or password")) {
return context.getString(R.string.account_failed_invalid_password);
} else {
return remoteMessage;
}
} else if ("ResourceException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
if (remoteMessage.contains("The requested resource is no longer available")) {
return context.getString(R.string.account_failed_migration);
} else {
return remoteMessage;
}
}
return exception.getMessage();
} else if (exception instanceof AuthlibInjectorDownloadException) {
return context.getString(R.string.account_failed_injector_download_failure);
} else if (exception instanceof CharacterDeletedException) {
return context.getString(R.string.account_failed_character_deleted);
} else if (exception instanceof InvalidSkinException) {
return context.getString(R.string.account_skin_invalid_skin);
} else if (exception instanceof MicrosoftService.XboxAuthorizationException) {
long errorCode = ((MicrosoftService.XboxAuthorizationException) exception).getErrorCode();
if (errorCode == MicrosoftService.XboxAuthorizationException.ADD_FAMILY) {
return context.getString(R.string.account_methods_microsoft_error_add_family);
} else if (errorCode == MicrosoftService.XboxAuthorizationException.COUNTRY_UNAVAILABLE) {
return context.getString(R.string.account_methods_microsoft_error_country_unavailable);
} else if (errorCode == MicrosoftService.XboxAuthorizationException.MISSING_XBOX_ACCOUNT) {
return context.getString(R.string.account_methods_microsoft_error_missing_xbox_account);
} else {
return context.getString(R.string.account_methods_microsoft_error_unknown);
}
} else if (exception instanceof MicrosoftService.NoMinecraftJavaEditionProfileException) {
return context.getString(R.string.account_methods_microsoft_error_no_character);
} else if (exception instanceof MicrosoftService.NoXuiException) {
return context.getString(R.string.account_methods_microsoft_error_add_family_probably);
} else if (exception instanceof OAuthAccount.WrongAccountException) {
return context.getString(R.string.account_failed_wrong_account);
} else if (exception.getClass() == AuthenticationException.class) {
return exception.getLocalizedMessage();
} else {
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
}
}
}

View File

@ -0,0 +1,76 @@
package com.tungsten.fcl.setting;
import static com.tungsten.fcl.setting.ConfigHolder.config;
import static com.tungsten.fclcore.util.Logging.LOG;
import com.google.gson.JsonParseException;
import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorServer;
import com.tungsten.fclcore.task.Schedulers;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.gson.Validation;
import com.tungsten.fclcore.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
public class AuthlibInjectorServers implements Validation {
public static final String CONFIG_FILENAME = "authlib-injectors.json";
private final List<String> urls;
public AuthlibInjectorServers(List<String> urls) {
this.urls = urls;
}
public List<String> getUrls() {
return urls;
}
@Override
public void validate() throws JsonParseException {
if (urls == null)
throw new JsonParseException("authlib-injectors.json -> urls cannot be null");
}
private static final Path configLocation = new File(FCLPath.FILES_DIR + "/" + CONFIG_FILENAME).toPath();
private static AuthlibInjectorServers configInstance;
public synchronized static void init() {
if (configInstance != null) {
throw new IllegalStateException("AuthlibInjectorServers is already loaded");
}
configInstance = new AuthlibInjectorServers(Collections.emptyList());
if (Files.exists(configLocation)) {
try {
String content = FileUtils.readText(configLocation);
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Malformed authlib-injectors.json", e);
}
}
if (ConfigHolder.isNewlyCreated() && !AuthlibInjectorServers.getConfigInstance().getUrls().isEmpty()) {
config().setPreferredLoginType(Accounts.getLoginType(Accounts.FACTORY_AUTHLIB_INJECTOR));
for (String url : AuthlibInjectorServers.getConfigInstance().getUrls()) {
Task.supplyAsync(Schedulers.io(), () -> AuthlibInjectorServer.locateServer(url))
.thenAcceptAsync(Schedulers.androidUIThread(), server -> config().getAuthlibInjectorServers().add(server))
.start();
}
}
}
public static AuthlibInjectorServers getConfigInstance() {
return configInstance;
}
}

View File

@ -0,0 +1,312 @@
package com.tungsten.fcl.setting;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorServer;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty;
import com.tungsten.fclcore.fakefx.beans.property.DoubleProperty;
import com.tungsten.fclcore.fakefx.beans.property.IntegerProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleBooleanProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleDoubleProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleIntegerProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty;
import com.tungsten.fclcore.fakefx.beans.property.StringProperty;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
import com.tungsten.fclcore.fakefx.collections.ObservableMap;
import com.tungsten.fclcore.fakefx.collections.ObservableSet;
import com.tungsten.fclcore.util.fakefx.ObservableHelper;
import com.tungsten.fclcore.util.fakefx.PropertyUtils;
import com.tungsten.fclcore.util.gson.FileTypeAdapter;
import com.tungsten.fclcore.util.gson.fakefx.creators.ObservableListCreator;
import com.tungsten.fclcore.util.gson.fakefx.creators.ObservableMapCreator;
import com.tungsten.fclcore.util.gson.fakefx.creators.ObservableSetCreator;
import com.tungsten.fclcore.util.gson.fakefx.factories.JavaFxPropertyTypeAdapterFactory;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.Map;
import java.util.TreeMap;
public final class Config implements Cloneable, Observable {
public static final int CURRENT_UI_VERSION = 0;
private static final Gson CONFIG_GSON = new GsonBuilder()
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.setPrettyPrinting()
.create();
@Nullable
public static Config fromJson(String json) throws JsonParseException {
Config loaded = CONFIG_GSON.fromJson(json, Config.class);
if (loaded == null) {
return null;
}
Config instance = new Config();
PropertyUtils.copyProperties(loaded, instance);
return instance;
}
@SerializedName("last")
private StringProperty selectedProfile = new SimpleStringProperty("");
@SerializedName("commonpath")
private StringProperty commonDirectory = new SimpleStringProperty(FCLPath.SHARED_COMMON_DIR);
@SerializedName("width")
private DoubleProperty width = new SimpleDoubleProperty();
@SerializedName("height")
private DoubleProperty height = new SimpleDoubleProperty();
@SerializedName("autoDownloadThreads")
private BooleanProperty autoDownloadThreads = new SimpleBooleanProperty(false);
@SerializedName("downloadThreads")
private IntegerProperty downloadThreads = new SimpleIntegerProperty(64);
@SerializedName("downloadType")
private StringProperty downloadType = new SimpleStringProperty("mcbbs");
@SerializedName("autoChooseDownloadType")
private BooleanProperty autoChooseDownloadType = new SimpleBooleanProperty(true);
@SerializedName("versionListSource")
private StringProperty versionListSource = new SimpleStringProperty("balanced");
@SerializedName("configurations")
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
@SerializedName("accounts")
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
@SerializedName("promptedVersion")
private StringProperty promptedVersion = new SimpleStringProperty();
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
/**
* The version of UI that the user have last used.
* If there is a major change in UI, {@link Config#CURRENT_UI_VERSION} should be increased.
* When {@link #CURRENT_UI_VERSION} is higher than the property, the user guide should be shown,
* then this property is set to the same value as {@link #CURRENT_UI_VERSION}.
* In particular, the property is default to 0, so that whoever open the application for the first time will see the guide.
*/
@SerializedName("uiVersion")
private IntegerProperty uiVersion = new SimpleIntegerProperty(0);
/**
* The preferred login type to use when the user wants to add an account.
*/
@SerializedName("preferredLoginType")
private StringProperty preferredLoginType = new SimpleStringProperty();
private transient ObservableHelper helper = new ObservableHelper(this);
public Config() {
PropertyUtils.attachListener(this, helper);
}
@Override
public void addListener(InvalidationListener listener) {
helper.addListener(listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper.removeListener(listener);
}
public String toJson() {
return CONFIG_GSON.toJson(this);
}
@Override
public Config clone() {
return fromJson(this.toJson());
}
// Getters & Setters & Properties
public String getSelectedProfile() {
return selectedProfile.get();
}
public void setSelectedProfile(String selectedProfile) {
this.selectedProfile.set(selectedProfile);
}
public StringProperty selectedProfileProperty() {
return selectedProfile;
}
public String getCommonDirectory() {
return commonDirectory.get();
}
public void setCommonDirectory(String commonDirectory) {
this.commonDirectory.set(commonDirectory);
}
public StringProperty commonDirectoryProperty() {
return commonDirectory;
}
public double getWidth() {
return width.get();
}
public DoubleProperty widthProperty() {
return width;
}
public void setWidth(double width) {
this.width.set(width);
}
public double getHeight() {
return height.get();
}
public DoubleProperty heightProperty() {
return height;
}
public void setHeight(double height) {
this.height.set(height);
}
public boolean getAutoDownloadThreads() {
return autoDownloadThreads.get();
}
public BooleanProperty autoDownloadThreadsProperty() {
return autoDownloadThreads;
}
public void setAutoDownloadThreads(boolean autoDownloadThreads) {
this.autoDownloadThreads.set(autoDownloadThreads);
}
public int getDownloadThreads() {
return downloadThreads.get();
}
public IntegerProperty downloadThreadsProperty() {
return downloadThreads;
}
public void setDownloadThreads(int downloadThreads) {
this.downloadThreads.set(downloadThreads);
}
public String getDownloadType() {
return downloadType.get();
}
public void setDownloadType(String downloadType) {
this.downloadType.set(downloadType);
}
public StringProperty downloadTypeProperty() {
return downloadType;
}
public boolean isAutoChooseDownloadType() {
return autoChooseDownloadType.get();
}
public BooleanProperty autoChooseDownloadTypeProperty() {
return autoChooseDownloadType;
}
public void setAutoChooseDownloadType(boolean autoChooseDownloadType) {
this.autoChooseDownloadType.set(autoChooseDownloadType);
}
public String getVersionListSource() {
return versionListSource.get();
}
public void setVersionListSource(String versionListSource) {
this.versionListSource.set(versionListSource);
}
public StringProperty versionListSourceProperty() {
return versionListSource;
}
public ObservableMap<String, Profile> getConfigurations() {
return configurations;
}
public ObservableList<Map<Object, Object>> getAccountStorages() {
return accountStorages;
}
public ObservableList<AuthlibInjectorServer> getAuthlibInjectorServers() {
return authlibInjectorServers;
}
public int getConfigVersion() {
return configVersion.get();
}
public IntegerProperty configVersionProperty() {
return configVersion;
}
public void setConfigVersion(int configVersion) {
this.configVersion.set(configVersion);
}
public int getUiVersion() {
return uiVersion.get();
}
public IntegerProperty uiVersionProperty() {
return uiVersion;
}
public void setUiVersion(int uiVersion) {
this.uiVersion.set(uiVersion);
}
public String getPreferredLoginType() {
return preferredLoginType.get();
}
public void setPreferredLoginType(String preferredLoginType) {
this.preferredLoginType.set(preferredLoginType);
}
public StringProperty preferredLoginTypeProperty() {
return preferredLoginType;
}
public String getPromptedVersion() {
return promptedVersion.get();
}
public StringProperty promptedVersionProperty() {
return promptedVersion;
}
public void setPromptedVersion(String promptedVersion) {
this.promptedVersion.set(promptedVersion);
}
}

View File

@ -0,0 +1,150 @@
package com.tungsten.fcl.setting;
import static com.tungsten.fclcore.util.Logging.LOG;
import com.google.gson.JsonParseException;
import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.util.InvocationDispatcher;
import com.tungsten.fclcore.util.Lang;
import com.tungsten.fclcore.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.util.logging.Level;
public final class ConfigHolder {
private ConfigHolder() {
}
public static final Path CONFIG_PATH = new File(FCLPath.FILES_DIR + "/global_config.json").toPath();
public static final Path GLOBAL_CONFIG_PATH = new File(FCLPath.FILES_DIR + "/global_config.json").toPath();
private static Config configInstance;
private static GlobalConfig globalConfigInstance;
private static boolean newlyCreated;
public static Config config() {
if (configInstance == null) {
throw new IllegalStateException("Configuration hasn't been loaded");
}
return configInstance;
}
public static GlobalConfig globalConfig() {
if (globalConfigInstance == null) {
throw new IllegalStateException("Configuration hasn't been loaded");
}
return globalConfigInstance;
}
public static boolean isNewlyCreated() {
return newlyCreated;
}
public synchronized static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
}
configInstance = loadConfig();
configInstance.addListener(source -> markConfigDirty());
globalConfigInstance = loadGlobalConfig();
globalConfigInstance.addListener(source -> markGlobalConfigDirty());
Settings.init();
if (newlyCreated) {
saveConfigSync();
}
}
private static Config loadConfig() throws IOException {
if (Files.exists(CONFIG_PATH)) {
try {
String content = FileUtils.readText(CONFIG_PATH);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
} else {
return deserialized;
}
} catch (JsonParseException e) {
LOG.log(Level.WARNING, "Malformed config.", e);
}
}
LOG.info("Creating an empty config");
newlyCreated = true;
return new Config();
}
private static final InvocationDispatcher<String> configWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
try {
writeToConfig(content);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save config", e);
}
});
private static void writeToConfig(String content) throws IOException {
LOG.info("Saving config");
synchronized (CONFIG_PATH) {
FileUtils.saveSafely(CONFIG_PATH, content);
}
}
static void markConfigDirty() {
configWriter.accept(configInstance.toJson());
}
private static void saveConfigSync() throws IOException {
writeToConfig(configInstance.toJson());
}
// Global Config
private static GlobalConfig loadGlobalConfig() throws IOException {
if (Files.exists(GLOBAL_CONFIG_PATH)) {
try {
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
GlobalConfig deserialized = GlobalConfig.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
} else {
return deserialized;
}
} catch (JsonParseException e) {
LOG.log(Level.WARNING, "Malformed config.", e);
}
}
LOG.info("Creating an empty global config");
return new GlobalConfig();
}
private static final InvocationDispatcher<String> globalConfigWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
try {
writeToGlobalConfig(content);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save config", e);
}
});
private static void writeToGlobalConfig(String content) throws IOException {
LOG.info("Saving global config");
synchronized (GLOBAL_CONFIG_PATH) {
FileUtils.saveSafely(GLOBAL_CONFIG_PATH, content);
}
}
static void markGlobalConfigDirty() {
globalConfigWriter.accept(globalConfigInstance.toJson());
}
private static void saveGlobalConfigSync() throws IOException {
writeToConfig(globalConfigInstance.toJson());
}
}

View File

@ -0,0 +1,162 @@
package com.tungsten.fcl.setting;
import static com.tungsten.fcl.setting.ConfigHolder.config;
import static com.tungsten.fclcore.task.FetchTask.DEFAULT_CONCURRENCY;
import static com.tungsten.fclcore.util.Lang.mapOf;
import static com.tungsten.fclcore.util.Pair.pair;
import android.content.Context;
import com.tungsten.fcl.R;
import com.tungsten.fcl.util.FXUtils;
import com.tungsten.fclcore.download.AdaptedDownloadProvider;
import com.tungsten.fclcore.download.ArtifactMalformedException;
import com.tungsten.fclcore.download.AutoDownloadProvider;
import com.tungsten.fclcore.download.BMCLAPIDownloadProvider;
import com.tungsten.fclcore.download.BalancedDownloadProvider;
import com.tungsten.fclcore.download.DownloadProvider;
import com.tungsten.fclcore.download.MojangDownloadProvider;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.task.DownloadException;
import com.tungsten.fclcore.task.FetchTask;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.io.ResponseCodeException;
import javax.net.ssl.SSLHandshakeException;
import java.io.FileNotFoundException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public final class DownloadProviders {
private DownloadProviders() {}
private static DownloadProvider currentDownloadProvider;
public static final Map<String, DownloadProvider> providersById;
public static final Map<String, DownloadProvider> rawProviders;
private static final AdaptedDownloadProvider fileDownloadProvider = new AdaptedDownloadProvider();
private static final MojangDownloadProvider MOJANG;
private static final BMCLAPIDownloadProvider BMCLAPI;
private static final BMCLAPIDownloadProvider MCBBS;
public static final String DEFAULT_PROVIDER_ID = "balanced";
public static final String DEFAULT_RAW_PROVIDER_ID = "mcbbs";
private static final InvalidationListener observer;
static {
String bmclapiRoot = "https://bmclapi2.bangbang93.com";
String bmclapiRootOverride = System.getProperty("hmcl.bmclapi.override");
if (bmclapiRootOverride != null) bmclapiRoot = bmclapiRootOverride;
MOJANG = new MojangDownloadProvider();
BMCLAPI = new BMCLAPIDownloadProvider(bmclapiRoot);
MCBBS = new BMCLAPIDownloadProvider("https://download.mcbbs.net");
rawProviders = mapOf(
pair("mojang", MOJANG),
pair("bmclapi", BMCLAPI),
pair("mcbbs", MCBBS)
);
AdaptedDownloadProvider fileProvider = new AdaptedDownloadProvider();
fileProvider.setDownloadProviderCandidates(Arrays.asList(MCBBS, BMCLAPI, MOJANG));
BalancedDownloadProvider balanced = new BalancedDownloadProvider(Arrays.asList(MCBBS, BMCLAPI, MOJANG));
providersById = mapOf(
pair("official", new AutoDownloadProvider(MOJANG, fileProvider)),
pair("balanced", new AutoDownloadProvider(balanced, fileProvider)),
pair("mirror", new AutoDownloadProvider(MCBBS, fileProvider)));
observer = FXUtils.observeWeak(() -> {
FetchTask.setDownloadExecutorConcurrency(
config().getAutoDownloadThreads() ? DEFAULT_CONCURRENCY : config().getDownloadThreads());
}, config().autoDownloadThreadsProperty(), config().downloadThreadsProperty());
}
static void init() {
FXUtils.onChangeAndOperate(config().versionListSourceProperty(), versionListSource -> {
if (!providersById.containsKey(versionListSource)) {
config().setVersionListSource(DEFAULT_PROVIDER_ID);
return;
}
currentDownloadProvider = Optional.ofNullable(providersById.get(versionListSource))
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
});
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
if (!rawProviders.containsKey(downloadType)) {
config().setDownloadType(DEFAULT_RAW_PROVIDER_ID);
return;
}
DownloadProvider primary = Optional.ofNullable(rawProviders.get(downloadType))
.orElse(rawProviders.get(DEFAULT_RAW_PROVIDER_ID));
fileDownloadProvider.setDownloadProviderCandidates(
Stream.concat(
Stream.of(primary),
rawProviders.values().stream().filter(x -> x != primary)
).collect(Collectors.toList())
);
});
}
public static String getPrimaryDownloadProviderId() {
String downloadType = config().getDownloadType();
if (providersById.containsKey(downloadType))
return downloadType;
else
return DEFAULT_PROVIDER_ID;
}
public static DownloadProvider getDownloadProviderByPrimaryId(String primaryId) {
return Optional.ofNullable(providersById.get(primaryId))
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
}
/**
* Get current primary preferred download provider
*/
public static DownloadProvider getDownloadProvider() {
return config().isAutoChooseDownloadType() ? currentDownloadProvider : fileDownloadProvider;
}
public static String localizeErrorMessage(Context context, Throwable exception) {
if (exception instanceof DownloadException) {
URL url = ((DownloadException) exception).getUrl();
if (exception.getCause() instanceof SocketTimeoutException) {
return context.getString(R.string.install_failed_downloading_timeout);
} else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (responseCodeException.getResponseCode() == 404) {
return context.getString(R.string.download_code_404);
} else {
return context.getString(R.string.install_failed_downloading_detail) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception.getCause() instanceof FileNotFoundException) {
return context.getString(R.string.download_code_404);
} else if (exception.getCause() instanceof AccessDeniedException) {
return context.getString(R.string.install_failed_downloading_detail) + "\n" + context.getString(R.string.exception_access_denied);
} else if (exception.getCause() instanceof ArtifactMalformedException) {
return context.getString(R.string.install_failed_downloading_detail) + "\n" + context.getString(R.string.exception_artifact_malformed);
} else if (exception.getCause() instanceof SSLHandshakeException) {
return context.getString(R.string.install_failed_downloading_detail) + "\n" + context.getString(R.string.exception_ssl_handshake);
} else {
return context.getString(R.string.install_failed_downloading_detail) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception instanceof ArtifactMalformedException) {
return context.getString(R.string.exception_artifact_malformed);
} else if (exception instanceof CancellationException) {
return context.getString(R.string.message_cancelled);
}
return StringUtils.getStackTrace(exception);
}
}

View File

@ -0,0 +1,145 @@
package com.tungsten.fcl.setting;
import static com.tungsten.fclcore.util.Lang.mapOf;
import static com.tungsten.fclcore.util.Pair.pair;
import com.google.gson.annotations.SerializedName;
import com.tungsten.fclcore.auth.OAuth;
import com.tungsten.fclcore.fakefx.beans.property.ObjectProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleObjectProperty;
import com.tungsten.fclcore.task.Schedulers;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.gson.UUIDTypeAdapter;
import com.tungsten.fclcore.util.io.HttpRequest;
import com.tungsten.fclcore.util.io.NetworkUtils;
import java.util.UUID;
public final class FCLAccounts {
private static final ObjectProperty<FCLAccount> account = new SimpleObjectProperty<>();
private FCLAccounts() {
}
public static FCLAccount getAccount() {
return account.get();
}
public static ObjectProperty<FCLAccount> accountProperty() {
return account;
}
public static void setAccount(FCLAccount account) {
FCLAccounts.account.set(account);
}
public static Task<Void> login() {
String nonce = UUIDTypeAdapter.fromUUID(UUID.randomUUID());
String scope = "openid offline_access";
return Task.supplyAsync(() -> {
OAuth.Session session = Accounts.OAUTH_CALLBACK.startServer();
Accounts.OAUTH_CALLBACK.openBrowser(NetworkUtils.withQuery(
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
mapOf(
pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()),
pair("response_type", "id_token code"),
pair("response_mode", "form_post"),
pair("scope", scope),
pair("redirect_uri", session.getRedirectURI()),
pair("nonce", nonce)
)));
String code = session.waitFor();
// Authorization Code -> Token
String responseText = HttpRequest.POST("https://login.microsoftonline.com/common/oauth2/v2.0/token")
.form(mapOf(pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()), pair("code", code),
pair("grant_type", "authorization_code"), pair("client_secret", Accounts.OAUTH_CALLBACK.getClientSecret()),
pair("redirect_uri", session.getRedirectURI()), pair("scope", scope)))
.getString();
OAuth.AuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
OAuth.AuthorizationResponse.class);
FCLAccountProfile profile = HttpRequest.GET("https://hmcl.huangyuhui.net/api/user")
.header("Token-Type", response.tokenType)
.header("Access-Token", response.accessToken)
.header("Authorization-Provider", "microsoft")
.authorization("Bearer", session.getIdToken())
.getJson(FCLAccountProfile.class);
return new FCLAccount("microsoft", profile.nickname, profile.email, profile.role, session.getIdToken(), response.tokenType, response.accessToken, response.refreshToken);
}).thenAcceptAsync(Schedulers.androidUIThread(), FCLAccounts::setAccount);
}
public static class FCLAccount implements HttpRequest.Authorization {
private final String provider;
private final String nickname;
private final String email;
private final String role;
private final String idToken;
private final String tokenType;
private final String accessToken;
private final String refreshToken;
public FCLAccount(String provider, String nickname, String email, String role, String idToken, String tokenType, String accessToken, String refreshToken) {
this.provider = provider;
this.nickname = nickname;
this.email = email;
this.role = role;
this.idToken = idToken;
this.tokenType = tokenType;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
public String getProvider() {
return provider;
}
public String getNickname() {
return nickname;
}
public String getEmail() {
return email;
}
public String getRole() {
return role;
}
public String getIdToken() {
return idToken;
}
@Override
public String getTokenType() {
return tokenType;
}
@Override
public String getAccessToken() {
return accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
}
private static class FCLAccountProfile {
@SerializedName("ID")
String id;
@SerializedName("Provider")
String provider;
@SerializedName("Email")
String email;
@SerializedName("NickName")
String nickname;
@SerializedName("Role")
String role;
}
}

View File

@ -0,0 +1,148 @@
package com.tungsten.fcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.property.IntegerProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleIntegerProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty;
import com.tungsten.fclcore.fakefx.beans.property.StringProperty;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
import com.tungsten.fclcore.fakefx.collections.ObservableMap;
import com.tungsten.fclcore.fakefx.collections.ObservableSet;
import com.tungsten.fclcore.util.fakefx.ObservableHelper;
import com.tungsten.fclcore.util.fakefx.PropertyUtils;
import com.tungsten.fclcore.util.gson.FileTypeAdapter;
import com.tungsten.fclcore.util.gson.fakefx.creators.ObservableListCreator;
import com.tungsten.fclcore.util.gson.fakefx.creators.ObservableMapCreator;
import com.tungsten.fclcore.util.gson.fakefx.creators.ObservableSetCreator;
import com.tungsten.fclcore.util.gson.fakefx.factories.JavaFxPropertyTypeAdapterFactory;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.lang.reflect.Type;
import java.util.*;
@JsonAdapter(GlobalConfig.Serializer.class)
public class GlobalConfig implements Cloneable, Observable {
private static final Gson CONFIG_GSON = new GsonBuilder()
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.setPrettyPrinting()
.create();
@Nullable
public static GlobalConfig fromJson(String json) throws JsonParseException {
GlobalConfig loaded = CONFIG_GSON.fromJson(json, GlobalConfig.class);
if (loaded == null) {
return null;
}
GlobalConfig instance = new GlobalConfig();
PropertyUtils.copyProperties(loaded, instance);
instance.unknownFields.putAll(loaded.unknownFields);
return instance;
}
private IntegerProperty agreementVersion = new SimpleIntegerProperty();
private StringProperty multiplayerToken = new SimpleStringProperty();
private final Map<String, Object> unknownFields = new HashMap<>();
private transient ObservableHelper helper = new ObservableHelper(this);
public GlobalConfig() {
PropertyUtils.attachListener(this, helper);
}
@Override
public void addListener(InvalidationListener listener) {
helper.addListener(listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper.removeListener(listener);
}
public String toJson() {
return CONFIG_GSON.toJson(this);
}
@Override
public GlobalConfig clone() {
return fromJson(this.toJson());
}
public int getAgreementVersion() {
return agreementVersion.get();
}
public IntegerProperty agreementVersionProperty() {
return agreementVersion;
}
public void setAgreementVersion(int agreementVersion) {
this.agreementVersion.set(agreementVersion);
}
public String getMultiplayerToken() {
return multiplayerToken.get();
}
public StringProperty multiplayerTokenProperty() {
return multiplayerToken;
}
public void setMultiplayerToken(String multiplayerToken) {
this.multiplayerToken.set(multiplayerToken);
}
public static class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
"agreementVersion",
"multiplayerToken"
));
@Override
public JsonElement serialize(GlobalConfig src, Type typeOfSrc, JsonSerializationContext context) {
if (src == null) {
return JsonNull.INSTANCE;
}
JsonObject jsonObject = new JsonObject();
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
jsonObject.add("multiplayerToken", context.serialize(src.getMultiplayerToken()));
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
jsonObject.add(entry.getKey(), context.serialize(entry.getValue()));
}
return jsonObject;
}
@Override
public GlobalConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!(json instanceof JsonObject)) return null;
JsonObject obj = (JsonObject) json;
GlobalConfig config = new GlobalConfig();
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
config.setMultiplayerToken(Optional.ofNullable(obj.get("multiplayerToken")).map(JsonElement::getAsString).orElse(null));
for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
if (!knownFields.contains(entry.getKey())) {
config.unknownFields.put(entry.getKey(), context.deserialize(entry.getValue(), Object.class));
}
}
return config;
}
}
}

View File

@ -0,0 +1,219 @@
package com.tungsten.fcl.setting;
import static com.tungsten.fcl.util.FXUtils.onInvalidating;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.tungsten.fcl.game.FCLCacheRepository;
import com.tungsten.fcl.game.FCLGameRepository;
import com.tungsten.fcl.util.WeakListenerHolder;
import com.tungsten.fclcore.download.DefaultDependencyManager;
import com.tungsten.fclcore.download.DownloadProvider;
import com.tungsten.fclcore.event.EventBus;
import com.tungsten.fclcore.event.EventPriority;
import com.tungsten.fclcore.event.RefreshedVersionsEvent;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.property.ObjectProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyObjectProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyObjectWrapper;
import com.tungsten.fclcore.fakefx.beans.property.SimpleObjectProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty;
import com.tungsten.fclcore.fakefx.beans.property.StringProperty;
import com.tungsten.fclcore.game.Version;
import com.tungsten.fclcore.util.ToStringBuilder;
import com.tungsten.fclcore.util.fakefx.ObservableHelper;
import java.io.File;
import java.lang.reflect.Type;
import java.util.Optional;
@JsonAdapter(Profile.Serializer.class)
public final class Profile implements Observable {
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private final FCLGameRepository repository;
private final StringProperty selectedVersion = new SimpleStringProperty();
public StringProperty selectedVersionProperty() {
return selectedVersion;
}
public String getSelectedVersion() {
return selectedVersion.get();
}
public void setSelectedVersion(String selectedVersion) {
this.selectedVersion.set(selectedVersion);
}
private final ObjectProperty<File> gameDir;
public ObjectProperty<File> gameDirProperty() {
return gameDir;
}
public File getGameDir() {
return gameDir.get();
}
public void setGameDir(File gameDir) {
this.gameDir.set(gameDir);
}
private final ReadOnlyObjectWrapper<VersionSetting> global = new ReadOnlyObjectWrapper<>(this, "global");
public ReadOnlyObjectProperty<VersionSetting> globalProperty() {
return global.getReadOnlyProperty();
}
public VersionSetting getGlobal() {
return global.get();
}
private final SimpleStringProperty name;
public StringProperty nameProperty() {
return name;
}
public String getName() {
return name.get();
}
public void setName(String name) {
this.name.set(name);
}
public Profile(String name) {
this(name, new File(".minecraft"));
}
public Profile(String name, File initialGameDir) {
this(name, initialGameDir, new VersionSetting());
}
public Profile(String name, File initialGameDir, VersionSetting global) {
this(name, initialGameDir, global, null);
}
public Profile(String name, File initialGameDir, VersionSetting global, String selectedVersion) {
this.name = new SimpleStringProperty(this, "name", name);
gameDir = new SimpleObjectProperty<>(this, "gameDir", initialGameDir);
repository = new FCLGameRepository(this, initialGameDir);
this.global.set(global == null ? new VersionSetting() : global);
this.selectedVersion.set(selectedVersion);
gameDir.addListener((a, b, newValue) -> repository.changeDirectory(newValue));
this.selectedVersion.addListener(o -> checkSelectedVersion());
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST));
addPropertyChangedListener(onInvalidating(this::invalidate));
}
private void checkSelectedVersion() {
if (!repository.isLoaded()) return;
String newValue = selectedVersion.get();
if (!repository.hasVersion(newValue)) {
Optional<String> version = repository.getVersions().stream().findFirst().map(Version::getId);
if (version.isPresent())
selectedVersion.setValue(version.get());
else if (newValue != null)
selectedVersion.setValue(null);
}
}
public FCLGameRepository getRepository() {
return repository;
}
public DefaultDependencyManager getDependency() {
return getDependency(DownloadProviders.getDownloadProvider());
}
public DefaultDependencyManager getDependency(DownloadProvider downloadProvider) {
return new DefaultDependencyManager(repository, downloadProvider, FCLCacheRepository.REPOSITORY);
}
public VersionSetting getVersionSetting(String id) {
return repository.getVersionSetting(id);
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("gameDir", getGameDir())
.append("name", getName())
.toString();
}
private void addPropertyChangedListener(InvalidationListener listener) {
name.addListener(listener);
global.addListener(listener);
gameDir.addListener(listener);
global.get().addPropertyChangedListener(listener);
selectedVersion.addListener(listener);
}
private ObservableHelper observableHelper = new ObservableHelper(this);
@Override
public void addListener(InvalidationListener listener) {
observableHelper.addListener(listener);
}
@Override
public void removeListener(InvalidationListener listener) {
observableHelper.removeListener(listener);
}
private void invalidate() {
observableHelper.invalidate();
}
public static class ProfileVersion {
private final Profile profile;
private final String version;
public ProfileVersion(Profile profile, String version) {
this.profile = profile;
this.version = version;
}
public Profile getProfile() {
return profile;
}
public String getVersion() {
return version;
}
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
@Override
public JsonElement serialize(Profile src, Type typeOfSrc, JsonSerializationContext context) {
if (src == null)
return JsonNull.INSTANCE;
JsonObject jsonObject = new JsonObject();
jsonObject.add("global", context.serialize(src.getGlobal()));
jsonObject.addProperty("gameDir", src.getGameDir().getPath());
jsonObject.addProperty("selectedMinecraftVersion", src.getSelectedVersion());
return jsonObject;
}
@Override
public Profile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject)) return null;
JsonObject obj = (JsonObject) json;
String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse("");
return new Profile("Default",
new File(gameDir),
context.deserialize(obj.get("global"), VersionSetting.class),
Optional.ofNullable(obj.get("selectedMinecraftVersion")).map(JsonElement::getAsString).orElse(""));
}
}
}

View File

@ -0,0 +1,181 @@
package com.tungsten.fcl.setting;
import static com.tungsten.fcl.setting.ConfigHolder.config;
import static com.tungsten.fcl.util.FXUtils.onInvalidating;
import static com.tungsten.fclcore.fakefx.collections.FXCollections.observableArrayList;
import com.tungsten.fcl.R;
import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.event.EventBus;
import com.tungsten.fclcore.event.RefreshedVersionsEvent;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.property.ObjectProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyListProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyListWrapper;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyStringProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyStringWrapper;
import com.tungsten.fclcore.fakefx.beans.property.SimpleObjectProperty;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
public final class Profiles {
private Profiles() {
}
private static final ObservableList<Profile> profiles = observableArrayList(profile -> new Observable[] { profile });
private static final ReadOnlyListWrapper<Profile> profilesWrapper = new ReadOnlyListWrapper<>(profiles);
private static ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<Profile>() {
{
profiles.addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
if (!initialized)
return;
Profile profile = get();
if (profiles.isEmpty()) {
if (profile != null) {
set(null);
return;
}
} else {
if (!profiles.contains(profile)) {
set(profiles.get(0));
return;
}
}
config().setSelectedProfile(profile == null ? "" : profile.getName());
if (profile != null) {
if (profile.getRepository().isLoaded())
selectedVersion.bind(profile.selectedVersionProperty());
else {
selectedVersion.unbind();
selectedVersion.set(null);
// bind when repository was reloaded.
profile.getRepository().refreshVersionsAsync().start();
}
} else {
selectedVersion.unbind();
selectedVersion.set(null);
}
}
};
private static void checkProfiles() {
if (profiles.isEmpty()) {
Profile current = new Profile(FCLPath.CONTEXT.getString(R.string.profile_shared), new File(FCLPath.SHARED_COMMON_DIR), new VersionSetting(), null);
Profile home = new Profile(FCLPath.CONTEXT.getString(R.string.profile_private), new File(FCLPath.PRIVATE_COMMON_DIR));
profiles.addAll(current, home);
}
}
/**
* True if {@link #init()} hasn't been called.
*/
private static boolean initialized = false;
static {
profiles.addListener(onInvalidating(Profiles::updateProfileStorages));
profiles.addListener(onInvalidating(Profiles::checkProfiles));
selectedProfile.addListener((a, b, newValue) -> {
if (newValue != null)
newValue.getRepository().refreshVersionsAsync().start();
});
}
private static void updateProfileStorages() {
// don't update the underlying storage before data loading is completed
// otherwise it might cause data loss
if (!initialized)
return;
// update storage
config().getConfigurations().clear();
config().getConfigurations().putAll(profiles.stream().collect(Collectors.toMap(Profile::getName, it -> it)));
}
/**
* Called when it's ready to load profiles from {@link ConfigHolder#config()}.
*/
static void init() {
if (initialized)
throw new IllegalStateException("Already initialized");
HashSet<String> names = new HashSet<>();
config().getConfigurations().forEach((name, profile) -> {
if (!names.add(name)) return;
profiles.add(profile);
profile.setName(name);
});
checkProfiles();
initialized = true;
selectedProfile.set(
profiles.stream()
.filter(it -> it.getName().equals(config().getSelectedProfile()))
.findFirst()
.orElse(profiles.get(0)));
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> {
Profile profile = selectedProfile.get();
if (profile != null && profile.getRepository() == event.getSource()) {
selectedVersion.bind(profile.selectedVersionProperty());
for (Consumer<Profile> listener : versionsListeners)
listener.accept(profile);
}
});
}
public static ObservableList<Profile> getProfiles() {
return profiles;
}
public static ReadOnlyListProperty<Profile> profilesProperty() {
return profilesWrapper.getReadOnlyProperty();
}
public static Profile getSelectedProfile() {
return selectedProfile.get();
}
public static void setSelectedProfile(Profile profile) {
selectedProfile.set(profile);
}
public static ObjectProperty<Profile> selectedProfileProperty() {
return selectedProfile;
}
private static final ReadOnlyStringWrapper selectedVersion = new ReadOnlyStringWrapper();
public static ReadOnlyStringProperty selectedVersionProperty() {
return selectedVersion.getReadOnlyProperty();
}
// Guaranteed that the repository is loaded.
public static String getSelectedVersion() {
return selectedVersion.get();
}
private static final List<Consumer<Profile>> versionsListeners = new ArrayList<>(4);
public static void registerVersionsListener(Consumer<Profile> listener) {
Profile profile = getSelectedProfile();
if (profile != null && profile.getRepository().isLoaded())
listener.accept(profile);
versionsListeners.add(listener);
}
}

View File

@ -0,0 +1,37 @@
package com.tungsten.fcl.setting;
import static com.tungsten.fcl.setting.ConfigHolder.config;
import com.tungsten.fcl.game.FCLCacheRepository;
import com.tungsten.fclcore.fakefx.beans.binding.Bindings;
import com.tungsten.fclcore.util.CacheRepository;
public final class Settings {
private static Settings instance;
public static Settings instance() {
if (instance == null) {
throw new IllegalStateException("Settings hasn't been initialized");
}
return instance;
}
/**
* Should be called from {@link ConfigHolder#init()}.
*/
static void init() {
instance = new Settings();
}
private Settings() {
DownloadProviders.init();
Accounts.init();
Profiles.init();
AuthlibInjectorServers.init();
CacheRepository.setInstance(FCLCacheRepository.REPOSITORY);
FCLCacheRepository.REPOSITORY.directoryProperty().bind(Bindings.createStringBinding(() -> config().getCommonDirectory(), config().commonDirectoryProperty()));
}
}

View File

@ -0,0 +1,459 @@
package com.tungsten.fcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty;
import com.tungsten.fclcore.fakefx.beans.property.IntegerProperty;
import com.tungsten.fclcore.fakefx.beans.property.ObjectProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleBooleanProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleIntegerProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleObjectProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty;
import com.tungsten.fclcore.fakefx.beans.property.StringProperty;
import com.tungsten.fclcore.game.GameDirectoryType;
import com.tungsten.fclcore.game.JavaVersion;
import com.tungsten.fclcore.game.ProcessPriority;
import com.tungsten.fclcore.game.Version;
import com.tungsten.fclcore.task.Schedulers;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.Lang;
import com.tungsten.fclcore.util.platform.MemoryUtils;
import com.tungsten.fclcore.util.versioning.VersionNumber;
import java.lang.reflect.Type;
import java.util.Optional;
@JsonAdapter(VersionSetting.Serializer.class)
public final class VersionSetting implements Cloneable {
private boolean global = false;
public boolean isGlobal() {
return global;
}
public void setGlobal(boolean global) {
this.global = global;
}
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", true);
public BooleanProperty usesGlobalProperty() {
return usesGlobalProperty;
}
/**
* HMCL Version Settings have been divided into 2 parts.
* 1. Global settings.
* 2. Version settings.
* If a version claims that it uses global settings, its version setting will be disabled.
* <p>
* Defaults false because if one version uses global first, custom version file will not be generated.
*/
public boolean isUsesGlobal() {
return usesGlobalProperty.get();
}
public void setUsesGlobal(boolean usesGlobal) {
usesGlobalProperty.set(usesGlobal);
}
// java
private final ObjectProperty<Integer> javaProperty = new SimpleObjectProperty<>(this, "java", 0);
public ObjectProperty<Integer> javaProperty() {
return javaProperty;
}
/**
* Java version or "Custom" if user customizes java directory, "Default" if the jvm that this app relies on.
*/
public int getJava() {
return javaProperty.get();
}
public void setJava(int java) {
javaProperty.set(java);
}
private final StringProperty permSizeProperty = new SimpleStringProperty(this, "permSize", "");
public StringProperty permSizeProperty() {
return permSizeProperty;
}
/**
* The permanent generation size of JVM garbage collection.
*/
public String getPermSize() {
return permSizeProperty.get();
}
public void setPermSize(String permSize) {
permSizeProperty.set(permSize);
}
private final IntegerProperty maxMemoryProperty = new SimpleIntegerProperty(this, "maxMemory", MemoryUtils.findBestRAMAllocation(FCLPath.CONTEXT));
public IntegerProperty maxMemoryProperty() {
return maxMemoryProperty;
}
/**
* The maximum memory/MB that JVM can allocate for heap.
*/
public int getMaxMemory() {
return maxMemoryProperty.get();
}
public void setMaxMemory(int maxMemory) {
maxMemoryProperty.set(maxMemory);
}
/**
* The minimum memory that JVM can allocate for heap.
*/
private final ObjectProperty<Integer> minMemoryProperty = new SimpleObjectProperty<>(this, "minMemory", null);
public ObjectProperty<Integer> minMemoryProperty() {
return minMemoryProperty;
}
public Integer getMinMemory() {
return minMemoryProperty.get();
}
public void setMinMemory(Integer minMemory) {
minMemoryProperty.set(minMemory);
}
private final BooleanProperty autoMemory = new SimpleBooleanProperty(this, "autoMemory", true);
public boolean isAutoMemory() {
return autoMemory.get();
}
public BooleanProperty autoMemoryProperty() {
return autoMemory;
}
public void setAutoMemory(boolean autoMemory) {
this.autoMemory.set(autoMemory);
}
// options
private final StringProperty javaArgsProperty = new SimpleStringProperty(this, "javaArgs", "");
public StringProperty javaArgsProperty() {
return javaArgsProperty;
}
/**
* The user customized arguments passed to JVM.
*/
public String getJavaArgs() {
return javaArgsProperty.get();
}
public void setJavaArgs(String javaArgs) {
javaArgsProperty.set(javaArgs);
}
private final StringProperty minecraftArgsProperty = new SimpleStringProperty(this, "minecraftArgs", "");
public StringProperty minecraftArgsProperty() {
return minecraftArgsProperty;
}
/**
* The user customized arguments passed to Minecraft.
*/
public String getMinecraftArgs() {
return minecraftArgsProperty.get();
}
public void setMinecraftArgs(String minecraftArgs) {
minecraftArgsProperty.set(minecraftArgs);
}
private final BooleanProperty notCheckJVMProperty = new SimpleBooleanProperty(this, "notCheckJVM", false);
public BooleanProperty notCheckJVMProperty() {
return notCheckJVMProperty;
}
/**
* True if HMCL does not check JVM validity.
*/
public boolean isNotCheckJVM() {
return notCheckJVMProperty.get();
}
public void setNotCheckJVM(boolean notCheckJVM) {
notCheckJVMProperty.set(notCheckJVM);
}
private final BooleanProperty notCheckGameProperty = new SimpleBooleanProperty(this, "notCheckGame", false);
public BooleanProperty notCheckGameProperty() {
return notCheckGameProperty;
}
/**
* True if HMCL does not check game's completeness.
*/
public boolean isNotCheckGame() {
return notCheckGameProperty.get();
}
public void setNotCheckGame(boolean notCheckGame) {
notCheckGameProperty.set(notCheckGame);
}
// Minecraft settings.
private final StringProperty serverIpProperty = new SimpleStringProperty(this, "serverIp", "");
public StringProperty serverIpProperty() {
return serverIpProperty;
}
/**
* The server ip that will be entered after Minecraft successfully loaded ly.
* <p>
* Format: ip:port or without port.
*/
public String getServerIp() {
return serverIpProperty.get();
}
public void setServerIp(String serverIp) {
serverIpProperty.set(serverIp);
}
private final BooleanProperty fullscreenProperty = new SimpleBooleanProperty(this, "fullscreen", false);
public BooleanProperty fullscreenProperty() {
return fullscreenProperty;
}
/**
* True if Minecraft started in fullscreen mode.
*/
public boolean isFullscreen() {
return fullscreenProperty.get();
}
public void setFullscreen(boolean fullscreen) {
fullscreenProperty.set(fullscreen);
}
private final IntegerProperty widthProperty = new SimpleIntegerProperty(this, "width", 854);
public IntegerProperty widthProperty() {
return widthProperty;
}
/**
* The width of Minecraft window, defaults 800.
* <p>
* The field saves int value.
* String type prevents unexpected value from JsonParseException.
* We can only reset this field instead of recreating the whole setting file.
*/
public int getWidth() {
return widthProperty.get();
}
public void setWidth(int width) {
widthProperty.set(width);
}
private final IntegerProperty heightProperty = new SimpleIntegerProperty(this, "height", 480);
public IntegerProperty heightProperty() {
return heightProperty;
}
/**
* The height of Minecraft window, defaults 480.
* <p>
* The field saves int value.
* String type prevents unexpected value from JsonParseException.
* We can only reset this field instead of recreating the whole setting file.
*/
public int getHeight() {
return heightProperty.get();
}
public void setHeight(int height) {
heightProperty.set(height);
}
/**
* 0 - .minecraft<br/>
* 1 - .minecraft/versions/&lt;version&gt;/<br/>
*/
private final ObjectProperty<GameDirectoryType> gameDirTypeProperty = new SimpleObjectProperty<>(this, "gameDirType", GameDirectoryType.ROOT_FOLDER);
public ObjectProperty<GameDirectoryType> gameDirTypeProperty() {
return gameDirTypeProperty;
}
public GameDirectoryType getGameDirType() {
return gameDirTypeProperty.get();
}
public void setGameDirType(GameDirectoryType gameDirType) {
gameDirTypeProperty.set(gameDirType);
}
private final ObjectProperty<ProcessPriority> processPriorityProperty = new SimpleObjectProperty<>(this, "processPriority", ProcessPriority.NORMAL);
public ObjectProperty<ProcessPriority> processPriorityProperty() {
return processPriorityProperty;
}
public ProcessPriority getProcessPriority() {
return processPriorityProperty.get();
}
public void setProcessPriority(ProcessPriority processPriority) {
processPriorityProperty.set(processPriority);
}
// launcher settings
public Task<JavaVersion> getJavaVersion(Version version) {
return Task.runAsync(Schedulers.androidUIThread(), () -> {
if (getJava() != 0 && getJava() != 1 && getJava() != 2) {
setJava(0);
}
}).thenSupplyAsync(() -> {
if (getJava() == 0) {
return JavaVersion.getSuitableJavaVersion(version);
}
else {
return JavaVersion.getJavaFromId(getJava());
}
});
}
public void addPropertyChangedListener(InvalidationListener listener) {
usesGlobalProperty.addListener(listener);
javaProperty.addListener(listener);
permSizeProperty.addListener(listener);
maxMemoryProperty.addListener(listener);
minMemoryProperty.addListener(listener);
autoMemory.addListener(listener);
javaArgsProperty.addListener(listener);
minecraftArgsProperty.addListener(listener);
notCheckGameProperty.addListener(listener);
notCheckJVMProperty.addListener(listener);
serverIpProperty.addListener(listener);
fullscreenProperty.addListener(listener);
widthProperty.addListener(listener);
heightProperty.addListener(listener);
gameDirTypeProperty.addListener(listener);
processPriorityProperty.addListener(listener);
}
@Override
public VersionSetting clone() {
VersionSetting versionSetting = new VersionSetting();
versionSetting.setUsesGlobal(isUsesGlobal());
versionSetting.setJava(getJava());
versionSetting.setPermSize(getPermSize());
versionSetting.setMaxMemory(getMaxMemory());
versionSetting.setMinMemory(getMinMemory());
versionSetting.setAutoMemory(isAutoMemory());
versionSetting.setJavaArgs(getJavaArgs());
versionSetting.setMinecraftArgs(getMinecraftArgs());
versionSetting.setNotCheckGame(isNotCheckGame());
versionSetting.setNotCheckJVM(isNotCheckJVM());
versionSetting.setServerIp(getServerIp());
versionSetting.setFullscreen(isFullscreen());
versionSetting.setWidth(getWidth());
versionSetting.setHeight(getHeight());
versionSetting.setGameDirType(getGameDirType());
versionSetting.setProcessPriority(getProcessPriority());
return versionSetting;
}
public static class Serializer implements JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
@Override
public JsonElement serialize(VersionSetting src, Type typeOfSrc, JsonSerializationContext context) {
if (src == null) return JsonNull.INSTANCE;
JsonObject obj = new JsonObject();
obj.addProperty("usesGlobal", src.isUsesGlobal());
obj.addProperty("javaArgs", src.getJavaArgs());
obj.addProperty("minecraftArgs", src.getMinecraftArgs());
obj.addProperty("maxMemory", src.getMaxMemory() <= 0 ? MemoryUtils.findBestRAMAllocation(FCLPath.CONTEXT) : src.getMaxMemory());
obj.addProperty("minMemory", src.getMinMemory());
obj.addProperty("autoMemory", src.isAutoMemory());
obj.addProperty("permSize", src.getPermSize());
obj.addProperty("width", src.getWidth());
obj.addProperty("height", src.getHeight());
obj.addProperty("serverIp", src.getServerIp());
obj.addProperty("java", src.getJava());
obj.addProperty("fullscreen", src.isFullscreen());
obj.addProperty("notCheckGame", src.isNotCheckGame());
obj.addProperty("notCheckJVM", src.isNotCheckJVM());
obj.addProperty("processPriority", src.getProcessPriority().ordinal());
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
return obj;
}
@Override
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject))
return null;
JsonObject obj = (JsonObject) json;
int maxMemoryN = parseJsonPrimitive(Optional.ofNullable(obj.get("maxMemory")).map(JsonElement::getAsJsonPrimitive).orElse(null), MemoryUtils.findBestRAMAllocation(FCLPath.CONTEXT));
if (maxMemoryN <= 0) maxMemoryN = MemoryUtils.findBestRAMAllocation(FCLPath.CONTEXT);
VersionSetting vs = new VersionSetting();
vs.setUsesGlobal(Optional.ofNullable(obj.get("usesGlobal")).map(JsonElement::getAsBoolean).orElse(false));
vs.setJavaArgs(Optional.ofNullable(obj.get("javaArgs")).map(JsonElement::getAsString).orElse(""));
vs.setMinecraftArgs(Optional.ofNullable(obj.get("minecraftArgs")).map(JsonElement::getAsString).orElse(""));
vs.setMaxMemory(maxMemoryN);
vs.setMinMemory(Optional.ofNullable(obj.get("minMemory")).map(JsonElement::getAsInt).orElse(null));
vs.setAutoMemory(Optional.ofNullable(obj.get("autoMemory")).map(JsonElement::getAsBoolean).orElse(true));
vs.setPermSize(Optional.ofNullable(obj.get("permSize")).map(JsonElement::getAsString).orElse(""));
vs.setWidth(Optional.ofNullable(obj.get("width")).map(JsonElement::getAsJsonPrimitive).map(this::parseJsonPrimitive).orElse(0));
vs.setHeight(Optional.ofNullable(obj.get("height")).map(JsonElement::getAsJsonPrimitive).map(this::parseJsonPrimitive).orElse(0));
vs.setServerIp(Optional.ofNullable(obj.get("serverIp")).map(JsonElement::getAsString).orElse(""));
vs.setJava(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsInt).orElse(0));
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
vs.setProcessPriority(ProcessPriority.values()[Optional.ofNullable(obj.get("processPriority")).map(JsonElement::getAsInt).orElse(ProcessPriority.NORMAL.ordinal())]);
vs.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(GameDirectoryType.ROOT_FOLDER.ordinal())]);
return vs;
}
private int parseJsonPrimitive(JsonPrimitive primitive) {
return parseJsonPrimitive(primitive, 0);
}
private int parseJsonPrimitive(JsonPrimitive primitive, int defaultValue) {
if (primitive == null)
return defaultValue;
else if (primitive.isNumber())
return primitive.getAsInt();
else
return Lang.parseInt(primitive.getAsString(), defaultValue);
}
}
}

View File

@ -0,0 +1,35 @@
package com.tungsten.fcl.util;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.WeakInvalidationListener;
import com.tungsten.fclcore.fakefx.beans.value.ObservableValue;
import java.util.function.Consumer;
public final class FXUtils {
public static InvalidationListener onInvalidating(Runnable action) {
return arg -> action.run();
}
public static <T> void onChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
consumer.accept(value.getValue());
onChange(value, consumer);
}
public static <T> void onChange(ObservableValue<T> value, Consumer<T> consumer) {
value.addListener((a, b, c) -> consumer.accept(c));
}
public static InvalidationListener observeWeak(Runnable runnable, Observable... observables) {
InvalidationListener originalListener = observable -> runnable.run();
WeakInvalidationListener listener = new WeakInvalidationListener(originalListener);
for (Observable observable : observables) {
observable.addListener(listener);
}
runnable.run();
return originalListener;
}
}

View File

@ -0,0 +1,259 @@
package com.tungsten.fcl.util;
import static com.tungsten.fclcore.util.Logging.LOG;
import static com.tungsten.fclcore.util.Pair.pair;
import com.tungsten.fclcore.mod.RemoteModRepository;
import com.tungsten.fclcore.util.Pair;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.io.IOUtils;
import org.jetbrains.annotations.Nullable;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
/**
* Parser for mod_data.txt
*
* @see <a href="https://www.mcmod.cn">mcmod.cn</a>
*/
public enum ModTranslations {
MOD("/assets/mod_data.txt") {
@Override
public String getMcmodUrl(Mod mod) {
return String.format("https://www.mcmod.cn/class/%s.html", mod.getMcmod());
}
},
MODPACK("/assets/modpack_data.txt") {
@Override
public String getMcmodUrl(Mod mod) {
return String.format("https://www.mcmod.cn/modpack/%s.html", mod.getMcmod());
}
},
EMPTY("") {
@Override
public String getMcmodUrl(Mod mod) {
return "";
}
};
public static ModTranslations getTranslationsByRepositoryType(RemoteModRepository.Type type) {
switch (type) {
case MOD:
return MOD;
case MODPACK:
return MODPACK;
default:
return EMPTY;
}
}
private final String resourceName;
private List<Mod> mods;
private Map<String, Mod> modIdMap; // mod id -> mod
private Map<String, Mod> curseForgeMap; // curseforge id -> mod
private List<Pair<String, Mod>> keywords;
private int maxKeywordLength = -1;
ModTranslations(String resourceName) {
this.resourceName = resourceName;
}
@Nullable
public Mod getModByCurseForgeId(String id) {
if (StringUtils.isBlank(id) || !loadCurseForgeMap()) return null;
return curseForgeMap.get(id);
}
@Nullable
public Mod getModById(String id) {
if (StringUtils.isBlank(id) || !loadModIdMap()) return null;
return modIdMap.get(id);
}
public abstract String getMcmodUrl(Mod mod);
public List<Mod> searchMod(String query) {
if (!loadKeywords()) return Collections.emptyList();
StringBuilder newQuery = query.chars()
.filter(ch -> !Character.isSpaceChar(ch))
.collect(StringBuilder::new, (sb, value) -> sb.append((char)value), StringBuilder::append);
query = newQuery.toString();
StringUtils.LongestCommonSubsequence lcs = new StringUtils.LongestCommonSubsequence(query.length(), maxKeywordLength);
List<Pair<Integer, Mod>> modList = new ArrayList<>();
for (Pair<String, Mod> keyword : keywords) {
int value = lcs.calc(query, keyword.getKey());
if (value >= Math.max(1, query.length() - 3)) {
modList.add(pair(value, keyword.getValue()));
}
}
return modList.stream()
.sorted((a, b) -> -a.getKey().compareTo(b.getKey()))
.map(Pair::getValue)
.collect(Collectors.toList());
}
private boolean loadFromResource() {
if (mods != null) return true;
if (StringUtils.isBlank(resourceName)) {
mods = Collections.emptyList();
return true;
}
try {
String modData = IOUtils.readFullyAsString(ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8);
mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList());
return true;
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to load " + resourceName, e);
return false;
}
}
private boolean loadCurseForgeMap() {
if (curseForgeMap != null) {
return true;
}
if (mods == null) {
if (!loadFromResource()) return false;
}
curseForgeMap = new HashMap<>();
for (Mod mod : mods) {
if (StringUtils.isNotBlank(mod.getCurseforge())) {
curseForgeMap.put(mod.getCurseforge(), mod);
}
}
return true;
}
private boolean loadModIdMap() {
if (modIdMap != null) {
return true;
}
if (mods == null) {
if (!loadFromResource()) return false;
}
modIdMap = new HashMap<>();
for (Mod mod : mods) {
for (String id : mod.getModIds()) {
if (StringUtils.isNotBlank(id) && !"examplemod".equals(id)) {
modIdMap.put(id, mod);
}
}
}
return true;
}
private boolean loadKeywords() {
if (keywords != null) {
return true;
}
if (mods == null) {
if (!loadFromResource()) return false;
}
keywords = new ArrayList<>();
maxKeywordLength = -1;
for (Mod mod : mods) {
if (StringUtils.isNotBlank(mod.getName())) {
keywords.add(pair(mod.getName(), mod));
maxKeywordLength = Math.max(maxKeywordLength, mod.getName().length());
}
if (StringUtils.isNotBlank(mod.getSubname())) {
keywords.add(pair(mod.getSubname(), mod));
maxKeywordLength = Math.max(maxKeywordLength, mod.getSubname().length());
}
if (StringUtils.isNotBlank(mod.getAbbr())) {
keywords.add(pair(mod.getAbbr(), mod));
maxKeywordLength = Math.max(maxKeywordLength, mod.getAbbr().length());
}
}
return true;
}
public static class Mod {
private final String curseforge;
private final String mcmod;
private final String mcbbs;
private final List<String> modIds;
private final String name;
private final String subname;
private final String abbr;
public Mod(String line) {
String[] items = line.split(";", -1);
if (items.length != 7) {
throw new IllegalArgumentException("Illegal mod data line, 7 items expected " + line);
}
curseforge = items[0];
mcmod = items[1];
mcbbs = items[2];
modIds = Collections.unmodifiableList(Arrays.asList(items[3].split(",")));
name = items[4];
subname = items[5];
abbr = items[6];
}
public Mod(String curseforge, String mcmod, String mcbbs, List<String> modIds, String name, String subname, String abbr) {
this.curseforge = curseforge;
this.mcmod = mcmod;
this.mcbbs = mcbbs;
this.modIds = modIds;
this.name = name;
this.subname = subname;
this.abbr = abbr;
}
public String getDisplayName() {
StringBuilder builder = new StringBuilder();
if (StringUtils.isNotBlank(abbr)) {
builder.append("[").append(abbr.trim()).append("] ");
}
builder.append(name);
if (StringUtils.isNotBlank(subname)) {
builder.append(" (").append(subname).append(")");
}
return builder.toString();
}
public String getCurseforge() {
return curseforge;
}
public String getMcmod() {
return mcmod;
}
public String getMcbbs() {
return mcbbs;
}
public List<String> getModIds() {
return modIds;
}
public String getName() {
return name;
}
public String getSubname() {
return subname;
}
public String getAbbr() {
return abbr;
}
}
}

View File

@ -0,0 +1,20 @@
package com.tungsten.fcl.util;
import java.io.InputStream;
public class ResourceNotFoundError extends Error {
public ResourceNotFoundError(String message) {
super(message);
}
public ResourceNotFoundError(String message, Throwable cause) {
super(message, cause);
}
public static InputStream getResourceAsStream(String url) {
InputStream stream = ResourceNotFoundError.class.getResourceAsStream(url);
if (stream == null)
throw new ResourceNotFoundError("Resource not found: " + url);
return stream;
}
}

View File

@ -0,0 +1,41 @@
package com.tungsten.fcl.util;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.WeakInvalidationListener;
import com.tungsten.fclcore.fakefx.beans.value.ChangeListener;
import com.tungsten.fclcore.fakefx.beans.value.WeakChangeListener;
import com.tungsten.fclcore.fakefx.collections.ListChangeListener;
import com.tungsten.fclcore.fakefx.collections.WeakListChangeListener;
import java.util.LinkedList;
import java.util.List;
public class WeakListenerHolder {
private List<Object> refs = new LinkedList<>();
public WeakListenerHolder() {
}
public WeakInvalidationListener weak(InvalidationListener listener) {
refs.add(listener);
return new WeakInvalidationListener(listener);
}
public <T> WeakChangeListener<T> weak(ChangeListener<T> listener) {
refs.add(listener);
return new WeakChangeListener<>(listener);
}
public <T> WeakListChangeListener<T> weak(ListChangeListener<T> listener) {
refs.add(listener);
return new WeakListChangeListener<>(listener);
}
public void add(Object obj) {
refs.add(obj);
}
public boolean remove(Object obj) {
return refs.remove(obj);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -12,4 +12,48 @@
<string name="download">下载资源</string> <string name="download">下载资源</string>
<string name="multiplayer">多人联机</string> <string name="multiplayer">多人联机</string>
<string name="setting">全局设置</string> <string name="setting">全局设置</string>
<string name="account_methods_offline">离线账户</string>
<string name="account_methods_yggdrasil">Mojang 账户</string>
<string name="account_methods_authlib_injector">外置账户</string>
<string name="account_methods_microsoft">微软账户</string>
<string name="account_failed_no_character">该账户中无角色。</string>
<string name="account_failed_connect_authentication_server">无法连接认证服务器,请检查网络。</string>
<string name="account_failed_server_response_malformed">无效的返回码,该认证服务器可能失效。</string>
<string name="account_failed_invalid_credentials">不正确的密码或被限速,请稍后再试。</string>
<string name="account_failed_invalid_token">请尝试重新登录。</string>
<string name="account_failed_invalid_password">无效的密码</string>
<string name="account_failed_migration">你的账户需要迁移至微软账户,如果已经迁移,请尝试重新登录。</string>
<string name="account_failed_injector_download_failure">无法下载 authlib-injector。 请检查网络,或修改下载源。</string>
<string name="account_failed_character_deleted">该角色已被删除。</string>
<string name="account_failed_wrong_account">登录了错误的账户。</string>
<string name="account_skin_invalid_skin">无效的皮肤文件</string>
<string name="account_methods_microsoft_error_add_family">你尚未满 18 岁,需要一位成年将你加入至家庭中。</string>
<string name="account_methods_microsoft_error_country_unavailable">Xbox Live 不支持您所在的国家/地区。</string>
<string name="account_methods_microsoft_error_missing_xbox_account">你的微软账户没有链接到 Xbox 账户,请先创建。</string>
<string name="account_methods_microsoft_error_unknown">登录失败</string>
<string name="account_methods_microsoft_error_no_character">你的账户尚未获取 Minecraft : Java Edition</string>
<string name="account_methods_microsoft_error_add_family_probably">请检查并确保年龄设置大于 18 岁。</string>
<string name="account_methods_microsoft_close_page">Microsoft 账户登录完成</string>
<string name="download_code_404">未找到文件</string>
<string name="exception_access_denied">无法获取文件。</string>
<string name="exception_artifact_malformed">无法校验文件。</string>
<string name="exception_ssl_handshake">缺少 SSL 证书。</string>
<string name="install_failed_downloading_timeout">下载超时</string>
<string name="install_failed_downloading_detail">无法下载</string>
<string name="launch_state_dependencies">检查依赖</string>
<string name="launch_state_modpack">下载依赖</string>
<string name="launch_state_java">检查 Java</string>
<string name="launch_state_logging_in">登录</string>
<string name="launch_state_waiting_launching">等待游戏启动</string>
<string name="launch_state_done">完成</string>
<string name="message_cancelled">操作已取消</string>
<string name="profile_shared">共有目录</string>
<string name="profile_private">私有目录</string>
</resources> </resources>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="default_theme_color">#9EFF4A</color> <color name="default_theme_color">#7797CF</color>
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
</resources> </resources>

View File

@ -1,7 +1,9 @@
<resources> <resources>
<string name="app_name" translatable="false">Fold Craft Launcher</string> <string name="app_name" translatable="false">Fold Craft Launcher</string>
<string name="app_version" translatable="false">1_0_0</string>
<string name="splash_title">Welcome to Fold Craft Launcher</string> <string name="splash_title">Welcome to Fold Craft Launcher</string>
<string name="splash_eula_error">Cannot get EULA, please check the network.</string> <string name="splash_eula_error">Cannot get EULA, please check the network_</string>
<string name="splash_eula_next">Agree and Continue</string> <string name="splash_eula_next">Agree and Continue</string>
<string name="splash_runtime_title">Install or update app runtime</string> <string name="splash_runtime_title">Install or update app runtime</string>
<string name="splash_runtime_lwjgl2" translatable="false">LWJGL 2</string> <string name="splash_runtime_lwjgl2" translatable="false">LWJGL 2</string>
@ -18,4 +20,48 @@
<string name="download">Download</string> <string name="download">Download</string>
<string name="multiplayer">Multiplayer</string> <string name="multiplayer">Multiplayer</string>
<string name="setting">Setting</string> <string name="setting">Setting</string>
<string name="account_methods_offline">Offline Account</string>
<string name="account_methods_yggdrasil">Mojang Account</string>
<string name="account_methods_authlib_injector">External Account</string>
<string name="account_methods_microsoft">Microsoft Account</string>
<string name="account_failed_no_character">There are no characters linked to this account.</string>
<string name="account_failed_connect_authentication_server">Unable to contact authentication servers, your Internet connection may be down.</string>
<string name="account_failed_server_response_malformed">Invalid server response, the authentication server may not be working.</string>
<string name="account_failed_invalid_credentials">Incorrect password or rate limited, please try again later.</string>
<string name="account_failed_invalid_token">Please try to re-login again.</string>
<string name="account_failed_invalid_password">Invalid password</string>
<string name="account_failed_migration">Your account needs to be migrated to a Microsoft account. If you already did, you should re-login to your migrated Microsoft account instead.</string>
<string name="account_failed_injector_download_failure">Unable to download authlib-injector. Please check your network, or try switching to a different download mirror.</string>
<string name="account_failed_character_deleted">The character has already been deleted.</string>
<string name="account_failed_wrong_account">You have logged in to the wrong account.</string>
<string name="account_skin_invalid_skin">Invalid skin file</string>
<string name="account_methods_microsoft_error_add_family">Since you are not yet 18 years old, an adult must add you to a family in order for you to play Minecraft.</string>
<string name="account_methods_microsoft_error_country_unavailable">Xbox Live is not available in your current country/region.</string>
<string name="account_methods_microsoft_error_missing_xbox_account">Your Microsoft account does not have a linked Xbox account yet. Please create one before continuing.</string>
<string name="account_methods_microsoft_error_unknown">Failed to log in</string>
<string name="account_methods_microsoft_error_no_character">Your account does not own the Minecraft Java Edition.\nThe game profile may not have been created.</string>
<string name="account_methods_microsoft_error_add_family_probably">Please check if the age indicated in your account settings is at least 18 years old. If not and you believe this is an error, you can go to official website to change it.</string>
<string name="account_methods_microsoft_close_page">Microsoft account authorization is now completed.</string>
<string name="download_code_404">File not found</string>
<string name="exception_access_denied">Unable to access the file.</string>
<string name="exception_artifact_malformed">Cannot verify the integrity of the downloaded files.</string>
<string name="exception_ssl_handshake">Unable to establish SSL connection due to missing SSL certificates in current Java installation.</string>
<string name="install_failed_downloading_timeout">Download timeout</string>
<string name="install_failed_downloading_detail">Unable to download</string>
<string name="launch_state_dependencies">Resolving dependencies</string>
<string name="launch_state_modpack">Downloading dependencies</string>
<string name="launch_state_java">Checking Java version</string>
<string name="launch_state_logging_in">Logging in</string>
<string name="launch_state_waiting_launching">Waiting for the game to launch</string>
<string name="launch_state_done">Completing launch</string>
<string name="message_cancelled">Operation was cancelled</string>
<string name="profile_shared">Shared Directory</string>
<string name="profile_private">Private Directory</string>
</resources> </resources>

View File

@ -37,7 +37,7 @@ dependencies {
implementation 'org.apache.commons:commons-compress:1.21' implementation 'org.apache.commons:commons-compress:1.21'
implementation 'com.moandjiezana.toml:toml4j:0.7.2' implementation 'com.moandjiezana.toml:toml4j:0.7.2'
implementation 'org.jenkins-ci:constant-pool-scanner:1.2' implementation 'org.jenkins-ci:constant-pool-scanner:1.2'
implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.google.code.gson:gson:2.10'
implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.7.0' implementation 'com.google.android.material:material:1.7.0'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'

View File

@ -2,11 +2,11 @@ package com.tungsten.fclcore.auth;
import com.tungsten.fclcore.auth.yggdrasil.Texture; import com.tungsten.fclcore.auth.yggdrasil.Texture;
import com.tungsten.fclcore.auth.yggdrasil.TextureType; import com.tungsten.fclcore.auth.yggdrasil.TextureType;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.binding.Bindings;
import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding;
import com.tungsten.fclcore.util.ToStringBuilder; import com.tungsten.fclcore.util.ToStringBuilder;
import com.tungsten.fclcore.fakefx.Bindings;
import com.tungsten.fclcore.fakefx.InvalidationListener;
import com.tungsten.fclcore.fakefx.ObjectBinding;
import com.tungsten.fclcore.fakefx.Observable;
import com.tungsten.fclcore.util.fakefx.ObservableHelper; import com.tungsten.fclcore.util.fakefx.ObservableHelper;
import java.util.Map; import java.util.Map;

View File

@ -28,8 +28,8 @@ import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.JsonAdapter;
import com.tungsten.fclcore.auth.yggdrasil.YggdrasilService; import com.tungsten.fclcore.auth.yggdrasil.YggdrasilService;
import com.tungsten.fclcore.fakefx.InvalidationListener; import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.Observable; import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.util.fakefx.ObservableHelper; import com.tungsten.fclcore.util.fakefx.ObservableHelper;
@JsonAdapter(AuthlibInjectorServer.Deserializer.class) @JsonAdapter(AuthlibInjectorServer.Deserializer.class)

View File

@ -18,8 +18,8 @@ import com.tungsten.fclcore.auth.ServerResponseMalformedException;
import com.tungsten.fclcore.auth.yggdrasil.Texture; import com.tungsten.fclcore.auth.yggdrasil.Texture;
import com.tungsten.fclcore.auth.yggdrasil.TextureType; import com.tungsten.fclcore.auth.yggdrasil.TextureType;
import com.tungsten.fclcore.auth.yggdrasil.YggdrasilService; import com.tungsten.fclcore.auth.yggdrasil.YggdrasilService;
import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding;
import com.tungsten.fclcore.util.fakefx.BindingMapping; import com.tungsten.fclcore.util.fakefx.BindingMapping;
import com.tungsten.fclcore.fakefx.ObjectBinding;
public class MicrosoftAccount extends OAuthAccount { public class MicrosoftAccount extends OAuthAccount {

View File

@ -22,11 +22,11 @@ import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorDownloadExceptio
import com.tungsten.fclcore.auth.yggdrasil.Texture; import com.tungsten.fclcore.auth.yggdrasil.Texture;
import com.tungsten.fclcore.auth.yggdrasil.TextureModel; import com.tungsten.fclcore.auth.yggdrasil.TextureModel;
import com.tungsten.fclcore.auth.yggdrasil.TextureType; import com.tungsten.fclcore.auth.yggdrasil.TextureType;
import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding;
import com.tungsten.fclcore.game.Arguments; import com.tungsten.fclcore.game.Arguments;
import com.tungsten.fclcore.game.LaunchOptions; import com.tungsten.fclcore.game.LaunchOptions;
import com.tungsten.fclcore.util.StringUtils; import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.ToStringBuilder; import com.tungsten.fclcore.util.ToStringBuilder;
import com.tungsten.fclcore.fakefx.ObjectBinding;
import com.tungsten.fclcore.util.gson.UUIDTypeAdapter; import com.tungsten.fclcore.util.gson.UUIDTypeAdapter;
public class OfflineAccount extends Account { public class OfflineAccount extends Account {

View File

@ -16,8 +16,8 @@ import com.tungsten.fclcore.auth.ClassicAccount;
import com.tungsten.fclcore.auth.CredentialExpiredException; import com.tungsten.fclcore.auth.CredentialExpiredException;
import com.tungsten.fclcore.auth.NoCharacterException; import com.tungsten.fclcore.auth.NoCharacterException;
import com.tungsten.fclcore.auth.ServerResponseMalformedException; import com.tungsten.fclcore.auth.ServerResponseMalformedException;
import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding;
import com.tungsten.fclcore.util.fakefx.BindingMapping; import com.tungsten.fclcore.util.fakefx.BindingMapping;
import com.tungsten.fclcore.fakefx.ObjectBinding;
import com.tungsten.fclcore.util.gson.UUIDTypeAdapter; import com.tungsten.fclcore.util.gson.UUIDTypeAdapter;
public class YggdrasilAccount extends ClassicAccount { public class YggdrasilAccount extends ClassicAccount {

View File

@ -1,11 +0,0 @@
package com.tungsten.fclcore.fakefx;
public interface Binding<T> extends ObservableValue<T> {
boolean isValid();
void invalidate();
void dispose();
}

View File

@ -1,32 +0,0 @@
package com.tungsten.fclcore.fakefx;
import java.util.concurrent.Callable;
public final class Bindings {
private Bindings() {
}
public static <T> ObjectBinding<T> createObjectBinding(final Callable<T> func, final Observable... dependencies) {
return new ObjectBinding<T>() {
{
bind(dependencies);
}
@Override
protected T computeValue() {
try {
return func.call();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
public void dispose() {
super.unbind(dependencies);
}
};
}
}

View File

@ -1,58 +0,0 @@
package com.tungsten.fclcore.fakefx;
public abstract class BooleanExpression implements ObservableBooleanValue {
public BooleanExpression() {
}
@Override
public Boolean getValue() {
return get();
}
public static BooleanExpression booleanExpression(
final ObservableBooleanValue value) {
if (value == null) {
throw new NullPointerException("Value must be specified.");
}
return (value instanceof BooleanExpression) ? (BooleanExpression) value
: new BooleanBinding() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected boolean computeValue() {
return value.get();
}
};
}
public static BooleanExpression booleanExpression(final ObservableValue<Boolean> value) {
if (value == null) {
throw new NullPointerException("Value must be specified.");
}
return (value instanceof BooleanExpression) ? (BooleanExpression) value
: new BooleanBinding() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected boolean computeValue() {
final Boolean val = value.getValue();
return val == null ? false : val;
}
};
}
}

View File

@ -1,43 +0,0 @@
package com.tungsten.fclcore.fakefx;
public abstract class BooleanProperty extends ReadOnlyBooleanProperty implements
Property<Boolean>, WritableBooleanValue {
public BooleanProperty() {
}
@Override
public void setValue(Boolean v) {
if (v == null) {
set(false);
} else {
set(v.booleanValue());
}
}
@Override
public void bindBidirectional(Property<Boolean> other) {
}
@Override
public void unbindBidirectional(Property<Boolean> other) {
}
@Override
public String toString() {
final Object bean = getBean();
final String name = getName();
final StringBuilder result = new StringBuilder(
"BooleanProperty [");
if (bean != null) {
result.append("bean: ").append(bean).append(", ");
}
if ((name != null) && (!name.equals(""))) {
result.append("name: ").append(name).append(", ");
}
result.append("value: ").append(get()).append("]");
return result.toString();
}
}

View File

@ -1,6 +0,0 @@
package com.tungsten.fclcore.fakefx;
public interface ChangeListener<T> {
void changed(ObservableValue<? extends T> observable, T oldValue, T newValue);
}

View File

@ -0,0 +1,38 @@
package com.tungsten.fclcore.fakefx;
import com.tungsten.fclcore.fakefx.util.FXPermission;
/**
* Constants used for permission checks.
*/
public final class FXPermissions {
// Prevent instantiation
private FXPermissions() {
}
public static final FXPermission ACCESS_CLIPBOARD_PERMISSION =
new FXPermission("accessClipboard");
public static final FXPermission ACCESS_WINDOW_LIST_PERMISSION =
new FXPermission("accessWindowList");
public static final FXPermission CREATE_ROBOT_PERMISSION =
new FXPermission("createRobot");
public static final FXPermission CREATE_TRANSPARENT_WINDOW_PERMISSION =
new FXPermission("createTransparentWindow");
public static final FXPermission UNRESTRICTED_FULL_SCREEN_PERMISSION =
new FXPermission("unrestrictedFullScreen");
public static final FXPermission LOAD_FONT_PERMISSION =
new FXPermission("loadFont");
public static final FXPermission MODIFY_FXML_CLASS_LOADER_PERMISSION =
new FXPermission("modifyFXMLClassLoader");
public static final FXPermission SET_WINDOW_ALWAYS_ON_TOP_PERMISSION =
new FXPermission("setWindowAlwaysOnTop");
}

View File

@ -1,6 +0,0 @@
package com.tungsten.fclcore.fakefx;
public interface InvalidationListener {
public void invalidated(Observable observable);
}

View File

@ -1,90 +0,0 @@
package com.tungsten.fclcore.fakefx;
public abstract class ObjectBinding<T> extends ObjectExpression<T> implements Binding<T> {
private T value;
private boolean valid = false;
private BindingHelperObserver observer;
private ExpressionHelper<T> helper = null;
public ObjectBinding() {
}
@Override
public void addListener(InvalidationListener listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
@Override
public void addListener(ChangeListener<? super T> listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(ChangeListener<? super T> listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
protected final void bind(Observable... dependencies) {
if ((dependencies != null) && (dependencies.length > 0)) {
if (observer == null) {
observer = new BindingHelperObserver(this);
}
for (final Observable dep : dependencies) {
dep.addListener(observer);
}
}
}
protected final void unbind(Observable... dependencies) {
if (observer != null) {
for (final Observable dep : dependencies) {
dep.removeListener(observer);
}
observer = null;
}
}
@Override
public void dispose() {
}
@Override
public final T get() {
if (!valid) {
value = computeValue();
valid = true;
}
return value;
}
protected void onInvalidating() {
}
@Override
public final void invalidate() {
if (valid) {
valid = false;
onInvalidating();
ExpressionHelper.fireValueChangedEvent(helper);
}
}
@Override
public final boolean isValid() {
return valid;
}
protected abstract T computeValue();
@Override
public String toString() {
return valid ? "ObjectBinding [value: " + get() + "]"
: "ObjectBinding [invalid]";
}
}

View File

@ -1,12 +0,0 @@
package com.tungsten.fclcore.fakefx;
public abstract class ObjectExpression<T> implements ObservableObjectValue<T> {
@Override
public T getValue() {
return get();
}
public ObjectExpression() {
}
}

View File

@ -1,8 +0,0 @@
package com.tungsten.fclcore.fakefx;
public interface Observable {
void addListener(InvalidationListener listener);
void removeListener(InvalidationListener listener);
}

View File

@ -1,6 +0,0 @@
package com.tungsten.fclcore.fakefx;
public interface ObservableBooleanValue extends ObservableValue<Boolean> {
boolean get();
}

View File

@ -1,19 +0,0 @@
package com.tungsten.fclcore.fakefx;
import java.util.Collection;
import java.util.List;
public interface ObservableList<E> extends List<E>, Observable {
public boolean addAll(E... elements);
public boolean setAll(E... elements);
public boolean setAll(Collection<? extends E> col);
public boolean removeAll(E... elements);
public boolean retainAll(E... elements);
public void remove(int from, int to);
}

View File

@ -1,6 +0,0 @@
package com.tungsten.fclcore.fakefx;
public interface ObservableObjectValue<T> extends ObservableValue<T> {
T get();
}

View File

@ -1,10 +0,0 @@
package com.tungsten.fclcore.fakefx;
public interface ObservableValue<T> extends Observable {
void addListener(ChangeListener<? super T> listener);
void removeListener(ChangeListener<? super T> listener);
T getValue();
}

View File

@ -0,0 +1,281 @@
package com.tungsten.fclcore.fakefx;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Locale;
import java.util.Properties;
public class PlatformUtil {
// NOTE: since this class can be initialized by application code in some
// cases, we must encapsulate all calls to System.getProperty("...") in
// a doPrivileged block except for standard JVM properties such as
// os.name, os.version, os.arch, java.vm.name, etc.
private static final String os = System.getProperty("os.name");
private static final String version = System.getProperty("os.version");
private static final boolean embedded;
private static final String embeddedType;
private static final boolean useEGL;
private static final boolean doEGLCompositing;
// a property used to denote a non-default impl for this host
private static String javafxPlatform;
static {
@SuppressWarnings("removal")
String str1 = AccessController.doPrivileged((PrivilegedAction<String>) () -> System.getProperty("javafx.platform"));
javafxPlatform = str1;
loadProperties();
@SuppressWarnings("removal")
boolean bool1 = AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("com.sun.javafx.isEmbedded"));
embedded = bool1;
@SuppressWarnings("removal")
String str2 = AccessController.doPrivileged((PrivilegedAction<String>) () -> System.getProperty("glass.platform", "").toLowerCase(Locale.ROOT));
embeddedType = str2;
@SuppressWarnings("removal")
boolean bool2 = AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("use.egl"));
useEGL = bool2;
if (useEGL) {
@SuppressWarnings("removal")
boolean bool3 = AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("doNativeComposite"));
doEGLCompositing = bool3;
} else
doEGLCompositing = false;
}
private static final boolean ANDROID = "android".equals(javafxPlatform) || "Dalvik".equals(System.getProperty("java.vm.name"));
private static final boolean WINDOWS = os.startsWith("Windows");
private static final boolean WINDOWS_VISTA_OR_LATER = WINDOWS && versionNumberGreaterThanOrEqualTo(6.0f);
private static final boolean WINDOWS_7_OR_LATER = WINDOWS && versionNumberGreaterThanOrEqualTo(6.1f);
private static final boolean MAC = os.startsWith("Mac");
private static final boolean LINUX = os.startsWith("Linux") && !ANDROID;
private static final boolean SOLARIS = os.startsWith("SunOS");
private static final boolean IOS = os.startsWith("iOS");
private static final boolean STATIC_BUILD = "Substrate VM".equals(System.getProperty("java.vm.name"));
/**
* Utility method used to determine whether the version number as
* reported by system properties is greater than or equal to a given
* value.
*
* @param value The value to test against.
* @return false if the version number cannot be parsed as a float,
* otherwise the comparison against value.
*/
private static boolean versionNumberGreaterThanOrEqualTo(float value) {
try {
return Float.parseFloat(version) >= value;
} catch (Exception e) {
return false;
}
}
/**
* Returns true if the operating system is a form of Windows.
*/
public static boolean isWindows(){
return WINDOWS;
}
/**
* Returns true if the operating system is at least Windows Vista(v6.0).
*/
public static boolean isWinVistaOrLater(){
return WINDOWS_VISTA_OR_LATER;
}
/**
* Returns true if the operating system is at least Windows 7(v6.1).
*/
public static boolean isWin7OrLater(){
return WINDOWS_7_OR_LATER;
}
/**
* Returns true if the operating system is a form of Mac OS.
*/
public static boolean isMac(){
return MAC;
}
/**
* Returns true if the operating system is a form of Linux.
*/
public static boolean isLinux(){
return LINUX;
}
public static boolean useEGL() {
return useEGL;
}
public static boolean useEGLWindowComposition() {
return doEGLCompositing;
}
public static boolean useGLES2() {
@SuppressWarnings("removal")
String useGles2 =
AccessController.doPrivileged((PrivilegedAction<String>) () -> System.getProperty("use.gles2"));
if ("true".equals(useGles2))
return true;
else
return false;
}
/**
* Returns true if the operating system is a form of Unix, including Linux.
*/
public static boolean isSolaris(){
return SOLARIS;
}
/**
* Returns true if the operating system is a form of Linux or Solaris
*/
public static boolean isUnix(){
return LINUX || SOLARIS;
}
/**
* Returns true if the platform is embedded.
*/
public static boolean isEmbedded() {
return embedded;
}
/**
* Returns a string with the embedded type - ie eglx11, eglfb, dfb or null.
*/
public static String getEmbeddedType() {
return embeddedType;
}
/**
* Returns true if the operating system is iOS
*/
public static boolean isIOS(){
return IOS;
}
/**
* Returns true if the current runtime is a statically linked image
*/
public static boolean isStaticBuild(){
return STATIC_BUILD;
}
private static void loadPropertiesFromFile(final File file) {
Properties p = new Properties();
try {
InputStream in = new FileInputStream(file);
p.load(in);
in.close();
} catch (IOException e) {
e.printStackTrace();
}
if (javafxPlatform == null) {
javafxPlatform = p.getProperty("javafx.platform");
}
String prefix = javafxPlatform + ".";
int prefixLength = prefix.length();
boolean foundPlatform = false;
for (Object o : p.keySet()) {
String key = (String) o;
if (key.startsWith(prefix)) {
foundPlatform = true;
String systemKey = key.substring(prefixLength);
if (System.getProperty(systemKey) == null) {
String value = p.getProperty(key);
System.setProperty(systemKey, value);
}
}
}
if (!foundPlatform) {
System.err.println(
"Warning: No settings found for javafx.platform='"
+ javafxPlatform + "'");
}
}
/** Returns the directory containing the JavaFX runtime, or null
* if the directory cannot be located
*/
private static File getRTDir() {
try {
String theClassFile = "PlatformUtil.class";
Class theClass = PlatformUtil.class;
URL url = theClass.getResource(theClassFile);
if (url == null) return null;
String classUrlString = url.toString();
if (!classUrlString.startsWith("jar:file:")
|| classUrlString.indexOf('!') == -1) {
return null;
}
// Strip out the "jar:" and everything after and including the "!"
String s = classUrlString.substring(4,
classUrlString.lastIndexOf('!'));
// Strip everything after the last "/" or "\" to get rid of the jar filename
int lastIndexOfSlash = Math.max(
s.lastIndexOf('/'), s.lastIndexOf('\\'));
return new File(new URL(s.substring(0, lastIndexOfSlash + 1)).getPath());
} catch (MalformedURLException e) {
return null;
}
}
@SuppressWarnings("removal")
private static void loadProperties() {
final String vmname = System.getProperty("java.vm.name");
final String arch = System.getProperty("os.arch");
if (! (javafxPlatform != null ||
(arch != null && arch.equals("arm")) ||
(vmname != null && vmname.indexOf("Embedded") > 0))) {
return;
}
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
final File rtDir = getRTDir();
final String propertyFilename = "javafx.platform.properties";
File rtProperties = new File(rtDir, propertyFilename);
// First look for javafx.platform.properties in the JavaFX runtime
// Then in the installation directory of the JRE
if (rtProperties.exists()) {
loadPropertiesFromFile(rtProperties);
return null;
}
String javaHome = System.getProperty("java.home");
File javaHomeProperties = new File(javaHome,
"lib" + File.separator
+ propertyFilename);
if (javaHomeProperties.exists()) {
loadPropertiesFromFile(javaHomeProperties);
return null;
}
String javafxRuntimePath = System.getProperty("javafx.runtime.path");
File javafxRuntimePathProperties = new File(javafxRuntimePath,
File.separator + propertyFilename);
if (javafxRuntimePathProperties.exists()) {
loadPropertiesFromFile(javafxRuntimePathProperties);
return null;
}
return null;
});
}
public static boolean isAndroid() {
return ANDROID;
}
}

View File

@ -1,15 +0,0 @@
package com.tungsten.fclcore.fakefx;
public interface Property<T> extends ReadOnlyProperty<T>, WritableValue<T> {
void bind(ObservableValue<? extends T> observable);
void unbind();
boolean isBound();
void bindBidirectional(Property<T> other);
void unbindBidirectional(Property<T> other);
}

View File

@ -1,25 +0,0 @@
package com.tungsten.fclcore.fakefx;
public abstract class ReadOnlyBooleanProperty extends BooleanExpression
implements ReadOnlyProperty<Boolean> {
public ReadOnlyBooleanProperty() {
}
@Override
public String toString() {
final Object bean = getBean();
final String name = getName();
final StringBuilder result = new StringBuilder(
"ReadOnlyBooleanProperty [");
if (bean != null) {
result.append("bean: ").append(bean).append(", ");
}
if ((name != null) && !name.equals("")) {
result.append("name: ").append(name).append(", ");
}
result.append("value: ").append(get()).append("]");
return result.toString();
}
}

View File

@ -1,9 +0,0 @@
package com.tungsten.fclcore.fakefx;
public interface ReadOnlyProperty<T> extends ObservableValue<T> {
Object getBean();
String getName();
}

View File

@ -1,39 +0,0 @@
package com.tungsten.fclcore.fakefx;
public class SimpleBooleanProperty extends BooleanPropertyBase {
private static final Object DEFAULT_BEAN = null;
private static final String DEFAULT_NAME = "";
private final Object bean;
private final String name;
@Override
public Object getBean() {
return bean;
}
@Override
public String getName() {
return name;
}
public SimpleBooleanProperty() {
this(DEFAULT_BEAN, DEFAULT_NAME);
}
public SimpleBooleanProperty(boolean initialValue) {
this(DEFAULT_BEAN, DEFAULT_NAME, initialValue);
}
public SimpleBooleanProperty(Object bean, String name) {
this.bean = bean;
this.name = (name == null) ? DEFAULT_NAME : name;
}
public SimpleBooleanProperty(Object bean, String name, boolean initialValue) {
super(initialValue);
this.bean = bean;
this.name = (name == null) ? DEFAULT_NAME : name;
}
}

View File

@ -0,0 +1,38 @@
package com.tungsten.fclcore.fakefx;
import java.util.AbstractList;
import java.util.RandomAccess;
/**
* An unmodifiable array-based List implementation. This is essentially like the
* package private UnmodifiableRandomAccessList of the JDK, and helps us to
* avoid having to do a lot of conversion work when we want to pass an array
* into an unmodifiable list implementation (otherwise we would have to create
* a temporary list that is then passed to Collections.unmodifiableList).
*/
public class UnmodifiableArrayList<T> extends AbstractList<T> implements RandomAccess {
private T[] elements;
private final int size;
/**
* The given elements are used directly (a defensive copy is not made),
* and the given size is used as the size of this list. It is the callers
* responsibility to make sure the size is accurate.
*
* @param elements The elements to use.
* @param size The size must be <= the length of the elements array
*/
public UnmodifiableArrayList(T[] elements, int size) {
assert elements == null ? size == 0 : size <= elements.length;
this.size = size;
this.elements = elements;
}
@Override public T get(int index) {
return elements[index];
}
@Override public int size() {
return size;
}
}

View File

@ -1,12 +0,0 @@
package com.tungsten.fclcore.fakefx;
public interface WritableBooleanValue extends WritableValue<Boolean> {
boolean get();
void set(boolean value);
@Override
void setValue(Boolean value);
}

View File

@ -1,9 +0,0 @@
package com.tungsten.fclcore.fakefx;
public interface WritableValue<T> {
T getValue();
void setValue(T value);
}

View File

@ -0,0 +1,26 @@
package com.tungsten.fclcore.fakefx.beans;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Specifies a property to which child elements will be added or set when an
* explicit property is not given.
*
* @since JavaFX 2.0
*/
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DefaultProperty {
/**
* The name of the default property.
* @return the name of the property
*/
public String value();
}

View File

@ -0,0 +1,23 @@
package com.tungsten.fclcore.fakefx.beans;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Specifies a property to which FXML ID values will be applied.
*
*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface IDProperty {
/**
* The name of the ID property.
*/
public String value();
}

View File

@ -0,0 +1,18 @@
package com.tungsten.fclcore.fakefx.beans;
@FunctionalInterface
public interface InvalidationListener {
/**
* This method needs to be provided by an implementation of
* {@code InvalidationListener}. It is called if an {@link Observable}
* becomes invalid.
* <p>
* In general, it is considered bad practice to modify the observed value in
* this method.
*
* @param observable
* The {@code Observable} that became invalid
*/
public void invalidated(Observable observable);
}

View File

@ -0,0 +1,27 @@
package com.tungsten.fclcore.fakefx.beans;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
/**
* Annotation that provides information about argument's name.
*
* @since JavaFX 8.0
*/
@Retention(RUNTIME)
@Target(PARAMETER)
public @interface NamedArg {
/**
* The name of the annotated argument.
* @return the name of the annotated argument
*/
public String value();
/**
* The default value of the annotated argument.
* @return the default value of the annotated argument
*/
public String defaultValue() default "";
}

View File

@ -0,0 +1,48 @@
package com.tungsten.fclcore.fakefx.beans;
public interface Observable {
/**
* Adds an {@link InvalidationListener} which will be notified whenever the
* {@code Observable} becomes invalid. If the same
* listener is added more than once, then it will be notified more than
* once. That is, no check is made to ensure uniqueness.
* <p>
* Note that the same actual {@code InvalidationListener} instance may be
* safely registered for different {@code Observables}.
* <p>
* The {@code Observable} stores a strong reference to the listener
* which will prevent the listener from being garbage collected and may
* result in a memory leak. It is recommended to either unregister a
* listener by calling {@link #removeListener(InvalidationListener)
* removeListener} after use or to use an instance of
* {@link WeakInvalidationListener} avoid this situation.
*
* @see #removeListener(InvalidationListener)
*
* @param listener
* The listener to register
* @throws NullPointerException
* if the listener is null
*/
void addListener(InvalidationListener listener);
/**
* Removes the given listener from the list of listeners, that are notified
* whenever the value of the {@code Observable} becomes invalid.
* <p>
* If the given listener has not been previously registered (i.e. it was
* never added) then this method call is a no-op. If it had been previously
* added then it will be removed. If it had been added more than once, then
* only the first occurrence will be removed.
*
* @see #addListener(InvalidationListener)
*
* @param listener
* The listener to remove
* @throws NullPointerException
* if the listener is null
*/
void removeListener(InvalidationListener listener);
}

View File

@ -0,0 +1,65 @@
package com.tungsten.fclcore.fakefx.beans;
import java.lang.ref.WeakReference;
/**
* A {@code WeakInvalidationListener} can be used if an {@link Observable}
* should only maintain a weak reference to the listener. This helps to avoid
* memory leaks that can occur if observers are not unregistered from observed
* objects after use.
* <p>
* A {@code WeakInvalidationListener} is created by passing in the original
* {@link InvalidationListener}. The {@code WeakInvalidationListener} should
* then be registered to listen for changes of the observed object.
* <p>
* Note: You have to keep a reference to the {@code InvalidationListener} that
* was passed in as long as it is in use, otherwise it can be garbage collected
* too soon.
*
* @see InvalidationListener
* @see Observable
*
*
* @since JavaFX 2.0
*/
public final class WeakInvalidationListener implements InvalidationListener, WeakListener {
private final WeakReference<InvalidationListener> ref;
/**
* The constructor of {@code WeakInvalidationListener}.
*
* @param listener
* The original listener that should be notified
*/
public WeakInvalidationListener(@NamedArg("listener") InvalidationListener listener) {
if (listener == null) {
throw new NullPointerException("Listener must be specified.");
}
this.ref = new WeakReference<InvalidationListener>(listener);
}
/**
* {@inheritDoc}
*/
@Override
public boolean wasGarbageCollected() {
return (ref.get() == null);
}
/**
* {@inheritDoc}
*/
@Override
public void invalidated(Observable observable) {
InvalidationListener listener = ref.get();
if (listener != null) {
listener.invalidated(observable);
} else {
// The weakly reference listener has been garbage collected,
// so this WeakListener will now unhook itself from the
// source bean
observable.removeListener(this);
}
}
}

View File

@ -1,4 +1,4 @@
package com.tungsten.fclcore.fakefx; package com.tungsten.fclcore.fakefx.beans;
public interface WeakListener { public interface WeakListener {
/** /**

View File

@ -0,0 +1,16 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.value.ObservableValue;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
public interface Binding<T> extends ObservableValue<T> {
boolean isValid();
void invalidate();
ObservableList<?> getDependencies();
void dispose();
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,12 @@
package com.tungsten.fclcore.fakefx; package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.value.ChangeListener;
import com.tungsten.fclcore.fakefx.binding.BindingHelperObserver;
import com.tungsten.fclcore.fakefx.binding.ExpressionHelper;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
public abstract class BooleanBinding extends BooleanExpression implements public abstract class BooleanBinding extends BooleanExpression implements
Binding<Boolean> { Binding<Boolean> {
@ -31,6 +39,13 @@ public abstract class BooleanBinding extends BooleanExpression implements
helper = ExpressionHelper.removeListener(helper, listener); helper = ExpressionHelper.removeListener(helper, listener);
} }
/**
* Start observing the dependencies for changes. If the value of one of the
* dependencies changes, the binding is marked as invalid.
*
* @param dependencies
* the dependencies to observe
*/
protected final void bind(Observable... dependencies) { protected final void bind(Observable... dependencies) {
if ((dependencies != null) && (dependencies.length > 0)) { if ((dependencies != null) && (dependencies.length > 0)) {
if (observer == null) { if (observer == null) {
@ -42,6 +57,12 @@ public abstract class BooleanBinding extends BooleanExpression implements
} }
} }
/**
* Stop observing the dependencies for changes.
*
* @param dependencies
* the dependencies to stop observing
*/
protected final void unbind(Observable... dependencies) { protected final void unbind(Observable... dependencies) {
if (observer != null) { if (observer != null) {
for (final Observable dep : dependencies) { for (final Observable dep : dependencies) {
@ -51,10 +72,32 @@ public abstract class BooleanBinding extends BooleanExpression implements
} }
} }
/**
* A default implementation of {@code dispose()} that is empty.
*/
@Override @Override
public void dispose() { public void dispose() {
} }
/**
* A default implementation of {@code getDependencies()} that returns an
* empty {@link ObservableList}.
*
* @return an empty {@code ObservableList}
*/
@Override
public ObservableList<?> getDependencies() {
return FXCollections.emptyObservableList();
}
/**
* Returns the result of {@link #computeValue()}. The method
* {@code computeValue()} is only called if the binding is invalid. The
* result is cached and returned if the binding did not become invalid since
* the last call of {@code get()}.
*
* @return the current value
*/
@Override @Override
public final boolean get() { public final boolean get() {
if (!valid) { if (!valid) {
@ -64,6 +107,11 @@ public abstract class BooleanBinding extends BooleanExpression implements
return value; return value;
} }
/**
* The method onInvalidating() can be overridden by extending classes to
* react, if this binding becomes invalid. The default implementation is
* empty.
*/
protected void onInvalidating() { protected void onInvalidating() {
} }
@ -81,8 +129,20 @@ public abstract class BooleanBinding extends BooleanExpression implements
return valid; return valid;
} }
/**
* Calculates the current value of this binding.
* <p>
* Classes extending {@code BooleanBinding} have to provide an
* implementation of {@code computeValue}.
*
* @return the current value
*/
protected abstract boolean computeValue(); protected abstract boolean computeValue();
/**
* Returns a string representation of this {@code BooleanBinding} object.
* @return a string representation of this {@code BooleanBinding} object.
*/
@Override @Override
public String toString() { public String toString() {
return valid ? "BooleanBinding [value: " + get() + "]" return valid ? "BooleanBinding [value: " + get() + "]"

View File

@ -0,0 +1,116 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.value.ObservableBooleanValue;
import com.tungsten.fclcore.fakefx.beans.value.ObservableValue;
import com.tungsten.fclcore.fakefx.binding.StringFormatter;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
public abstract class BooleanExpression implements ObservableBooleanValue {
public BooleanExpression() {
}
@Override
public Boolean getValue() {
return get();
}
public static BooleanExpression booleanExpression(
final ObservableBooleanValue value) {
if (value == null) {
throw new NullPointerException("Value must be specified.");
}
return (value instanceof BooleanExpression) ? (BooleanExpression) value
: new BooleanBinding() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected boolean computeValue() {
return value.get();
}
@Override
public ObservableList<ObservableBooleanValue> getDependencies() {
return FXCollections.singletonObservableList(value);
}
};
}
public static BooleanExpression booleanExpression(final ObservableValue<Boolean> value) {
if (value == null) {
throw new NullPointerException("Value must be specified.");
}
return (value instanceof BooleanExpression) ? (BooleanExpression) value
: new BooleanBinding() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected boolean computeValue() {
final Boolean val = value.getValue();
return val == null ? false : val;
}
@Override
public ObservableList<ObservableValue<Boolean>> getDependencies() {
return FXCollections.singletonObservableList(value);
}
};
}
public BooleanBinding and(final ObservableBooleanValue other) {
return Bindings.and(this, other);
}
public BooleanBinding or(final ObservableBooleanValue other) {
return Bindings.or(this, other);
}
public BooleanBinding not() {
return Bindings.not(this);
}
public BooleanBinding isEqualTo(final ObservableBooleanValue other) {
return Bindings.equal(this, other);
}
public BooleanBinding isNotEqualTo(final ObservableBooleanValue other) {
return Bindings.notEqual(this, other);
}
public StringBinding asString() {
return (StringBinding) StringFormatter.convert(this);
}
public ObjectExpression<Boolean> asObject() {
return new ObjectBinding<Boolean>() {
{
bind(BooleanExpression.this);
}
@Override
public void dispose() {
unbind(BooleanExpression.this);
}
@Override
protected Boolean computeValue() {
return BooleanExpression.this.getValue();
}
};
}
}

View File

@ -0,0 +1,154 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.value.ChangeListener;
import com.tungsten.fclcore.fakefx.binding.BindingHelperObserver;
import com.tungsten.fclcore.fakefx.binding.ExpressionHelper;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
public abstract class DoubleBinding extends DoubleExpression implements
NumberBinding {
private double value;
private boolean valid;
private BindingHelperObserver observer;
private ExpressionHelper<Number> helper = null;
/**
* Creates a default {@code DoubleBinding}.
*/
public DoubleBinding() {
}
@Override
public void addListener(InvalidationListener listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
@Override
public void addListener(ChangeListener<? super Number> listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(ChangeListener<? super Number> listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
/**
* Start observing the dependencies for changes. If the value of one of the
* dependencies changes, the binding is marked as invalid.
*
* @param dependencies
* the dependencies to observe
*/
protected final void bind(Observable... dependencies) {
if ((dependencies != null) && (dependencies.length > 0)) {
if (observer == null) {
observer = new BindingHelperObserver(this);
}
for (final Observable dep : dependencies) {
dep.addListener(observer);
}
}
}
/**
* Stop observing the dependencies for changes.
*
* @param dependencies
* the dependencies to stop observing
*/
protected final void unbind(Observable... dependencies) {
if (observer != null) {
for (final Observable dep : dependencies) {
dep.removeListener(observer);
}
observer = null;
}
}
/**
* A default implementation of {@code dispose()} that is empty.
*/
@Override
public void dispose() {
}
/**
* A default implementation of {@code getDependencies()} that returns an
* empty {@link ObservableList}.
*
* @return an empty {@code ObservableList}
*/
@Override
public ObservableList<?> getDependencies() {
return FXCollections.emptyObservableList();
}
/**
* Returns the result of {@link #computeValue()}. The method
* {@code computeValue()} is only called if the binding is invalid. The
* result is cached and returned if the binding did not become invalid since
* the last call of {@code get()}.
*
* @return the current value
*/
@Override
public final double get() {
if (!valid) {
value = computeValue();
valid = true;
}
return value;
}
/**
* The method onInvalidating() can be overridden by extending classes to
* react, if this binding becomes invalid. The default implementation is
* empty.
*/
protected void onInvalidating() {
}
@Override
public final void invalidate() {
if (valid) {
valid = false;
onInvalidating();
ExpressionHelper.fireValueChangedEvent(helper);
}
}
@Override
public final boolean isValid() {
return valid;
}
/**
* Calculates the current value of this binding.
* <p>
* Classes extending {@code DoubleBinding} have to provide an implementation
* of {@code computeValue}.
*
* @return the current value
*/
protected abstract double computeValue();
/**
* Returns a string representation of this {@code DoubleBinding} object.
* @return a string representation of this {@code DoubleBinding} object.
*/
@Override
public String toString() {
return valid ? "DoubleBinding [value: " + get() + "]"
: "DoubleBinding [invalid]";
}
}

View File

@ -0,0 +1,221 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.value.ObservableDoubleValue;
import com.tungsten.fclcore.fakefx.beans.value.ObservableNumberValue;
import com.tungsten.fclcore.fakefx.beans.value.ObservableValue;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
public abstract class DoubleExpression extends NumberExpressionBase implements
ObservableDoubleValue {
/**
* Creates a default {@code DoubleExpression}.
*/
public DoubleExpression() {
}
@Override
public int intValue() {
return (int) get();
}
@Override
public long longValue() {
return (long) get();
}
@Override
public float floatValue() {
return (float) get();
}
@Override
public double doubleValue() {
return get();
}
@Override
public Double getValue() {
return get();
}
public static DoubleExpression doubleExpression(
final ObservableDoubleValue value) {
if (value == null) {
throw new NullPointerException("Value must be specified.");
}
return (value instanceof DoubleExpression) ? (DoubleExpression) value
: new DoubleBinding() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected double computeValue() {
return value.get();
}
@Override
public ObservableList<ObservableDoubleValue> getDependencies() {
return FXCollections.singletonObservableList(value);
}
};
}
public static <T extends Number> DoubleExpression doubleExpression(final ObservableValue<T> value) {
if (value == null) {
throw new NullPointerException("Value must be specified.");
}
return (value instanceof DoubleExpression) ? (DoubleExpression) value
: new DoubleBinding() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected double computeValue() {
final T val = value.getValue();
return val == null ? 0.0 : val.doubleValue();
}
@Override
public ObservableList<ObservableValue<T>> getDependencies() {
return FXCollections.singletonObservableList(value);
}
};
}
@Override
public DoubleBinding negate() {
return (DoubleBinding) Bindings.negate(this);
}
@Override
public DoubleBinding add(final ObservableNumberValue other) {
return (DoubleBinding) Bindings.add(this, other);
}
@Override
public DoubleBinding add(final double other) {
return Bindings.add(this, other);
}
@Override
public DoubleBinding add(final float other) {
return (DoubleBinding) Bindings.add(this, other);
}
@Override
public DoubleBinding add(final long other) {
return (DoubleBinding) Bindings.add(this, other);
}
@Override
public DoubleBinding add(final int other) {
return (DoubleBinding) Bindings.add(this, other);
}
@Override
public DoubleBinding subtract(final ObservableNumberValue other) {
return (DoubleBinding) Bindings.subtract(this, other);
}
@Override
public DoubleBinding subtract(final double other) {
return Bindings.subtract(this, other);
}
@Override
public DoubleBinding subtract(final float other) {
return (DoubleBinding) Bindings.subtract(this, other);
}
@Override
public DoubleBinding subtract(final long other) {
return (DoubleBinding) Bindings.subtract(this, other);
}
@Override
public DoubleBinding subtract(final int other) {
return (DoubleBinding) Bindings.subtract(this, other);
}
@Override
public DoubleBinding multiply(final ObservableNumberValue other) {
return (DoubleBinding) Bindings.multiply(this, other);
}
@Override
public DoubleBinding multiply(final double other) {
return Bindings.multiply(this, other);
}
@Override
public DoubleBinding multiply(final float other) {
return (DoubleBinding) Bindings.multiply(this, other);
}
@Override
public DoubleBinding multiply(final long other) {
return (DoubleBinding) Bindings.multiply(this, other);
}
@Override
public DoubleBinding multiply(final int other) {
return (DoubleBinding) Bindings.multiply(this, other);
}
@Override
public DoubleBinding divide(final ObservableNumberValue other) {
return (DoubleBinding) Bindings.divide(this, other);
}
@Override
public DoubleBinding divide(final double other) {
return Bindings.divide(this, other);
}
@Override
public DoubleBinding divide(final float other) {
return (DoubleBinding) Bindings.divide(this, other);
}
@Override
public DoubleBinding divide(final long other) {
return (DoubleBinding) Bindings.divide(this, other);
}
@Override
public DoubleBinding divide(final int other) {
return (DoubleBinding) Bindings.divide(this, other);
}
public ObjectExpression<Double> asObject() {
return new ObjectBinding<Double>() {
{
bind(DoubleExpression.this);
}
@Override
public void dispose() {
unbind(DoubleExpression.this);
}
@Override
protected Double computeValue() {
return DoubleExpression.this.getValue();
}
};
}
}

View File

@ -0,0 +1,152 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.value.ChangeListener;
import com.tungsten.fclcore.fakefx.binding.BindingHelperObserver;
import com.tungsten.fclcore.fakefx.binding.ExpressionHelper;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
public abstract class FloatBinding extends FloatExpression implements
NumberBinding {
private float value;
private boolean valid;
private BindingHelperObserver observer;
private ExpressionHelper<Number> helper = null;
/**
* Creates a default {@code FloatBinding}.
*/
public FloatBinding() {
}
@Override
public void addListener(InvalidationListener listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
@Override
public void addListener(ChangeListener<? super Number> listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(ChangeListener<? super Number> listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
/**
* Start observing the dependencies for changes. If the value of one of the
* dependencies changes, the binding is marked as invalid.
*
* @param dependencies
* the dependencies to observe
*/
protected final void bind(Observable... dependencies) {
if ((dependencies != null) && (dependencies.length > 0)) {
if (observer == null) {
observer = new BindingHelperObserver(this);
}
for (final Observable dep : dependencies) {
dep.addListener(observer);
}
}
}
/**
* Stop observing the dependencies for changes.
*
* @param dependencies
* the dependencies to stop observing
*/
protected final void unbind(Observable... dependencies) {
if (observer != null) {
for (final Observable dep : dependencies) {
dep.removeListener(observer);
}
observer = null;
}
}
/**
* A default implementation of {@code dispose()} that is empty.
*/
@Override
public void dispose() {
}
/**
* A default implementation of {@code getDependencies()} that returns an
* empty {@link ObservableList}.
*
* @return an empty {@code ObservableList}
*/
@Override
public ObservableList<?> getDependencies() {
return FXCollections.emptyObservableList();
}
/**
* Returns the result of {@link #computeValue()}. The method
* {@code computeValue()} is only called if the binding is invalid. The
* result is cached and returned if the binding did not become invalid since
* the last call of {@code getValue}.
*/
@Override
public final float get() {
if (!valid) {
value = computeValue();
valid = true;
}
return value;
}
/**
* The method onInvalidating() can be overridden by extending classes to
* react, if this binding becomes invalid. The default implementation is
* empty.
*/
protected void onInvalidating() {
}
@Override
public final void invalidate() {
if (valid) {
valid = false;
onInvalidating();
ExpressionHelper.fireValueChangedEvent(helper);
}
}
@Override
public final boolean isValid() {
return valid;
}
/**
* Calculates the current value of this binding.
* <p>
* Classes extending {@code FloatBinding} have to provide an implementation
* of {@code computeValue}.
*
* @return the current value
*/
protected abstract float computeValue();
/**
* Returns a string representation of this {@code FloatBinding} object.
* @return a string representation of this {@code FloatBinding} object.
*/
@Override
public String toString() {
return valid ? "FloatBinding [value: " + get() + "]"
: "FloatBinding [invalid]";
}
}

View File

@ -0,0 +1,210 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.value.ObservableFloatValue;
import com.tungsten.fclcore.fakefx.beans.value.ObservableValue;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
public abstract class FloatExpression extends NumberExpressionBase implements
ObservableFloatValue {
/**
* Creates a default {@code FloatExpression}.
*/
public FloatExpression() {
}
@Override
public int intValue() {
return (int) get();
}
@Override
public long longValue() {
return (long) get();
}
@Override
public float floatValue() {
return get();
}
@Override
public double doubleValue() {
return (double) get();
}
@Override
public Float getValue() {
return get();
}
public static FloatExpression floatExpression(
final ObservableFloatValue value) {
if (value == null) {
throw new NullPointerException("Value must be specified.");
}
return (value instanceof FloatExpression) ? (FloatExpression) value
: new FloatBinding() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected float computeValue() {
return value.get();
}
@Override
public ObservableList<ObservableFloatValue> getDependencies() {
return FXCollections.singletonObservableList(value);
}
};
}
public static <T extends Number> FloatExpression floatExpression(final ObservableValue<T> value) {
if (value == null) {
throw new NullPointerException("Value must be specified.");
}
return (value instanceof FloatExpression) ? (FloatExpression) value
: new FloatBinding() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected float computeValue() {
final T val = value.getValue();
return val == null ? 0f : val.floatValue();
}
@Override
public ObservableList<ObservableValue<T>> getDependencies() {
return FXCollections.singletonObservableList(value);
}
};
}
@Override
public FloatBinding negate() {
return (FloatBinding) Bindings.negate(this);
}
@Override
public DoubleBinding add(final double other) {
return Bindings.add(this, other);
}
@Override
public FloatBinding add(final float other) {
return (FloatBinding) Bindings.add(this, other);
}
@Override
public FloatBinding add(final long other) {
return (FloatBinding) Bindings.add(this, other);
}
@Override
public FloatBinding add(final int other) {
return (FloatBinding) Bindings.add(this, other);
}
@Override
public DoubleBinding subtract(final double other) {
return Bindings.subtract(this, other);
}
@Override
public FloatBinding subtract(final float other) {
return (FloatBinding) Bindings.subtract(this, other);
}
@Override
public FloatBinding subtract(final long other) {
return (FloatBinding) Bindings.subtract(this, other);
}
@Override
public FloatBinding subtract(final int other) {
return (FloatBinding) Bindings.subtract(this, other);
}
@Override
public DoubleBinding multiply(final double other) {
return Bindings.multiply(this, other);
}
@Override
public FloatBinding multiply(final float other) {
return (FloatBinding) Bindings.multiply(this, other);
}
@Override
public FloatBinding multiply(final long other) {
return (FloatBinding) Bindings.multiply(this, other);
}
@Override
public FloatBinding multiply(final int other) {
return (FloatBinding) Bindings.multiply(this, other);
}
@Override
public DoubleBinding divide(final double other) {
return Bindings.divide(this, other);
}
@Override
public FloatBinding divide(final float other) {
return (FloatBinding) Bindings.divide(this, other);
}
@Override
public FloatBinding divide(final long other) {
return (FloatBinding) Bindings.divide(this, other);
}
@Override
public FloatBinding divide(final int other) {
return (FloatBinding) Bindings.divide(this, other);
}
/**
* Creates an {@link javafx.beans.binding.ObjectExpression} that holds the value
* of this {@code FloatExpression}. If the
* value of this {@code FloatExpression} changes, the value of the
* {@code ObjectExpression} will be updated automatically.
*
* @return the new {@code ObjectExpression}
* @since JavaFX 8.0
*/
public ObjectExpression<Float> asObject() {
return new ObjectBinding<Float>() {
{
bind(FloatExpression.this);
}
@Override
public void dispose() {
unbind(FloatExpression.this);
}
@Override
protected Float computeValue() {
return FloatExpression.this.getValue();
}
};
}
}

View File

@ -0,0 +1,154 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.value.ChangeListener;
import com.tungsten.fclcore.fakefx.binding.BindingHelperObserver;
import com.tungsten.fclcore.fakefx.binding.ExpressionHelper;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
public abstract class IntegerBinding extends IntegerExpression implements
NumberBinding {
private int value;
private boolean valid = false;
private BindingHelperObserver observer;
private ExpressionHelper<Number> helper = null;
/**
* Creates a default {@code IntegerBinding}.
*/
public IntegerBinding() {
}
@Override
public void addListener(InvalidationListener listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
@Override
public void addListener(ChangeListener<? super Number> listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(ChangeListener<? super Number> listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
/**
* Start observing the dependencies for changes. If the value of one of the
* dependencies changes, the binding is marked as invalid.
*
* @param dependencies
* the dependencies to observe
*/
protected final void bind(Observable... dependencies) {
if ((dependencies != null) && (dependencies.length > 0)) {
if (observer == null) {
observer = new BindingHelperObserver(this);
}
for (final Observable dep : dependencies) {
dep.addListener(observer);
}
}
}
/**
* Stop observing the dependencies for changes.
*
* @param dependencies
* the dependencies to stop observing
*/
protected final void unbind(Observable... dependencies) {
if (observer != null) {
for (final Observable dep : dependencies) {
dep.removeListener(observer);
}
observer = null;
}
}
/**
* A default implementation of {@code dispose()} that is empty.
*/
@Override
public void dispose() {
}
/**
* A default implementation of {@code getDependencies()} that returns an
* empty {@link ObservableList}.
*
* @return an empty {@code ObservableList}
*/
@Override
public ObservableList<?> getDependencies() {
return FXCollections.emptyObservableList();
}
/**
* Returns the result of {@link #computeValue()}. The method
* {@code computeValue()} is only called if the binding is invalid. The
* result is cached and returned if the binding did not become invalid since
* the last call of {@code get()}.
*
* @return the current value
*/
@Override
public final int get() {
if (!valid) {
value = computeValue();
valid = true;
}
return value;
}
/**
* The method onInvalidating() can be overridden by extending classes to
* react, if this binding becomes invalid. The default implementation is
* empty.
*/
protected void onInvalidating() {
}
@Override
public final void invalidate() {
if (valid) {
valid = false;
onInvalidating();
ExpressionHelper.fireValueChangedEvent(helper);
}
}
@Override
public final boolean isValid() {
return valid;
}
/**
* Calculates the current value of this binding.
* <p>
* Classes extending {@code IntegerBinding} have to provide an
* implementation of {@code computeValue}.
*
* @return the current value
*/
protected abstract int computeValue();
/**
* Returns a string representation of this {@code IntegerBinding} object.
* @return a string representation of this {@code IntegerBinding} object.
*/
@Override
public String toString() {
return valid ? "IntegerBinding [value: " + get() + "]"
: "IntegerBinding [invalid]";
}
}

View File

@ -0,0 +1,201 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.value.ObservableIntegerValue;
import com.tungsten.fclcore.fakefx.beans.value.ObservableValue;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
public abstract class IntegerExpression extends NumberExpressionBase implements
ObservableIntegerValue {
/**
* Creates a default {@code IntegerExpression}.
*/
public IntegerExpression() {
}
@Override
public int intValue() {
return get();
}
@Override
public long longValue() {
return (long) get();
}
@Override
public float floatValue() {
return (float) get();
}
@Override
public double doubleValue() {
return (double) get();
}
@Override
public Integer getValue() {
return get();
}
public static IntegerExpression integerExpression(
final ObservableIntegerValue value) {
if (value == null) {
throw new NullPointerException("Value must be specified.");
}
return (value instanceof IntegerExpression) ? (IntegerExpression) value
: new IntegerBinding() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected int computeValue() {
return value.get();
}
@Override
public ObservableList<ObservableIntegerValue> getDependencies() {
return FXCollections.singletonObservableList(value);
}
};
}
public static <T extends Number> IntegerExpression integerExpression(final ObservableValue<T> value) {
if (value == null) {
throw new NullPointerException("Value must be specified.");
}
return (value instanceof IntegerExpression) ? (IntegerExpression) value
: new IntegerBinding() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected int computeValue() {
final T val = value.getValue();
return val == null ? 0 : val.intValue();
}
@Override
public ObservableList<ObservableValue<T>> getDependencies() {
return FXCollections.singletonObservableList(value);
}
};
}
@Override
public IntegerBinding negate() {
return (IntegerBinding) Bindings.negate(this);
}
@Override
public DoubleBinding add(final double other) {
return Bindings.add(this, other);
}
@Override
public FloatBinding add(final float other) {
return (FloatBinding) Bindings.add(this, other);
}
@Override
public LongBinding add(final long other) {
return (LongBinding) Bindings.add(this, other);
}
@Override
public IntegerBinding add(final int other) {
return (IntegerBinding) Bindings.add(this, other);
}
@Override
public DoubleBinding subtract(final double other) {
return Bindings.subtract(this, other);
}
@Override
public FloatBinding subtract(final float other) {
return (FloatBinding) Bindings.subtract(this, other);
}
@Override
public LongBinding subtract(final long other) {
return (LongBinding) Bindings.subtract(this, other);
}
@Override
public IntegerBinding subtract(final int other) {
return (IntegerBinding) Bindings.subtract(this, other);
}
@Override
public DoubleBinding multiply(final double other) {
return Bindings.multiply(this, other);
}
@Override
public FloatBinding multiply(final float other) {
return (FloatBinding) Bindings.multiply(this, other);
}
@Override
public LongBinding multiply(final long other) {
return (LongBinding) Bindings.multiply(this, other);
}
@Override
public IntegerBinding multiply(final int other) {
return (IntegerBinding) Bindings.multiply(this, other);
}
@Override
public DoubleBinding divide(final double other) {
return Bindings.divide(this, other);
}
@Override
public FloatBinding divide(final float other) {
return (FloatBinding) Bindings.divide(this, other);
}
@Override
public LongBinding divide(final long other) {
return (LongBinding) Bindings.divide(this, other);
}
@Override
public IntegerBinding divide(final int other) {
return (IntegerBinding) Bindings.divide(this, other);
}
public ObjectExpression<Integer> asObject() {
return new ObjectBinding<Integer>() {
{
bind(IntegerExpression.this);
}
@Override
public void dispose() {
unbind(IntegerExpression.this);
}
@Override
protected Integer computeValue() {
return IntegerExpression.this.getValue();
}
};
}
}

View File

@ -0,0 +1,260 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyBooleanProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyBooleanPropertyBase;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyIntegerProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyIntegerPropertyBase;
import com.tungsten.fclcore.fakefx.beans.value.ChangeListener;
import com.tungsten.fclcore.fakefx.binding.BindingHelperObserver;
import com.tungsten.fclcore.fakefx.binding.ListExpressionHelper;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ListChangeListener;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
public abstract class ListBinding<E> extends ListExpression<E> implements Binding<ObservableList<E>> {
/**
* Creates a default {@code ListBinding}.
*/
public ListBinding() {
}
private final ListChangeListener<E> listChangeListener = new ListChangeListener<E>() {
@Override
public void onChanged(Change<? extends E> change) {
invalidateProperties();
onInvalidating();
ListExpressionHelper.fireValueChangedEvent(helper, change);
}
};
private ObservableList<E> value;
private boolean valid = false;
private BindingHelperObserver observer;
private ListExpressionHelper<E> helper = null;
private SizeProperty size0;
private EmptyProperty empty0;
@Override
public ReadOnlyIntegerProperty sizeProperty() {
if (size0 == null) {
size0 = new SizeProperty();
}
return size0;
}
private class SizeProperty extends ReadOnlyIntegerPropertyBase {
@Override
public int get() {
return size();
}
@Override
public Object getBean() {
return ListBinding.this;
}
@Override
public String getName() {
return "size";
}
protected void fireValueChangedEvent() {
super.fireValueChangedEvent();
}
}
@Override
public ReadOnlyBooleanProperty emptyProperty() {
if (empty0 == null) {
empty0 = new EmptyProperty();
}
return empty0;
}
private class EmptyProperty extends ReadOnlyBooleanPropertyBase {
@Override
public boolean get() {
return isEmpty();
}
@Override
public Object getBean() {
return ListBinding.this;
}
@Override
public String getName() {
return "empty";
}
protected void fireValueChangedEvent() {
super.fireValueChangedEvent();
}
}
@Override
public void addListener(InvalidationListener listener) {
helper = ListExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper = ListExpressionHelper.removeListener(helper, listener);
}
@Override
public void addListener(ChangeListener<? super ObservableList<E>> listener) {
helper = ListExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(ChangeListener<? super ObservableList<E>> listener) {
helper = ListExpressionHelper.removeListener(helper, listener);
}
@Override
public void addListener(ListChangeListener<? super E> listener) {
helper = ListExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(ListChangeListener<? super E> listener) {
helper = ListExpressionHelper.removeListener(helper, listener);
}
/**
* Start observing the dependencies for changes. If the value of one of the
* dependencies changes, the binding is marked as invalid.
*
* @param dependencies
* the dependencies to observe
*/
protected final void bind(Observable... dependencies) {
if ((dependencies != null) && (dependencies.length > 0)) {
if (observer == null) {
observer = new BindingHelperObserver(this);
}
for (final Observable dep : dependencies) {
if (dep != null) {
dep.addListener(observer);
}
}
}
}
/**
* Stop observing the dependencies for changes.
*
* @param dependencies
* the dependencies to stop observing
*/
protected final void unbind(Observable... dependencies) {
if (observer != null) {
for (final Observable dep : dependencies) {
if (dep != null) {
dep.removeListener(observer);
}
}
observer = null;
}
}
/**
* A default implementation of {@code dispose()} that is empty.
*/
@Override
public void dispose() {
}
/**
* A default implementation of {@code getDependencies()} that returns an
* empty {@link ObservableList}.
*
* @return an empty {@code ObservableList}
*/
@Override
public ObservableList<?> getDependencies() {
return FXCollections.emptyObservableList();
}
/**
* Returns the result of {@link #computeValue()}. The method
* {@code computeValue()} is only called if the binding is invalid. The
* result is cached and returned if the binding did not become invalid since
* the last call of {@code get()}.
*
* @return the current value
*/
@Override
public final ObservableList<E> get() {
if (!valid) {
value = computeValue();
valid = true;
if (value != null) {
value.addListener(listChangeListener);
}
}
return value;
}
/**
* The method onInvalidating() can be overridden by extending classes to
* react, if this binding becomes invalid. The default implementation is
* empty.
*/
protected void onInvalidating() {
}
private void invalidateProperties() {
if (size0 != null) {
size0.fireValueChangedEvent();
}
if (empty0 != null) {
empty0.fireValueChangedEvent();
}
}
@Override
public final void invalidate() {
if (valid) {
if (value != null) {
value.removeListener(listChangeListener);
}
valid = false;
invalidateProperties();
onInvalidating();
ListExpressionHelper.fireValueChangedEvent(helper);
}
}
@Override
public final boolean isValid() {
return valid;
}
/**
* Calculates the current value of this binding.
* <p>
* Classes extending {@code ListBinding} have to provide an implementation
* of {@code computeValue}.
*
* @return the current value
*/
protected abstract ObservableList<E> computeValue();
/**
* Returns a string representation of this {@code ListBinding} object.
* @return a string representation of this {@code ListBinding} object.
*/
@Override
public String toString() {
return valid ? "ListBinding [value: " + get() + "]"
: "ListBinding [invalid]";
}
}

View File

@ -0,0 +1,339 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyBooleanProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyIntegerProperty;
import com.tungsten.fclcore.fakefx.beans.value.ObservableIntegerValue;
import com.tungsten.fclcore.fakefx.beans.value.ObservableListValue;
import com.tungsten.fclcore.fakefx.binding.StringFormatter;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
public abstract class ListExpression<E> implements ObservableListValue<E> {
private static final ObservableList EMPTY_LIST = FXCollections.emptyObservableList();
/**
* Creates a default {@code ListExpression}.
*/
public ListExpression() {
}
@Override
public ObservableList<E> getValue() {
return get();
}
public static <E> ListExpression<E> listExpression(final ObservableListValue<E> value) {
if (value == null) {
throw new NullPointerException("List must be specified.");
}
return value instanceof ListExpression ? (ListExpression<E>) value
: new ListBinding<E>() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected ObservableList<E> computeValue() {
return value.get();
}
@Override
public ObservableList<ObservableListValue<E>> getDependencies() {
return FXCollections.singletonObservableList(value);
}
};
}
/**
* The size of the list
* @return the size
*/
public int getSize() {
return size();
}
/**
* An integer property that represents the size of the list.
* @return the property
*/
public abstract ReadOnlyIntegerProperty sizeProperty();
/**
* A boolean property that is {@code true}, if the list is empty.
* @return the {@code ReadOnlyBooleanProperty}
*
*/
public abstract ReadOnlyBooleanProperty emptyProperty();
/**
* Creates a new {@link ObjectBinding} that contains the element at the specified position.
* If {@code index} points behind the list, the {@code ObjectBinding} contains {@code null}.
*
* @param index the index of the element
* @return the {@code ObjectBinding}
* @throws IllegalArgumentException if {@code index < 0}
*/
public ObjectBinding<E> valueAt(int index) {
return Bindings.valueAt(this, index);
}
/**
* Creates a new {@link ObjectBinding} that contains the element at the specified position.
* If {@code index} points outside of the list, the {@code ObjectBinding} contains {@code null}.
*
* @param index the index of the element
* @return the {@code ObjectBinding}
* @throws NullPointerException if {@code index} is {@code null}
*/
public ObjectBinding<E> valueAt(ObservableIntegerValue index) {
return Bindings.valueAt(this, index);
}
/**
* Creates a new {@link BooleanBinding} that holds {@code true} if this list is equal to
* another {@link ObservableList}.
*
* @param other
* the other {@code ObservableList}
* @return the new {@code BooleanBinding}
* @throws NullPointerException
* if {@code other} is {@code null}
*/
public BooleanBinding isEqualTo(final ObservableList<?> other) {
return Bindings.equal(this, other);
}
/**
* Creates a new {@link BooleanBinding} that holds {@code true} if this list is not equal to
* another {@link ObservableList}.
*
* @param other
* the other {@code ObservableList}
* @return the new {@code BooleanBinding}
* @throws NullPointerException
* if {@code other} is {@code null}
*/
public BooleanBinding isNotEqualTo(final ObservableList<?> other) {
return Bindings.notEqual(this, other);
}
/**
* Creates a new {@link BooleanBinding} that holds {@code true} if the wrapped list is {@code null}.
*
* @return the new {@code BooleanBinding}
*/
public BooleanBinding isNull() {
return Bindings.isNull(this);
}
/**
* Creates a new {@link BooleanBinding} that holds {@code true} if the wrapped list is not {@code null}.
*
* @return the new {@code BooleanBinding}
*/
public BooleanBinding isNotNull() {
return Bindings.isNotNull(this);
}
public StringBinding asString() {
return (StringBinding) StringFormatter.convert(this);
}
@Override
public int size() {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.size() : list.size();
}
@Override
public boolean isEmpty() {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.isEmpty() : list.isEmpty();
}
@Override
public boolean contains(Object obj) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.contains(obj) : list.contains(obj);
}
@Override
public Iterator<E> iterator() {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.iterator() : list.iterator();
}
@Override
public Object[] toArray() {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.toArray() : list.toArray();
}
@Override
public <T> T[] toArray(T[] array) {
final ObservableList<E> list = get();
return (list == null)? (T[]) EMPTY_LIST.toArray(array) : list.toArray(array);
}
@Override
public boolean add(E element) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.add(element) : list.add(element);
}
@Override
public boolean remove(Object obj) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.remove(obj) : list.remove(obj);
}
@Override
public boolean containsAll(Collection<?> objects) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.contains(objects) : list.containsAll(objects);
}
@Override
public boolean addAll(Collection<? extends E> elements) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.addAll(elements) : list.addAll(elements);
}
@Override
public boolean addAll(int i, Collection<? extends E> elements) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.addAll(i, elements) : list.addAll(i, elements);
}
@Override
public boolean removeAll(Collection<?> objects) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.removeAll(objects) : list.removeAll(objects);
}
@Override
public boolean retainAll(Collection<?> objects) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.retainAll(objects) : list.retainAll(objects);
}
@Override
public void clear() {
final ObservableList<E> list = get();
if (list == null) {
EMPTY_LIST.clear();
} else {
list.clear();
}
}
@Override
public E get(int i) {
final ObservableList<E> list = get();
return (list == null)? (E) EMPTY_LIST.get(i) : list.get(i);
}
@Override
public E set(int i, E element) {
final ObservableList<E> list = get();
return (list == null)? (E) EMPTY_LIST.set(i, element) : list.set(i, element);
}
@Override
public void add(int i, E element) {
final ObservableList<E> list = get();
if (list == null) {
EMPTY_LIST.add(i, element);
} else {
list.add(i, element);
}
}
@Override
public E remove(int i) {
final ObservableList<E> list = get();
return (list == null)? (E) EMPTY_LIST.remove(i) : list.remove(i);
}
@Override
public int indexOf(Object obj) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.indexOf(obj) : list.indexOf(obj);
}
@Override
public int lastIndexOf(Object obj) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.lastIndexOf(obj) : list.lastIndexOf(obj);
}
@Override
public ListIterator<E> listIterator() {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.listIterator() : list.listIterator();
}
@Override
public ListIterator<E> listIterator(int i) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.listIterator(i) : list.listIterator(i);
}
@Override
public List<E> subList(int from, int to) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.subList(from, to) : list.subList(from, to);
}
@Override
public boolean addAll(E... elements) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.addAll(elements) : list.addAll(elements);
}
@Override
public boolean setAll(E... elements) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.setAll(elements) : list.setAll(elements);
}
@Override
public boolean setAll(Collection<? extends E> elements) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.setAll(elements) : list.setAll(elements);
}
@Override
public boolean removeAll(E... elements) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.removeAll(elements) : list.removeAll(elements);
}
@Override
public boolean retainAll(E... elements) {
final ObservableList<E> list = get();
return (list == null)? EMPTY_LIST.retainAll(elements) : list.retainAll(elements);
}
@Override
public void remove(int from, int to) {
final ObservableList<E> list = get();
if (list == null) {
EMPTY_LIST.remove(from, to);
} else {
list.remove(from, to);
}
}
}

View File

@ -0,0 +1,155 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.value.ChangeListener;
import com.tungsten.fclcore.fakefx.binding.BindingHelperObserver;
import com.tungsten.fclcore.fakefx.binding.ExpressionHelper;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
public abstract class LongBinding extends LongExpression implements
NumberBinding {
private long value;
private boolean valid = false;
private BindingHelperObserver observer;
private ExpressionHelper<Number> helper = null;
/**
* Creates a default {@code LongBinding}.
*/
public LongBinding() {
}
@Override
public void addListener(InvalidationListener listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
@Override
public void addListener(ChangeListener<? super Number> listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(ChangeListener<? super Number> listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
/**
* Start observing the dependencies for changes. If the value of one of the
* dependencies changes, the binding is marked as invalid.
*
* @param dependencies
* the dependencies to observe
*/
protected final void bind(Observable... dependencies) {
if ((dependencies != null) && (dependencies.length > 0)) {
if (observer == null) {
observer = new BindingHelperObserver(this);
}
for (final Observable dep : dependencies) {
dep.addListener(observer);
}
}
}
/**
* Stop observing the dependencies for changes.
*
* @param dependencies
* the dependencies to stop observing
*/
protected final void unbind(Observable... dependencies) {
if (observer != null) {
for (final Observable dep : dependencies) {
dep.removeListener(observer);
}
observer = null;
}
}
/**
* A default implementation of {@code dispose()} that is empty.
*/
@Override
public void dispose() {
}
/**
* A default implementation of {@code getDependencies()} that returns an
* empty {@link ObservableList}.
*
* @return an empty {@code ObservableList}
*/
@Override
public ObservableList<?> getDependencies() {
return FXCollections.emptyObservableList();
}
/**
* Returns the result of {@link #computeValue()}. The method
* {@code computeValue()} is only called if the binding is invalid. The
* result is cached and returned if the binding did not become invalid since
* the last call of {@code get()}.
*
* @return the current value
*/
@Override
public final long get() {
if (!valid) {
value = computeValue();
valid = true;
}
return value;
}
/**
* The method onInvalidating() can be overridden by extending classes to
* react, if this binding becomes invalid. The default implementation is
* empty.
*/
protected void onInvalidating() {
}
@Override
public final void invalidate() {
if (valid) {
valid = false;
onInvalidating();
ExpressionHelper.fireValueChangedEvent(helper);
}
}
@Override
public final boolean isValid() {
return valid;
}
/**
* Calculates the current value of this binding.
* <p>
* Classes extending {@code LongBinding} have to provide an implementation
* of {@code computeValue}.
*
* @return the current value
*/
protected abstract long computeValue();
/**
* Returns a string representation of this {@code LongBinding} object.
* @return a string representation of this {@code LongBinding} object.
*/
@Override
public String toString() {
return valid ? "LongBinding [value: " + get() + "]"
: "LongBinding [invalid]";
}
}

View File

@ -0,0 +1,200 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.value.ObservableLongValue;
import com.tungsten.fclcore.fakefx.beans.value.ObservableValue;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
public abstract class LongExpression extends NumberExpressionBase implements
ObservableLongValue {
/**
* Creates a default {@code LongExpression}.
*/
public LongExpression() {
}
@Override
public int intValue() {
return (int) get();
}
@Override
public long longValue() {
return get();
}
@Override
public float floatValue() {
return (float) get();
}
@Override
public double doubleValue() {
return (double) get();
}
@Override
public Long getValue() {
return get();
}
public static LongExpression longExpression(final ObservableLongValue value) {
if (value == null) {
throw new NullPointerException("Value must be specified.");
}
return (value instanceof LongExpression) ? (LongExpression) value
: new LongBinding() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected long computeValue() {
return value.get();
}
@Override
public ObservableList<ObservableLongValue> getDependencies() {
return FXCollections.singletonObservableList(value);
}
};
}
public static <T extends Number> LongExpression longExpression(final ObservableValue<T> value) {
if (value == null) {
throw new NullPointerException("Value must be specified.");
}
return (value instanceof LongExpression) ? (LongExpression) value
: new LongBinding() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected long computeValue() {
final T val = value.getValue();
return val == null ? 0L : val.longValue();
}
@Override
public ObservableList<ObservableValue<T>> getDependencies() {
return FXCollections.singletonObservableList(value);
}
};
}
@Override
public LongBinding negate() {
return (LongBinding) Bindings.negate(this);
}
@Override
public DoubleBinding add(final double other) {
return Bindings.add(this, other);
}
@Override
public FloatBinding add(final float other) {
return (FloatBinding) Bindings.add(this, other);
}
@Override
public LongBinding add(final long other) {
return (LongBinding) Bindings.add(this, other);
}
@Override
public LongBinding add(final int other) {
return (LongBinding) Bindings.add(this, other);
}
@Override
public DoubleBinding subtract(final double other) {
return Bindings.subtract(this, other);
}
@Override
public FloatBinding subtract(final float other) {
return (FloatBinding) Bindings.subtract(this, other);
}
@Override
public LongBinding subtract(final long other) {
return (LongBinding) Bindings.subtract(this, other);
}
@Override
public LongBinding subtract(final int other) {
return (LongBinding) Bindings.subtract(this, other);
}
@Override
public DoubleBinding multiply(final double other) {
return Bindings.multiply(this, other);
}
@Override
public FloatBinding multiply(final float other) {
return (FloatBinding) Bindings.multiply(this, other);
}
@Override
public LongBinding multiply(final long other) {
return (LongBinding) Bindings.multiply(this, other);
}
@Override
public LongBinding multiply(final int other) {
return (LongBinding) Bindings.multiply(this, other);
}
@Override
public DoubleBinding divide(final double other) {
return Bindings.divide(this, other);
}
@Override
public FloatBinding divide(final float other) {
return (FloatBinding) Bindings.divide(this, other);
}
@Override
public LongBinding divide(final long other) {
return (LongBinding) Bindings.divide(this, other);
}
@Override
public LongBinding divide(final int other) {
return (LongBinding) Bindings.divide(this, other);
}
public ObjectExpression<Long> asObject() {
return new ObjectBinding<Long>() {
{
bind(LongExpression.this);
}
@Override
public void dispose() {
unbind(LongExpression.this);
}
@Override
protected Long computeValue() {
return LongExpression.this.getValue();
}
};
}
}

View File

@ -0,0 +1,261 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyBooleanProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyBooleanPropertyBase;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyIntegerProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyIntegerPropertyBase;
import com.tungsten.fclcore.fakefx.beans.value.ChangeListener;
import com.tungsten.fclcore.fakefx.binding.BindingHelperObserver;
import com.tungsten.fclcore.fakefx.binding.MapExpressionHelper;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.MapChangeListener;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
import com.tungsten.fclcore.fakefx.collections.ObservableMap;
public abstract class MapBinding<K, V> extends MapExpression<K, V> implements Binding<ObservableMap<K, V>> {
private final MapChangeListener<K, V> mapChangeListener = new MapChangeListener<K, V>() {
@Override
public void onChanged(Change<? extends K, ? extends V> change) {
invalidateProperties();
onInvalidating();
MapExpressionHelper.fireValueChangedEvent(helper, change);
}
};
private ObservableMap<K, V> value;
private boolean valid = false;
private BindingHelperObserver observer;
private MapExpressionHelper<K, V> helper = null;
private SizeProperty size0;
private EmptyProperty empty0;
/**
* Creates a default {@code MapBinding}.
*/
public MapBinding() {
}
@Override
public ReadOnlyIntegerProperty sizeProperty() {
if (size0 == null) {
size0 = new SizeProperty();
}
return size0;
}
private class SizeProperty extends ReadOnlyIntegerPropertyBase {
@Override
public int get() {
return size();
}
@Override
public Object getBean() {
return MapBinding.this;
}
@Override
public String getName() {
return "size";
}
protected void fireValueChangedEvent() {
super.fireValueChangedEvent();
}
}
@Override
public ReadOnlyBooleanProperty emptyProperty() {
if (empty0 == null) {
empty0 = new EmptyProperty();
}
return empty0;
}
private class EmptyProperty extends ReadOnlyBooleanPropertyBase {
@Override
public boolean get() {
return isEmpty();
}
@Override
public Object getBean() {
return MapBinding.this;
}
@Override
public String getName() {
return "empty";
}
protected void fireValueChangedEvent() {
super.fireValueChangedEvent();
}
}
@Override
public void addListener(InvalidationListener listener) {
helper = MapExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper = MapExpressionHelper.removeListener(helper, listener);
}
@Override
public void addListener(ChangeListener<? super ObservableMap<K, V>> listener) {
helper = MapExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(ChangeListener<? super ObservableMap<K, V>> listener) {
helper = MapExpressionHelper.removeListener(helper, listener);
}
@Override
public void addListener(MapChangeListener<? super K, ? super V> listener) {
helper = MapExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(MapChangeListener<? super K, ? super V> listener) {
helper = MapExpressionHelper.removeListener(helper, listener);
}
/**
* Start observing the dependencies for changes. If the value of one of the
* dependencies changes, the binding is marked as invalid.
*
* @param dependencies
* the dependencies to observe
*/
protected final void bind(Observable... dependencies) {
if ((dependencies != null) && (dependencies.length > 0)) {
if (observer == null) {
observer = new BindingHelperObserver(this);
}
for (final Observable dep : dependencies) {
if (dep != null) {
dep.addListener(observer);
}
}
}
}
/**
* Stop observing the dependencies for changes.
*
* @param dependencies
* the dependencies to stop observing
*/
protected final void unbind(Observable... dependencies) {
if (observer != null) {
for (final Observable dep : dependencies) {
if (dep != null) {
dep.removeListener(observer);
}
}
observer = null;
}
}
/**
* A default implementation of {@code dispose()} that is empty.
*/
@Override
public void dispose() {
}
/**
* A default implementation of {@code getDependencies()} that returns an
* empty {@link ObservableList}.
*
* @return an empty {@code ObservableList}
*/
@Override
public ObservableList<?> getDependencies() {
return FXCollections.emptyObservableList();
}
/**
* Returns the result of {@link #computeValue()}. The method
* {@code computeValue()} is only called if the binding is invalid. The
* result is cached and returned if the binding did not become invalid since
* the last call of {@code get()}.
*
* @return the current value
*/
@Override
public final ObservableMap<K, V> get() {
if (!valid) {
value = computeValue();
valid = true;
if (value != null) {
value.addListener(mapChangeListener);
}
}
return value;
}
/**
* The method onInvalidating() can be overridden by extending classes to
* react, if this binding becomes invalid. The default implementation is
* empty.
*/
protected void onInvalidating() {
}
private void invalidateProperties() {
if (size0 != null) {
size0.fireValueChangedEvent();
}
if (empty0 != null) {
empty0.fireValueChangedEvent();
}
}
@Override
public final void invalidate() {
if (valid) {
if (value != null) {
value.removeListener(mapChangeListener);
}
valid = false;
invalidateProperties();
onInvalidating();
MapExpressionHelper.fireValueChangedEvent(helper);
}
}
@Override
public final boolean isValid() {
return valid;
}
/**
* Calculates the current value of this binding.
* <p>
* Classes extending {@code MapBinding} have to provide an implementation
* of {@code computeValue}.
*
* @return the current value
*/
protected abstract ObservableMap<K, V> computeValue();
/**
* Returns a string representation of this {@code MapBinding} object.
* @return a string representation of this {@code MapBinding} object.
*/
@Override
public String toString() {
return valid ? "MapBinding [value: " + get() + "]"
: "MapBinding [invalid]";
}
}

View File

@ -0,0 +1,257 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyBooleanProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyIntegerProperty;
import com.tungsten.fclcore.fakefx.beans.value.ObservableMapValue;
import com.tungsten.fclcore.fakefx.beans.value.ObservableValue;
import com.tungsten.fclcore.fakefx.binding.StringFormatter;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.MapChangeListener;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
import com.tungsten.fclcore.fakefx.collections.ObservableMap;
import java.util.*;
public abstract class MapExpression<K, V> implements ObservableMapValue<K, V> {
private static final ObservableMap EMPTY_MAP = new EmptyObservableMap();
private static class EmptyObservableMap<K, V> extends AbstractMap<K, V> implements ObservableMap<K, V> {
@Override
public Set<Entry<K, V>> entrySet() {
return Collections.emptySet();
}
@Override
public void addListener(MapChangeListener<? super K, ? super V> mapChangeListener) {
// no-op
}
@Override
public void removeListener(MapChangeListener<? super K, ? super V> mapChangeListener) {
// no-op
}
@Override
public void addListener(InvalidationListener listener) {
// no-op
}
@Override
public void removeListener(InvalidationListener listener) {
// no-op
}
}
@Override
public ObservableMap<K, V> getValue() {
return get();
}
/**
* Creates a default {@code MapExpression}.
*/
public MapExpression() {
}
public static <K, V> MapExpression<K, V> mapExpression(final ObservableMapValue<K, V> value) {
if (value == null) {
throw new NullPointerException("Map must be specified.");
}
return value instanceof MapExpression ? (MapExpression<K, V>) value
: new MapBinding<K, V>() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected ObservableMap<K, V> computeValue() {
return value.get();
}
@Override
public ObservableList<?> getDependencies() {
return FXCollections.singletonObservableList(value);
}
};
}
/**
* The size of the map
* @return the size
*/
public int getSize() {
return size();
}
/**
* An integer property that represents the size of the map.
* @return the property
*/
public abstract ReadOnlyIntegerProperty sizeProperty();
/**
* A boolean property that is {@code true}, if the map is empty.
* @return the {@code ReadOnlyBooleanProperty}
*/
public abstract ReadOnlyBooleanProperty emptyProperty();
/**
* Creates a new {@link ObjectBinding} that contains the mapping of the specified key.
*
* @param key the key of the mapping
* @return the {@code ObjectBinding}
*/
public ObjectBinding<V> valueAt(K key) {
return Bindings.valueAt(this, key);
}
/**
* Creates a new {@link ObjectBinding} that contains the mapping of the specified key.
*
* @param key the key of the mapping
* @return the {@code ObjectBinding}
* @throws NullPointerException if {@code key} is {@code null}
*/
public ObjectBinding<V> valueAt(ObservableValue<K> key) {
return Bindings.valueAt(this, key);
}
/**
* Creates a new {@link BooleanBinding} that holds {@code true} if this map is equal to
* another {@link ObservableMap}.
*
* @param other
* the other {@code ObservableMap}
* @return the new {@code BooleanBinding}
* @throws NullPointerException
* if {@code other} is {@code null}
*/
public BooleanBinding isEqualTo(final ObservableMap<?, ?> other) {
return Bindings.equal(this, other);
}
/**
* Creates a new {@link BooleanBinding} that holds {@code true} if this map is not equal to
* another {@link ObservableMap}.
*
* @param other
* the other {@code ObservableMap}
* @return the new {@code BooleanBinding}
* @throws NullPointerException
* if {@code other} is {@code null}
*/
public BooleanBinding isNotEqualTo(final ObservableMap<?, ?> other) {
return Bindings.notEqual(this, other);
}
/**
* Creates a new {@link BooleanBinding} that holds {@code true} if the wrapped map is {@code null}.
*
* @return the new {@code BooleanBinding}
*/
public BooleanBinding isNull() {
return Bindings.isNull(this);
}
/**
* Creates a new {@link BooleanBinding} that holds {@code true} if the wrapped map is not {@code null}.
*
* @return the new {@code BooleanBinding}
*/
public BooleanBinding isNotNull() {
return Bindings.isNotNull(this);
}
public StringBinding asString() {
return (StringBinding) StringFormatter.convert(this);
}
@Override
public int size() {
final ObservableMap<K, V> map = get();
return (map == null)? EMPTY_MAP.size() : map.size();
}
@Override
public boolean isEmpty() {
final ObservableMap<K, V> map = get();
return (map == null)? EMPTY_MAP.isEmpty() : map.isEmpty();
}
@Override
public boolean containsKey(Object obj) {
final ObservableMap<K, V> map = get();
return (map == null)? EMPTY_MAP.containsKey(obj) : map.containsKey(obj);
}
@Override
public boolean containsValue(Object obj) {
final ObservableMap<K, V> map = get();
return (map == null)? EMPTY_MAP.containsValue(obj) : map.containsValue(obj);
}
@Override
public V put(K key, V value) {
final ObservableMap<K, V> map = get();
return (map == null)? (V) EMPTY_MAP.put(key, value) : map.put(key, value);
}
@Override
public V remove(Object obj) {
final ObservableMap<K, V> map = get();
return (map == null)? (V) EMPTY_MAP.remove(obj) : map.remove(obj);
}
@Override
public void putAll(Map<? extends K, ? extends V> elements) {
final ObservableMap<K, V> map = get();
if (map == null) {
EMPTY_MAP.putAll(elements);
} else {
map.putAll(elements);
}
}
@Override
public void clear() {
final ObservableMap<K, V> map = get();
if (map == null) {
EMPTY_MAP.clear();
} else {
map.clear();
}
}
@Override
public Set<K> keySet() {
final ObservableMap<K, V> map = get();
return (map == null)? EMPTY_MAP.keySet() : map.keySet();
}
@Override
public Collection<V> values() {
final ObservableMap<K, V> map = get();
return (map == null)? EMPTY_MAP.values() : map.values();
}
@Override
public Set<Entry<K, V>> entrySet() {
final ObservableMap<K, V> map = get();
return (map == null)? EMPTY_MAP.entrySet() : map.entrySet();
}
@Override
public V get(Object key) {
final ObservableMap<K, V> map = get();
return (map == null)? (V) EMPTY_MAP.get(key) : map.get(key);
}
}

View File

@ -0,0 +1,4 @@
package com.tungsten.fclcore.fakefx.beans.binding;
public interface NumberBinding extends Binding<Number>, NumberExpression {
}

View File

@ -0,0 +1,165 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.value.ObservableNumberValue;
import java.util.Locale;
public interface NumberExpression extends ObservableNumberValue {
// ===============================================================
// Negation
NumberBinding negate();
// ===============================================================
// Plus
NumberBinding add(final ObservableNumberValue other);
NumberBinding add(final double other);
NumberBinding add(final float other);
NumberBinding add(final long other);
NumberBinding add(final int other);
// ===============================================================
// Minus
NumberBinding subtract(final ObservableNumberValue other);
NumberBinding subtract(final double other);
NumberBinding subtract(final float other);
NumberBinding subtract(final long other);
NumberBinding subtract(final int other);
// ===============================================================
// Times
NumberBinding multiply(final ObservableNumberValue other);
NumberBinding multiply(final double other);
NumberBinding multiply(final float other);
NumberBinding multiply(final long other);
NumberBinding multiply(final int other);
// ===============================================================
// DividedBy
NumberBinding divide(final ObservableNumberValue other);
NumberBinding divide(final double other);
NumberBinding divide(final float other);
NumberBinding divide(final long other);
NumberBinding divide(final int other);
// ===============================================================
// IsEqualTo
BooleanBinding isEqualTo(final ObservableNumberValue other);
BooleanBinding isEqualTo(final ObservableNumberValue other, double epsilon);
BooleanBinding isEqualTo(final double other, double epsilon);
BooleanBinding isEqualTo(final float other, double epsilon);
BooleanBinding isEqualTo(final long other);
BooleanBinding isEqualTo(final long other, double epsilon);
BooleanBinding isEqualTo(final int other);
BooleanBinding isEqualTo(final int other, double epsilon);
// ===============================================================
// IsNotEqualTo
BooleanBinding isNotEqualTo(final ObservableNumberValue other);
BooleanBinding isNotEqualTo(final ObservableNumberValue other,
double epsilon);
BooleanBinding isNotEqualTo(final double other, double epsilon);
BooleanBinding isNotEqualTo(final float other, double epsilon);
BooleanBinding isNotEqualTo(final long other);
BooleanBinding isNotEqualTo(final long other, double epsilon);
BooleanBinding isNotEqualTo(final int other);
BooleanBinding isNotEqualTo(final int other, double epsilon);
// ===============================================================
// IsGreaterThan
BooleanBinding greaterThan(final ObservableNumberValue other);
BooleanBinding greaterThan(final double other);
BooleanBinding greaterThan(final float other);
BooleanBinding greaterThan(final long other);
BooleanBinding greaterThan(final int other);
// ===============================================================
// IsLesserThan
BooleanBinding lessThan(final ObservableNumberValue other);
BooleanBinding lessThan(final double other);
BooleanBinding lessThan(final float other);
BooleanBinding lessThan(final long other);
BooleanBinding lessThan(final int other);
// ===============================================================
// IsGreaterThanOrEqualTo
BooleanBinding greaterThanOrEqualTo(final ObservableNumberValue other);
BooleanBinding greaterThanOrEqualTo(final double other);
BooleanBinding greaterThanOrEqualTo(final float other);
BooleanBinding greaterThanOrEqualTo(final long other);
BooleanBinding greaterThanOrEqualTo(final int other);
// ===============================================================
// IsLessThanOrEqualTo
BooleanBinding lessThanOrEqualTo(final ObservableNumberValue other);
BooleanBinding lessThanOrEqualTo(final double other);
BooleanBinding lessThanOrEqualTo(final float other);
BooleanBinding lessThanOrEqualTo(final long other);
BooleanBinding lessThanOrEqualTo(final int other);
// ===============================================================
// String conversions
StringBinding asString();
StringBinding asString(String format);
StringBinding asString(Locale locale, String format);
}

View File

@ -0,0 +1,292 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.value.ObservableDoubleValue;
import com.tungsten.fclcore.fakefx.beans.value.ObservableFloatValue;
import com.tungsten.fclcore.fakefx.beans.value.ObservableIntegerValue;
import com.tungsten.fclcore.fakefx.beans.value.ObservableLongValue;
import com.tungsten.fclcore.fakefx.beans.value.ObservableNumberValue;
import com.tungsten.fclcore.fakefx.binding.StringFormatter;
import java.util.Locale;
/**
* {@code NumberExpressionBase} contains convenience methods to generate bindings in a fluent style,
* that are common to all NumberExpression subclasses.
* <p>
* NumberExpressionBase serves as a place for common code of specific NumberExpression subclasses for the
* specific number type.
* @see IntegerExpression
* @see LongExpression
* @see FloatExpression
* @see DoubleExpression
* @since JavaFX 2.0
*/
public abstract class NumberExpressionBase implements NumberExpression {
/**
* Creates a default {@code NumberExpressionBase}.
*/
public NumberExpressionBase() {
}
public static <S extends Number> NumberExpressionBase numberExpression(
final ObservableNumberValue value) {
if (value == null) {
throw new NullPointerException("Value must be specified.");
}
NumberExpressionBase result = (NumberExpressionBase) ((value instanceof NumberExpressionBase) ? value
: (value instanceof ObservableIntegerValue) ? IntegerExpression
.integerExpression((ObservableIntegerValue) value)
: (value instanceof ObservableDoubleValue) ? DoubleExpression
.doubleExpression((ObservableDoubleValue) value)
: (value instanceof ObservableFloatValue) ? FloatExpression
.floatExpression((ObservableFloatValue) value)
: (value instanceof ObservableLongValue) ? LongExpression
.longExpression((ObservableLongValue) value)
: null);
if (result != null) {
return result;
} else {
throw new IllegalArgumentException("Unsupported Type");
}
}
@Override
public NumberBinding add(final ObservableNumberValue other) {
return Bindings.add(this, other);
}
@Override
public NumberBinding subtract(final ObservableNumberValue other) {
return Bindings.subtract(this, other);
}
@Override
public NumberBinding multiply(final ObservableNumberValue other) {
return Bindings.multiply(this, other);
}
@Override
public NumberBinding divide(final ObservableNumberValue other) {
return Bindings.divide(this, other);
}
// ===============================================================
// IsEqualTo
@Override
public BooleanBinding isEqualTo(final ObservableNumberValue other) {
return Bindings.equal(this, other);
}
@Override
public BooleanBinding isEqualTo(final ObservableNumberValue other,
double epsilon) {
return Bindings.equal(this, other, epsilon);
}
@Override
public BooleanBinding isEqualTo(final double other, double epsilon) {
return Bindings.equal(this, other, epsilon);
}
@Override
public BooleanBinding isEqualTo(final float other, double epsilon) {
return Bindings.equal(this, other, epsilon);
}
@Override
public BooleanBinding isEqualTo(final long other) {
return Bindings.equal(this, other);
}
@Override
public BooleanBinding isEqualTo(final long other, double epsilon) {
return Bindings.equal(this, other, epsilon);
}
@Override
public BooleanBinding isEqualTo(final int other) {
return Bindings.equal(this, other);
}
@Override
public BooleanBinding isEqualTo(final int other, double epsilon) {
return Bindings.equal(this, other, epsilon);
}
// ===============================================================
// IsNotEqualTo
@Override
public BooleanBinding isNotEqualTo(final ObservableNumberValue other) {
return Bindings.notEqual(this, other);
}
@Override
public BooleanBinding isNotEqualTo(final ObservableNumberValue other,
double epsilon) {
return Bindings.notEqual(this, other, epsilon);
}
@Override
public BooleanBinding isNotEqualTo(final double other, double epsilon) {
return Bindings.notEqual(this, other, epsilon);
}
@Override
public BooleanBinding isNotEqualTo(final float other, double epsilon) {
return Bindings.notEqual(this, other, epsilon);
}
@Override
public BooleanBinding isNotEqualTo(final long other) {
return Bindings.notEqual(this, other);
}
@Override
public BooleanBinding isNotEqualTo(final long other, double epsilon) {
return Bindings.notEqual(this, other, epsilon);
}
@Override
public BooleanBinding isNotEqualTo(final int other) {
return Bindings.notEqual(this, other);
}
@Override
public BooleanBinding isNotEqualTo(final int other, double epsilon) {
return Bindings.notEqual(this, other, epsilon);
}
// ===============================================================
// IsGreaterThan
@Override
public BooleanBinding greaterThan(final ObservableNumberValue other) {
return Bindings.greaterThan(this, other);
}
@Override
public BooleanBinding greaterThan(final double other) {
return Bindings.greaterThan(this, other);
}
@Override
public BooleanBinding greaterThan(final float other) {
return Bindings.greaterThan(this, other);
}
@Override
public BooleanBinding greaterThan(final long other) {
return Bindings.greaterThan(this, other);
}
@Override
public BooleanBinding greaterThan(final int other) {
return Bindings.greaterThan(this, other);
}
// ===============================================================
// IsLesserThan
@Override
public BooleanBinding lessThan(final ObservableNumberValue other) {
return Bindings.lessThan(this, other);
}
@Override
public BooleanBinding lessThan(final double other) {
return Bindings.lessThan(this, other);
}
@Override
public BooleanBinding lessThan(final float other) {
return Bindings.lessThan(this, other);
}
@Override
public BooleanBinding lessThan(final long other) {
return Bindings.lessThan(this, other);
}
@Override
public BooleanBinding lessThan(final int other) {
return Bindings.lessThan(this, other);
}
// ===============================================================
// IsGreaterThanOrEqualTo
@Override
public BooleanBinding greaterThanOrEqualTo(final ObservableNumberValue other) {
return Bindings.greaterThanOrEqual(this, other);
}
@Override
public BooleanBinding greaterThanOrEqualTo(final double other) {
return Bindings.greaterThanOrEqual(this, other);
}
@Override
public BooleanBinding greaterThanOrEqualTo(final float other) {
return Bindings.greaterThanOrEqual(this, other);
}
@Override
public BooleanBinding greaterThanOrEqualTo(final long other) {
return Bindings.greaterThanOrEqual(this, other);
}
@Override
public BooleanBinding greaterThanOrEqualTo(final int other) {
return Bindings.greaterThanOrEqual(this, other);
}
// ===============================================================
// IsLessThanOrEqualTo
@Override
public BooleanBinding lessThanOrEqualTo(final ObservableNumberValue other) {
return Bindings.lessThanOrEqual(this, other);
}
@Override
public BooleanBinding lessThanOrEqualTo(final double other) {
return Bindings.lessThanOrEqual(this, other);
}
@Override
public BooleanBinding lessThanOrEqualTo(final float other) {
return Bindings.lessThanOrEqual(this, other);
}
@Override
public BooleanBinding lessThanOrEqualTo(final long other) {
return Bindings.lessThanOrEqual(this, other);
}
@Override
public BooleanBinding lessThanOrEqualTo(final int other) {
return Bindings.lessThanOrEqual(this, other);
}
// ===============================================================
// String conversions
@Override
public StringBinding asString() {
return (StringBinding) StringFormatter.convert(this);
}
@Override
public StringBinding asString(String format) {
return (StringBinding) Bindings.format(format, this);
}
@Override
public StringBinding asString(Locale locale, String format) {
return (StringBinding) Bindings.format(locale, format, this);
}
}

View File

@ -0,0 +1,197 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.value.ChangeListener;
import com.tungsten.fclcore.fakefx.binding.BindingHelperObserver;
import com.tungsten.fclcore.fakefx.binding.ExpressionHelper;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
public abstract class ObjectBinding<T> extends ObjectExpression<T> implements
Binding<T> {
private T value;
private boolean valid = false;
private BindingHelperObserver observer;
private ExpressionHelper<T> helper = null;
/**
* Creates a default {@code ObjectBinding}.
*/
public ObjectBinding() {
}
@Override
public void addListener(InvalidationListener listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
@Override
public void addListener(ChangeListener<? super T> listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(ChangeListener<? super T> listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
/**
* Start observing the dependencies for changes. If the value of one of the
* dependencies changes, the binding is marked as invalid.
*
* @param dependencies
* the dependencies to observe
*/
protected final void bind(Observable... dependencies) {
if ((dependencies != null) && (dependencies.length > 0)) {
if (observer == null) {
observer = new BindingHelperObserver(this);
}
for (final Observable dep : dependencies) {
dep.addListener(observer);
}
}
}
/**
* Stop observing the dependencies for changes.
*
* @param dependencies
* the dependencies to stop observing
*/
protected final void unbind(Observable... dependencies) {
if (observer != null) {
for (final Observable dep : dependencies) {
dep.removeListener(observer);
}
observer = null;
}
}
/**
* A default implementation of {@code dispose()} that is empty.
*/
@Override
public void dispose() {
}
/**
* A default implementation of {@code getDependencies()} that returns an
* empty {@link ObservableList}.
*
* @return an empty {@code ObservableList}
*/
@Override
public ObservableList<?> getDependencies() {
return FXCollections.emptyObservableList();
}
/**
* Returns the result of {@link #computeValue()}. The method
* {@code computeValue()} is only called if the binding is invalid. The
* result is cached and returned if the binding did not become invalid since
* the last call of {@code get()}.
*
* @return the current value
*/
@Override
public final T get() {
if (!valid) {
T computed = computeValue();
if (!allowValidation()) {
return computed;
}
value = computed;
valid = true;
}
return value;
}
/**
* Called when this binding becomes invalid. Can be overridden by extending classes to react to the invalidation.
* The default implementation is empty.
*/
protected void onInvalidating() {
}
@Override
public final void invalidate() {
if (valid) {
valid = false;
onInvalidating();
ExpressionHelper.fireValueChangedEvent(helper);
/*
* Cached value should be cleared to avoid a strong reference to stale data,
* but only if this binding didn't become valid after firing the event:
*/
if (!valid) {
value = null;
}
}
}
@Override
public final boolean isValid() {
return valid;
}
/**
* Checks if the binding has at least one listener registered on it. This
* is useful for subclasses which want to conserve resources when not observed.
*
* @return {@code true} if this binding currently has one or more
* listeners registered on it, otherwise {@code false}
* @since 19
*/
protected final boolean isObserved() {
return helper != null;
}
/**
* Checks if the binding is allowed to become valid. Overriding classes can
* prevent a binding from becoming valid. This is useful in subclasses which
* do not always listen for invalidations of their dependencies and prefer to
* recompute the current value instead. This can also be useful if caching of
* the current computed value is not desirable.
* <p>
* The default implementation always allows bindings to become valid.
*
* @return {@code true} if this binding is allowed to become valid, otherwise
* {@code false}
* @since 19
*/
protected boolean allowValidation() {
return true;
}
/**
* Calculates the current value of this binding.
* <p>
* Classes extending {@code ObjectBinding} have to provide an implementation
* of {@code computeValue}.
*
* @return the current value
*/
protected abstract T computeValue();
/**
* Returns a string representation of this {@code ObjectBinding} object.
* @return a string representation of this {@code ObjectBinding} object.
*/
@Override
public String toString() {
return valid ? "ObjectBinding [value: " + get() + "]"
: "ObjectBinding [invalid]";
}
}

View File

@ -0,0 +1,114 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.value.ObservableObjectValue;
import com.tungsten.fclcore.fakefx.binding.StringFormatter;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
import java.util.Locale;
public abstract class ObjectExpression<T> implements ObservableObjectValue<T> {
@Override
public T getValue() {
return get();
}
/**
* Creates a default {@code ObjectExpression}.
*/
public ObjectExpression() {
}
public static <T> ObjectExpression<T> objectExpression(
final ObservableObjectValue<T> value) {
if (value == null) {
throw new NullPointerException("Value must be specified.");
}
return value instanceof ObjectExpression ? (ObjectExpression<T>) value
: new ObjectBinding<T>() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected T computeValue() {
return value.get();
}
@Override
public ObservableList<ObservableObjectValue<T>> getDependencies() {
return FXCollections.singletonObservableList(value);
}
};
}
public BooleanBinding isEqualTo(final ObservableObjectValue<?> other) {
return Bindings.equal(this, other);
}
/**
* Creates a new {@code BooleanExpression} that holds {@code true} if this
* {@code ObjectExpression} is equal to a constant value.
*
* @param other
* the constant value
* @return the new {@code BooleanExpression}
*/
public BooleanBinding isEqualTo(final Object other) {
return Bindings.equal(this, other);
}
public BooleanBinding isNotEqualTo(final ObservableObjectValue<?> other) {
return Bindings.notEqual(this, other);
}
/**
* Creates a new {@code BooleanExpression} that holds {@code true} if this
* {@code ObjectExpression} is not equal to a constant value.
*
* @param other
* the constant value
* @return the new {@code BooleanExpression}
*/
public BooleanBinding isNotEqualTo(final Object other) {
return Bindings.notEqual(this, other);
}
/**
* Creates a new {@link BooleanBinding} that holds {@code true} if this
* {@code ObjectExpression} is {@code null}.
*
* @return the new {@code BooleanBinding}
*/
public BooleanBinding isNull() {
return Bindings.isNull(this);
}
/**
* Creates a new {@link BooleanBinding} that holds {@code true} if this
* {@code ObjectExpression} is not {@code null}.
*
* @return the new {@code BooleanBinding}
*/
public BooleanBinding isNotNull() {
return Bindings.isNotNull(this);
}
public StringBinding asString() {
return (StringBinding) StringFormatter.convert(this);
}
public StringBinding asString(String format) {
return (StringBinding) Bindings.format(format, this);
}
public StringBinding asString(Locale locale, String format) {
return (StringBinding) Bindings.format(locale, format, this);
}
}

View File

@ -0,0 +1,285 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyBooleanProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyBooleanPropertyBase;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyIntegerProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyIntegerPropertyBase;
import com.tungsten.fclcore.fakefx.beans.value.ChangeListener;
import com.tungsten.fclcore.fakefx.binding.BindingHelperObserver;
import com.tungsten.fclcore.fakefx.binding.SetExpressionHelper;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
import com.tungsten.fclcore.fakefx.collections.ObservableSet;
import com.tungsten.fclcore.fakefx.collections.SetChangeListener;
/**
* Base class that provides most of the functionality needed to implement a
* {@link Binding} of an {@link ObservableSet}.
* <p>
* {@code SetBinding} provides a simple invalidation-scheme. An extending
* class can register dependencies by calling {@link #bind(Observable...)}.
* If one of the registered dependencies becomes invalid, this
* {@code SetBinding} is marked as invalid. With
* {@link #unbind(Observable...)} listening to dependencies can be stopped.
* <p>
* To provide a concrete implementation of this class, the method
* {@link #computeValue()} has to be implemented to calculate the value of this
* binding based on the current state of the dependencies. It is called when
* {@link #get()} is called for an invalid binding.
* <p>
* See {@link DoubleBinding} for an example how this base class can be extended.
*
* @see Binding
* @see SetExpression
*
* @param <E>
* the type of the {@code Set} elements
* @since JavaFX 2.1
*/
public abstract class SetBinding<E> extends SetExpression<E> implements Binding<ObservableSet<E>> {
/**
* Creates a default {@code SetBinding}.
*/
public SetBinding() {
}
private final SetChangeListener<E> setChangeListener = new SetChangeListener<E>() {
@Override
public void onChanged(Change<? extends E> change) {
invalidateProperties();
onInvalidating();
SetExpressionHelper.fireValueChangedEvent(helper, change);
}
};
private ObservableSet<E> value;
private boolean valid = false;
private BindingHelperObserver observer;
private SetExpressionHelper<E> helper = null;
private SizeProperty size0;
private EmptyProperty empty0;
@Override
public ReadOnlyIntegerProperty sizeProperty() {
if (size0 == null) {
size0 = new SizeProperty();
}
return size0;
}
private class SizeProperty extends ReadOnlyIntegerPropertyBase {
@Override
public int get() {
return size();
}
@Override
public Object getBean() {
return SetBinding.this;
}
@Override
public String getName() {
return "size";
}
protected void fireValueChangedEvent() {
super.fireValueChangedEvent();
}
}
@Override
public ReadOnlyBooleanProperty emptyProperty() {
if (empty0 == null) {
empty0 = new EmptyProperty();
}
return empty0;
}
private class EmptyProperty extends ReadOnlyBooleanPropertyBase {
@Override
public boolean get() {
return isEmpty();
}
@Override
public Object getBean() {
return SetBinding.this;
}
@Override
public String getName() {
return "empty";
}
protected void fireValueChangedEvent() {
super.fireValueChangedEvent();
}
}
@Override
public void addListener(InvalidationListener listener) {
helper = SetExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper = SetExpressionHelper.removeListener(helper, listener);
}
@Override
public void addListener(ChangeListener<? super ObservableSet<E>> listener) {
helper = SetExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(ChangeListener<? super ObservableSet<E>> listener) {
helper = SetExpressionHelper.removeListener(helper, listener);
}
@Override
public void addListener(SetChangeListener<? super E> listener) {
helper = SetExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(SetChangeListener<? super E> listener) {
helper = SetExpressionHelper.removeListener(helper, listener);
}
/**
* Start observing the dependencies for changes. If the value of one of the
* dependencies changes, the binding is marked as invalid.
*
* @param dependencies
* the dependencies to observe
*/
protected final void bind(Observable... dependencies) {
if ((dependencies != null) && (dependencies.length > 0)) {
if (observer == null) {
observer = new BindingHelperObserver(this);
}
for (final Observable dep : dependencies) {
if (dep != null) {
dep.addListener(observer);
}
}
}
}
/**
* Stop observing the dependencies for changes.
*
* @param dependencies
* the dependencies to stop observing
*/
protected final void unbind(Observable... dependencies) {
if (observer != null) {
for (final Observable dep : dependencies) {
if (dep != null) {
dep.removeListener(observer);
}
}
observer = null;
}
}
/**
* A default implementation of {@code dispose()} that is empty.
*/
@Override
public void dispose() {
}
/**
* A default implementation of {@code getDependencies()} that returns an
* empty {@link ObservableList}.
*
* @return an empty {@code ObservableList}
*/
@Override
public ObservableList<?> getDependencies() {
return FXCollections.emptyObservableList();
}
/**
* Returns the result of {@link #computeValue()}. The method
* {@code computeValue()} is only called if the binding is invalid. The
* result is cached and returned if the binding did not become invalid since
* the last call of {@code get()}.
*
* @return the current value
*/
@Override
public final ObservableSet<E> get() {
if (!valid) {
value = computeValue();
valid = true;
if (value != null) {
value.addListener(setChangeListener);
}
}
return value;
}
/**
* The method onInvalidating() can be overridden by extending classes to
* react, if this binding becomes invalid. The default implementation is
* empty.
*/
protected void onInvalidating() {
}
private void invalidateProperties() {
if (size0 != null) {
size0.fireValueChangedEvent();
}
if (empty0 != null) {
empty0.fireValueChangedEvent();
}
}
@Override
public final void invalidate() {
if (valid) {
if (value != null) {
value.removeListener(setChangeListener);
}
valid = false;
invalidateProperties();
onInvalidating();
SetExpressionHelper.fireValueChangedEvent(helper);
}
}
@Override
public final boolean isValid() {
return valid;
}
/**
* Calculates the current value of this binding.
* <p>
* Classes extending {@code SetBinding} have to provide an implementation
* of {@code computeValue}.
*
* @return the current value
*/
protected abstract ObservableSet<E> computeValue();
/**
* Returns a string representation of this {@code SetBinding} object.
* @return a string representation of this {@code SetBinding} object.
*/
@Override
public String toString() {
return valid ? "SetBinding [value: " + get() + "]"
: "SetBinding [invalid]";
}
}

View File

@ -0,0 +1,263 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyBooleanProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyIntegerProperty;
import com.tungsten.fclcore.fakefx.beans.value.ObservableSetValue;
import com.tungsten.fclcore.fakefx.binding.StringFormatter;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
import com.tungsten.fclcore.fakefx.collections.ObservableSet;
import com.tungsten.fclcore.fakefx.collections.SetChangeListener;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
public abstract class SetExpression<E> implements ObservableSetValue<E> {
/**
* Creates a default {@code SetExpression}.
*/
public SetExpression() {
}
private static final ObservableSet EMPTY_SET = new EmptyObservableSet();
private static class EmptyObservableSet<E> extends AbstractSet<E> implements ObservableSet<E> {
private static final Iterator iterator = new Iterator() {
@Override
public boolean hasNext() {
return false;
}
@Override
public Object next() {
throw new NoSuchElementException();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
@Override
public Iterator<E> iterator() {
return iterator;
}
@Override
public int size() {
return 0;
}
@Override
public void addListener(SetChangeListener<? super E> setChangeListener) {
// no-op
}
@Override
public void removeListener(SetChangeListener<? super E> setChangeListener) {
// no-op
}
@Override
public void addListener(InvalidationListener listener) {
// no-op
}
@Override
public void removeListener(InvalidationListener listener) {
// no-op
}
}
@Override
public ObservableSet<E> getValue() {
return get();
}
public static <E> SetExpression<E> setExpression(final ObservableSetValue<E> value) {
if (value == null) {
throw new NullPointerException("Set must be specified.");
}
return value instanceof SetExpression ? (SetExpression<E>) value
: new SetBinding<E>() {
{
super.bind(value);
}
@Override
public void dispose() {
super.unbind(value);
}
@Override
protected ObservableSet<E> computeValue() {
return value.get();
}
@Override
public ObservableList<?> getDependencies() {
return FXCollections.singletonObservableList(value);
}
};
}
/**
* The size of the set
* @return the size
*/
public int getSize() {
return size();
}
/**
* An integer property that represents the size of the set.
* @return the property
*/
public abstract ReadOnlyIntegerProperty sizeProperty();
/**
* A boolean property that is {@code true}, if the set is empty.
* @return the {@code ReadOnlyBooleanProperty}
*/
public abstract ReadOnlyBooleanProperty emptyProperty();
/**
* Creates a new {@link BooleanBinding} that holds {@code true} if this set is equal to
* another {@link ObservableSet}.
*
* @param other
* the other {@code ObservableSet}
* @return the new {@code BooleanBinding}
* @throws NullPointerException
* if {@code other} is {@code null}
*/
public BooleanBinding isEqualTo(final ObservableSet<?> other) {
return Bindings.equal(this, other);
}
/**
* Creates a new {@link BooleanBinding} that holds {@code true} if this set is not equal to
* another {@link ObservableSet}.
*
* @param other
* the other {@code ObservableSet}
* @return the new {@code BooleanBinding}
* @throws NullPointerException
* if {@code other} is {@code null}
*/
public BooleanBinding isNotEqualTo(final ObservableSet<?> other) {
return Bindings.notEqual(this, other);
}
/**
* Creates a new {@link BooleanBinding} that holds {@code true} if the wrapped set is {@code null}.
*
* @return the new {@code BooleanBinding}
*/
public BooleanBinding isNull() {
return Bindings.isNull(this);
}
/**
* Creates a new {@link BooleanBinding} that holds {@code true} if the wrapped set is not {@code null}.
*
* @return the new {@code BooleanBinding}
*/
public BooleanBinding isNotNull() {
return Bindings.isNotNull(this);
}
public StringBinding asString() {
return (StringBinding) StringFormatter.convert(this);
}
@Override
public int size() {
final ObservableSet<E> set = get();
return (set == null)? EMPTY_SET.size() : set.size();
}
@Override
public boolean isEmpty() {
final ObservableSet<E> set = get();
return (set == null)? EMPTY_SET.isEmpty() : set.isEmpty();
}
@Override
public boolean contains(Object obj) {
final ObservableSet<E> set = get();
return (set == null)? EMPTY_SET.contains(obj) : set.contains(obj);
}
@Override
public Iterator<E> iterator() {
final ObservableSet<E> set = get();
return (set == null)? EMPTY_SET.iterator() : set.iterator();
}
@Override
public Object[] toArray() {
final ObservableSet<E> set = get();
return (set == null)? EMPTY_SET.toArray() : set.toArray();
}
@Override
public <T> T[] toArray(T[] array) {
final ObservableSet<E> set = get();
return (set == null)? (T[]) EMPTY_SET.toArray(array) : set.toArray(array);
}
@Override
public boolean add(E element) {
final ObservableSet<E> set = get();
return (set == null)? EMPTY_SET.add(element) : set.add(element);
}
@Override
public boolean remove(Object obj) {
final ObservableSet<E> set = get();
return (set == null)? EMPTY_SET.remove(obj) : set.remove(obj);
}
@Override
public boolean containsAll(Collection<?> objects) {
final ObservableSet<E> set = get();
return (set == null)? EMPTY_SET.contains(objects) : set.containsAll(objects);
}
@Override
public boolean addAll(Collection<? extends E> elements) {
final ObservableSet<E> set = get();
return (set == null)? EMPTY_SET.addAll(elements) : set.addAll(elements);
}
@Override
public boolean removeAll(Collection<?> objects) {
final ObservableSet<E> set = get();
return (set == null)? EMPTY_SET.removeAll(objects) : set.removeAll(objects);
}
@Override
public boolean retainAll(Collection<?> objects) {
final ObservableSet<E> set = get();
return (set == null)? EMPTY_SET.retainAll(objects) : set.retainAll(objects);
}
@Override
public void clear() {
final ObservableSet<E> set = get();
if (set == null) {
EMPTY_SET.clear();
} else {
set.clear();
}
}
}

View File

@ -0,0 +1,155 @@
package com.tungsten.fclcore.fakefx.beans.binding;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.value.ChangeListener;
import com.tungsten.fclcore.fakefx.binding.BindingHelperObserver;
import com.tungsten.fclcore.fakefx.binding.ExpressionHelper;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
public abstract class StringBinding extends StringExpression implements
Binding<String> {
private String value;
private boolean valid = false;
private BindingHelperObserver observer;
private ExpressionHelper<String> helper = null;
/**
* Creates a default {@code StringBinding}.
*/
public StringBinding() {
}
@Override
public void addListener(InvalidationListener listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
@Override
public void addListener(ChangeListener<? super String> listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(ChangeListener<? super String> listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
/**
* Start observing the dependencies for changes. If the value of one of the
* dependencies changes, the binding is marked as invalid.
*
* @param dependencies
* the dependencies to observe
*/
protected final void bind(Observable... dependencies) {
if ((dependencies != null) && (dependencies.length > 0)) {
if (observer == null) {
observer = new BindingHelperObserver(this);
}
for (final Observable dep : dependencies) {
dep.addListener(observer);
}
}
}
/**
* Stop observing the dependencies for changes.
*
* @param dependencies
* the dependencies to stop observing
*/
protected final void unbind(Observable... dependencies) {
if (observer != null) {
for (final Observable dep : dependencies) {
dep.removeListener(observer);
}
observer = null;
}
}
/**
* A default implementation of {@code dispose()} that is empty.
*/
@Override
public void dispose() {
}
/**
* A default implementation of {@code getDependencies()} that returns an
* empty {@link ObservableList}.
*
* @return an empty {@code ObservableList}
*/
@Override
public ObservableList<?> getDependencies() {
return FXCollections.emptyObservableList();
}
/**
* Returns the result of {@link #computeValue()}. The method
* {@code computeValue()} is only called if the binding is invalid. The
* result is cached and returned if the binding did not become invalid since
* the last call of {@code get()}.
*
* @return the current value
*/
@Override
public final String get() {
if (!valid) {
value = computeValue();
valid = true;
}
return value;
}
/**
* The method onInvalidating() can be overridden by extending classes to
* react, if this binding becomes invalid. The default implementation is
* empty.
*/
protected void onInvalidating() {
}
@Override
public final void invalidate() {
if (valid) {
valid = false;
onInvalidating();
ExpressionHelper.fireValueChangedEvent(helper);
}
}
@Override
public final boolean isValid() {
return valid;
}
/**
* Calculates the current value of this binding.
* <p>
* Classes extending {@code StringBinding} have to provide an implementation
* of {@code computeValue}.
*
* @return the current value
*/
protected abstract String computeValue();
/**
* Returns a string representation of this {@code StringBinding} object.
* @return a string representation of this {@code StringBinding} object.
*/
@Override
public String toString() {
return valid ? "StringBinding [value: " + get() + "]"
: "StringBinding [invalid]";
}
}

Some files were not shown because too many files have changed in this diff Show More